diff options
| author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-08-18 21:08:44 +0000 |
|---|---|---|
| committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-08-18 21:08:44 +0000 |
| commit | c75f38907c1b28adf5f57a8ad34df9f86c36d4e7 (patch) | |
| tree | 2ddd657750116ea9460f76f439780af3ab255edb | |
| parent | bc578c5f89ff9d8ec03fbbd014714f9d1e5cb172 (diff) | |
| download | gitlab-ce-c75f38907c1b28adf5f57a8ad34df9f86c36d4e7.tar.gz | |
Add latest changes from gitlab-org/gitlab@master
20 files changed, 404 insertions, 59 deletions
diff --git a/app/graphql/mutations/ci/job/cancel.rb b/app/graphql/mutations/ci/job/cancel.rb new file mode 100644 index 00000000000..dc9f4d19779 --- /dev/null +++ b/app/graphql/mutations/ci/job/cancel.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +module Mutations + module Ci + module Job + class Cancel < Base + graphql_name 'JobCancel' + + field :job, + Types::Ci::JobType, + null: true, + description: 'Job after the mutation.' + + authorize :update_build + + def resolve(id:) + job = authorized_find!(id: id) + + ::Ci::BuildCancelService.new(job, current_user).execute + { + job: job, + errors: errors_on_object(job) + } + end + end + end + end +end diff --git a/app/graphql/mutations/ci/job/unschedule.rb b/app/graphql/mutations/ci/job/unschedule.rb new file mode 100644 index 00000000000..07b1896bd2c --- /dev/null +++ b/app/graphql/mutations/ci/job/unschedule.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +module Mutations + module Ci + module Job + class Unschedule < Base + graphql_name 'JobUnschedule' + + field :job, + Types::Ci::JobType, + null: true, + description: 'Job after the mutation.' + + authorize :update_build + + def resolve(id:) + job = authorized_find!(id: id) + + ::Ci::BuildUnscheduleService.new(job, current_user).execute + { + job: job, + errors: errors_on_object(job) + } + end + end + end + end +end diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb index 530298ba07a..293d19d068a 100644 --- a/app/graphql/types/mutation_type.rb +++ b/app/graphql/types/mutation_type.rb @@ -99,6 +99,8 @@ module Types mount_mutation Mutations::Ci::CiCdSettingsUpdate mount_mutation Mutations::Ci::Job::Play mount_mutation Mutations::Ci::Job::Retry + mount_mutation Mutations::Ci::Job::Cancel + mount_mutation Mutations::Ci::Job::Unschedule mount_mutation Mutations::Ci::JobTokenScope::AddProject mount_mutation Mutations::Ci::JobTokenScope::RemoveProject mount_mutation Mutations::Ci::Runner::Update, feature_flag: :runner_graphql_query diff --git a/app/models/application_record.rb b/app/models/application_record.rb index d362cfac4be..d9375b55e89 100644 --- a/app/models/application_record.rb +++ b/app/models/application_record.rb @@ -63,11 +63,27 @@ class ApplicationRecord < ActiveRecord::Base end def self.safe_find_or_create_by(*args, &block) + return optimized_safe_find_or_create_by(*args, &block) if Feature.enabled?(:optimize_safe_find_or_create_by, default_enabled: :yaml) + safe_ensure_unique(retries: 1) do find_or_create_by(*args, &block) end end + def self.optimized_safe_find_or_create_by(*args, &block) + record = find_by(*args) + return record if record.present? + + # We need to use `all.create` to make this implementation follow `find_or_create_by` which delegates this in + # https://github.com/rails/rails/blob/v6.1.3.2/activerecord/lib/active_record/querying.rb#L22 + # + # When calling this method on an association, just calling `self.create` would call `ActiveRecord::Persistence.create` + # and that skips some code that adds the newly created record to the association. + transaction(requires_new: true) { all.create(*args, &block) } + rescue ActiveRecord::RecordNotUnique + find_by(*args) + end + def create_or_load_association(association_name) association(association_name).create unless association(association_name).loaded? rescue ActiveRecord::RecordNotUnique, PG::UniqueViolation diff --git a/app/models/integration.rb b/app/models/integration.rb index 5c4d03f1fa8..a9c865569d0 100644 --- a/app/models/integration.rb +++ b/app/models/integration.rb @@ -274,7 +274,7 @@ class Integration < ApplicationRecord end def self.closest_group_integration(type, scope) - group_ids = scope.ancestors.select(:id) + group_ids = scope.ancestors(hierarchy_order: :asc).select(:id) array = group_ids.to_sql.present? ? "array(#{group_ids.to_sql})" : 'ARRAY[]' where(type: type, group_id: group_ids, inherit_from_id: nil) diff --git a/app/models/namespaces/traversal/linear.rb b/app/models/namespaces/traversal/linear.rb index 081e51c1028..33e8c3e5172 100644 --- a/app/models/namespaces/traversal/linear.rb +++ b/app/models/namespaces/traversal/linear.rb @@ -177,7 +177,13 @@ module Namespaces if hierarchy_order depth_sql = "ABS(#{traversal_ids.count} - array_length(traversal_ids, 1))" skope = skope.select(skope.arel_table[Arel.star], "#{depth_sql} as depth") - .order(depth: hierarchy_order) + # The SELECT includes an extra depth attribute. We wrap the SQL in a + # standard SELECT to avoid mismatched attribute errors when trying to + # chain future ActiveRelation commands, and retain the ordering. + skope = self.class + .without_sti_condition + .from(skope, self.class.table_name) + .order(depth: hierarchy_order) end skope diff --git a/app/models/project.rb b/app/models/project.rb index 38c0ebcc222..81b04e1316c 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -914,7 +914,13 @@ class Project < ApplicationRecord .base_and_ancestors(upto: top, hierarchy_order: hierarchy_order) end - alias_method :ancestors, :ancestors_upto + def ancestors(hierarchy_order: nil) + if Feature.enabled?(:linear_project_ancestors, self, default_enabled: :yaml) + group&.self_and_ancestors(hierarchy_order: hierarchy_order) || Group.none + else + ancestors_upto(hierarchy_order: hierarchy_order) + end + end def ancestors_upto_ids(...) ancestors_upto(...).pluck(:id) diff --git a/config/feature_flags/development/linear_project_ancestors.yml b/config/feature_flags/development/linear_project_ancestors.yml new file mode 100644 index 00000000000..28c8fbcbf59 --- /dev/null +++ b/config/feature_flags/development/linear_project_ancestors.yml @@ -0,0 +1,8 @@ +--- +name: linear_project_ancestors +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/68072 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/338403 +milestone: '14.2' +type: development +group: group::access +default_enabled: false diff --git a/config/feature_flags/development/optimize_safe_find_or_create_by.yml b/config/feature_flags/development/optimize_safe_find_or_create_by.yml new file mode 100644 index 00000000000..6adedb5cece --- /dev/null +++ b/config/feature_flags/development/optimize_safe_find_or_create_by.yml @@ -0,0 +1,8 @@ +--- +name: optimize_safe_find_or_create_by +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/68458 +rollout_issue_url: +milestone: '14.3' +type: development +group: group::database +default_enabled: false diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 63781f9f353..54709f56eb8 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -2882,6 +2882,25 @@ Input type: `JiraImportUsersInput` | <a id="mutationjiraimportuserserrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. | | <a id="mutationjiraimportusersjirausers"></a>`jiraUsers` | [`[JiraUser!]`](#jirauser) | Users returned from Jira, matched by email and name if possible. | +### `Mutation.jobCancel` + +Input type: `JobCancelInput` + +#### Arguments + +| Name | Type | Description | +| ---- | ---- | ----------- | +| <a id="mutationjobcancelclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | +| <a id="mutationjobcancelid"></a>`id` | [`CiBuildID!`](#cibuildid) | ID of the job to mutate. | + +#### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| <a id="mutationjobcancelclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | +| <a id="mutationjobcancelerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. | +| <a id="mutationjobcanceljob"></a>`job` | [`CiJob`](#cijob) | Job after the mutation. | + ### `Mutation.jobPlay` Input type: `JobPlayInput` @@ -2920,6 +2939,25 @@ Input type: `JobRetryInput` | <a id="mutationjobretryerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. | | <a id="mutationjobretryjob"></a>`job` | [`CiJob`](#cijob) | Job after the mutation. | +### `Mutation.jobUnschedule` + +Input type: `JobUnscheduleInput` + +#### Arguments + +| Name | Type | Description | +| ---- | ---- | ----------- | +| <a id="mutationjobunscheduleclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | +| <a id="mutationjobunscheduleid"></a>`id` | [`CiBuildID!`](#cibuildid) | ID of the job to mutate. | + +#### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| <a id="mutationjobunscheduleclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | +| <a id="mutationjobunscheduleerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. | +| <a id="mutationjobunschedulejob"></a>`job` | [`CiJob`](#cijob) | Job after the mutation. | + ### `Mutation.labelCreate` Input type: `LabelCreateInput` diff --git a/doc/api/packages/debian.md b/doc/api/packages/debian.md index 0912f894fa3..154c99d7e0a 100644 --- a/doc/api/packages/debian.md +++ b/doc/api/packages/debian.md @@ -28,22 +28,10 @@ for details on which headers and token types are supported. ## Enable the Debian API -Debian repository support is still a work in progress. It's gated behind a feature flag that's -**disabled by default**. +The Debian API is behind a feature flag that is disabled by default. [GitLab administrators with access to the GitLab Rails console](../../administration/feature_flags.md) -can opt to enable it. - -To enable it: - -```ruby -Feature.enable(:debian_packages) -``` - -To disable it: - -```ruby -Feature.disable(:debian_packages) -``` +can opt to enable it. To enable it, follow the instructions in +[Enable the Debian API](../../user/packages/debian_repository/index.md#enable-the-debian-api). ## Enable the Debian group API diff --git a/doc/api/packages/debian_project_distributions.md b/doc/api/packages/debian_project_distributions.md index 16079d1c5ab..bedf3f1f27a 100644 --- a/doc/api/packages/debian_project_distributions.md +++ b/doc/api/packages/debian_project_distributions.md @@ -20,22 +20,10 @@ For more information about working with Debian packages, see the ## Enable the Debian API -Debian repository support is still a work in progress. It's gated behind a feature flag that's -**disabled by default**. +The Debian API is behind a feature flag that is disabled by default. [GitLab administrators with access to the GitLab Rails console](../../administration/feature_flags.md) -can opt to enable it. - -To enable it: - -```ruby -Feature.enable(:debian_packages) -``` - -To disable it: - -```ruby -Feature.disable(:debian_packages) -``` +can opt to enable it. To enable it, follow the instructions in +[Enable the Debian API](../../user/packages/debian_repository/index.md#enable-the-debian-api). ## List all Debian distributions in a project diff --git a/doc/development/sql.md b/doc/development/sql.md index 4f181d01505..40ee19c0b9e 100644 --- a/doc/development/sql.md +++ b/doc/development/sql.md @@ -429,14 +429,33 @@ Using transactions does not solve this problem. To solve this we've added the `ApplicationRecord.safe_find_or_create_by`. -This method can be used just as you would the normal -`find_or_create_by` but it wraps the call in a *new* transaction and +This method can be used the same way as +`find_or_create_by`, but it wraps the call in a *new* transaction (or a subtransaction) and retries if it were to fail because of an `ActiveRecord::RecordNotUnique` error. To be able to use this method, make sure the model you want to use this on inherits from `ApplicationRecord`. +In Rails 6 and later, there is a +[`.create_or_find_by`](https://api.rubyonrails.org/classes/ActiveRecord/Relation.html#method-i-create_or_find_by) +method. This method differs from our `.safe_find_or_create_by` methods +because it performs the `INSERT`, and then performs the `SELECT` commands only if that call +fails. + +If the `INSERT` fails, it will leave a dead tuple around and +increment the primary key sequence (if any), among [other downsides](https://api.rubyonrails.org/classes/ActiveRecord/Relation.html#method-i-create_or_find_by). + +We prefer `.safe_find_or_create_by` if the common path is that we +have a single record which is reused after it has first been created. +However, if the more common path is to create a new record, and we only +want to avoid duplicate records to be inserted on edge cases +(for example a job-retry), then `.create_or_find_by` can save us a `SELECT`. + +Both methods use subtransactions internally if executed within the context of +an existing transaction. This can significantly impact overall performance, +especially if more than 64 live subtransactions are being used inside a single transaction. + ## Monitor SQL queries in production GitLab team members can monitor slow or canceled queries on GitLab.com diff --git a/doc/user/admin_area/license.md b/doc/user/admin_area/license.md index 49ffc82982a..0431b0d1628 100644 --- a/doc/user/admin_area/license.md +++ b/doc/user/admin_area/license.md @@ -153,6 +153,16 @@ additional users. More information on how to determine the required number of us and how to add additional seats can be found in the [licensing FAQ](https://about.gitlab.com/pricing/licensing-faq/). +In GitLab 14.2 and later, for instances that use a license file, you can exceed the number of purchased users and still activate your license. + +- If the users over license are less than or equal to 10% of the users in the subscription, + the license is applied and the overage is paid in the next true-up. +- If the users over license are more than 10% of the users in the subscription, + you cannot apply the license without purchasing more users. + +For example, if you purchased a license for 100 users, you can have 110 users when you activate +your license. However, if you have 111, you must purchase more users before you can activate. + ### There is a connectivity issue In GitLab 14.1 and later, to activate your subscription, your GitLab instance must be connected to the internet. diff --git a/doc/user/project/deploy_tokens/index.md b/doc/user/project/deploy_tokens/index.md index 2f4eb75fe4c..70363b67c88 100644 --- a/doc/user/project/deploy_tokens/index.md +++ b/doc/user/project/deploy_tokens/index.md @@ -173,6 +173,16 @@ To use a group deploy token: The scopes applied to a group deploy token (such as `read_repository`) apply consistently when cloning the repository of related projects. +### Pull images from the Dependency Proxy + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/280586) in GitLab 14.2. + +To pull images from the Dependency Proxy, you must: + +1. Create a group deploy token with both `read_registry` and `write_registry` scopes. +1. Take note of your `username` and `token`. +1. Follow the Depenency Proxy [authentication instructions](../../packages/dependency_proxy/index.md). + ### GitLab deploy token > [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/18414) in GitLab 10.8. diff --git a/spec/models/application_record_spec.rb b/spec/models/application_record_spec.rb index dbef71431a2..f9a05c720a3 100644 --- a/spec/models/application_record_spec.rb +++ b/spec/models/application_record_spec.rb @@ -39,13 +39,14 @@ RSpec.describe ApplicationRecord do let(:suggestion_attributes) { attributes_for(:suggestion).merge!(note_id: note.id) } - describe '.safe_find_or_create_by' do + shared_examples '.safe_find_or_create_by' do it 'creates the suggestion avoiding race conditions' do - expect(Suggestion).to receive(:find_or_create_by).and_raise(ActiveRecord::RecordNotUnique) - allow(Suggestion).to receive(:find_or_create_by).and_call_original + existing_suggestion = double(:Suggestion) - expect { Suggestion.safe_find_or_create_by(suggestion_attributes) } - .to change { Suggestion.count }.by(1) + expect(Suggestion).to receive(:find_by).and_return(nil, existing_suggestion) + expect(Suggestion).to receive(:create).and_raise(ActiveRecord::RecordNotUnique) + + expect(Suggestion.safe_find_or_create_by(suggestion_attributes)).to eq(existing_suggestion) end it 'passes a block to find_or_create_by' do @@ -62,10 +63,8 @@ RSpec.describe ApplicationRecord do end end - describe '.safe_find_or_create_by!' do + shared_examples '.safe_find_or_create_by!' do it 'creates a record using safe_find_or_create_by' do - expect(Suggestion).to receive(:find_or_create_by).and_call_original - expect(Suggestion.safe_find_or_create_by!(suggestion_attributes)) .to be_a(Suggestion) end @@ -89,6 +88,24 @@ RSpec.describe ApplicationRecord do .to raise_error(ActiveRecord::RecordNotFound) end end + + context 'when optimized_safe_find_or_create_by is enabled' do + before do + stub_feature_flags(optimized_safe_find_or_create_by: true) + end + + it_behaves_like '.safe_find_or_create_by' + it_behaves_like '.safe_find_or_create_by!' + end + + context 'when optimized_safe_find_or_create_by is disabled' do + before do + stub_feature_flags(optimized_safe_find_or_create_by: false) + end + + it_behaves_like '.safe_find_or_create_by' + it_behaves_like '.safe_find_or_create_by!' + end end describe '.underscore' do diff --git a/spec/models/gpg_signature_spec.rb b/spec/models/gpg_signature_spec.rb index 997d9bbec72..7a1799c670e 100644 --- a/spec/models/gpg_signature_spec.rb +++ b/spec/models/gpg_signature_spec.rb @@ -54,8 +54,10 @@ RSpec.describe GpgSignature do end it 'does not raise an error in case of a race condition' do - expect(described_class).to receive(:find_or_create_by).and_raise(ActiveRecord::RecordNotUnique) - allow(described_class).to receive(:find_or_create_by).and_call_original + expect(described_class).to receive(:find_by).and_return(nil, double(described_class, persisted?: true)) + + expect(described_class).to receive(:create).and_raise(ActiveRecord::RecordNotUnique) + allow(described_class).to receive(:create).and_call_original described_class.safe_create!(attributes) end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 2945a842ebc..d8f3a63d221 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -6,6 +6,7 @@ RSpec.describe Project, factory_default: :keep do include ProjectForksHelper include GitHelpers include ExternalAuthorizationServiceHelpers + include ReloadHelpers using RSpec::Parameterized::TableSyntax let_it_be(:namespace) { create_default(:namespace).freeze } @@ -3021,29 +3022,106 @@ RSpec.describe Project, factory_default: :keep do end end - describe '#ancestors_upto' do - let_it_be(:parent) { create(:group) } - let_it_be(:child) { create(:group, parent: parent) } - let_it_be(:child2) { create(:group, parent: child) } - let_it_be(:project) { create(:project, namespace: child2) } + shared_context 'project with group ancestry' do + let(:parent) { create(:group) } + let(:child) { create(:group, parent: parent) } + let(:child2) { create(:group, parent: child) } + let(:project) { create(:project, namespace: child2) } + + before do + reload_models(parent, child, child2) + end + end + + shared_context 'project with namespace ancestry' do + let(:namespace) { create :namespace } + let(:project) { create :project, namespace: namespace } + end - it 'returns all ancestors when no namespace is given' do - expect(project.ancestors_upto).to contain_exactly(child2, child, parent) + shared_examples 'project with group ancestors' do + it 'returns all ancestors' do + is_expected.to contain_exactly(child2, child, parent) end + end + + shared_examples 'project with ordered group ancestors' do + let(:hierarchy_order) { :desc } - it 'includes ancestors upto but excluding the given ancestor' do - expect(project.ancestors_upto(parent)).to contain_exactly(child2, child) + it 'returns ancestors ordered by descending hierarchy' do + is_expected.to eq([parent, child, child2]) end + end + + shared_examples '#ancestors' do + context 'group ancestory' do + include_context 'project with group ancestry' - describe 'with hierarchy_order' do - it 'returns ancestors ordered by descending hierarchy' do - expect(project.ancestors_upto(hierarchy_order: :desc)).to eq([parent, child, child2]) + it_behaves_like 'project with group ancestors' do + subject { project.ancestors } end - it 'can be used with upto option' do - expect(project.ancestors_upto(parent, hierarchy_order: :desc)).to eq([child, child2]) + it_behaves_like 'project with ordered group ancestors' do + subject { project.ancestors(hierarchy_order: hierarchy_order) } end end + + context 'namespace ancestry' do + include_context 'project with namespace ancestry' + + subject { project.ancestors } + + it { is_expected.to be_empty } + end + end + + describe '#ancestors' do + context 'with linear_project_ancestors feature flag enabled' do + before do + stub_feature_flags(linear_project_ancestors: true) + end + + include_examples '#ancestors' + end + + context 'with linear_project_ancestors feature flag disabled' do + before do + stub_feature_flags(linear_project_ancestors: false) + end + + include_examples '#ancestors' + end + end + + describe '#ancestors_upto' do + context 'group ancestry' do + include_context 'project with group ancestry' + + it_behaves_like 'project with group ancestors' do + subject { project.ancestors_upto } + end + + it_behaves_like 'project with ordered group ancestors' do + subject { project.ancestors_upto(hierarchy_order: hierarchy_order) } + end + + it 'includes ancestors upto but excluding the given ancestor' do + expect(project.ancestors_upto(parent)).to contain_exactly(child2, child) + end + + describe 'with hierarchy_order' do + it 'can be used with upto option' do + expect(project.ancestors_upto(parent, hierarchy_order: :desc)).to eq([child, child2]) + end + end + end + + context 'namespace ancestry' do + include_context 'project with namespace ancestry' + + subject { project.ancestors_upto } + + it { is_expected.to be_empty } + end end describe '#root_ancestor' do diff --git a/spec/requests/api/graphql/mutations/ci/job_cancel_spec.rb b/spec/requests/api/graphql/mutations/ci/job_cancel_spec.rb new file mode 100644 index 00000000000..ee0f0a9bccb --- /dev/null +++ b/spec/requests/api/graphql/mutations/ci/job_cancel_spec.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe "JobCancel" do + include GraphqlHelpers + + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project) } + let_it_be(:pipeline) { create(:ci_pipeline, project: project, user: user) } + let_it_be(:job) { create(:ci_build, pipeline: pipeline, name: 'build') } + + let(:mutation) do + variables = { + id: job.to_global_id.to_s + } + graphql_mutation(:job_cancel, variables, + <<-QL + errors + job { + id + } + QL + ) + end + + let(:mutation_response) { graphql_mutation_response(:job_cancel) } + + it 'returns an error if the user is not allowed to cancel the job' do + project.add_developer(user) + post_graphql_mutation(mutation, current_user: user) + + expect(graphql_errors).not_to be_empty + end + + it 'cancels a job' do + job_id = ::Gitlab::GlobalId.build(job, id: job.id).to_s + project.add_maintainer(user) + post_graphql_mutation(mutation, current_user: user) + + expect(response).to have_gitlab_http_status(:success) + expect(mutation_response['job']['id']).to eq(job_id) + expect(job.reload.status).to eq('canceled') + end +end diff --git a/spec/requests/api/graphql/mutations/ci/job_unschedule_spec.rb b/spec/requests/api/graphql/mutations/ci/job_unschedule_spec.rb new file mode 100644 index 00000000000..4ddc019a2b5 --- /dev/null +++ b/spec/requests/api/graphql/mutations/ci/job_unschedule_spec.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'JobUnschedule' do + include GraphqlHelpers + + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project) } + let_it_be(:pipeline) { create(:ci_pipeline, project: project, user: user) } + let_it_be(:job) { create(:ci_build, :scheduled, pipeline: pipeline, name: 'build') } + + let(:mutation) do + variables = { + id: job.to_global_id.to_s + } + graphql_mutation(:job_unschedule, variables, + <<-QL + errors + job { + id + } + QL + ) + end + + let(:mutation_response) { graphql_mutation_response(:job_unschedule) } + + it 'returns an error if the user is not allowed to unschedule the job' do + project.add_developer(user) + + post_graphql_mutation(mutation, current_user: user) + + expect(graphql_errors).not_to be_empty + expect(job.reload.status).to eq('scheduled') + end + + it 'unschedules a job' do + project.add_maintainer(user) + + job_id = ::Gitlab::GlobalId.build(job, id: job.id).to_s + post_graphql_mutation(mutation, current_user: user) + + expect(response).to have_gitlab_http_status(:success) + expect(mutation_response['job']['id']).to eq(job_id) + expect(job.reload.status).to eq('manual') + end +end |
