diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-08-18 11:17:02 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-08-18 11:17:02 +0300 |
commit | b39512ed755239198a9c294b6a45e65c05900235 (patch) | |
tree | d234a3efade1de67c46b9e5a38ce813627726aa7 /spec/services | |
parent | d31474cf3b17ece37939d20082b07f6657cc79a9 (diff) |
Add latest changes from gitlab-org/gitlab@15-3-stable-eev15.3.0-rc42
Diffstat (limited to 'spec/services')
174 files changed, 4925 insertions, 2475 deletions
diff --git a/spec/services/alert_management/process_prometheus_alert_service_spec.rb b/spec/services/alert_management/process_prometheus_alert_service_spec.rb index 86a6cdee52d..ae52a09be48 100644 --- a/spec/services/alert_management/process_prometheus_alert_service_spec.rb +++ b/spec/services/alert_management/process_prometheus_alert_service_spec.rb @@ -44,6 +44,7 @@ RSpec.describe AlertManagement::ProcessPrometheusAlertService do end it_behaves_like 'processes new firing alert' + include_examples 'handles race condition in alert creation' context 'with resolving payload' do let(:prometheus_status) { 'resolved' } diff --git a/spec/services/audit_events/build_service_spec.rb b/spec/services/audit_events/build_service_spec.rb new file mode 100644 index 00000000000..caf405a53aa --- /dev/null +++ b/spec/services/audit_events/build_service_spec.rb @@ -0,0 +1,154 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe AuditEvents::BuildService do + let(:author) { build_stubbed(:author, current_sign_in_ip: '127.0.0.1') } + let(:deploy_token) { build_stubbed(:deploy_token, user: author) } + let(:scope) { build_stubbed(:group) } + let(:target) { build_stubbed(:project) } + let(:ip_address) { '192.168.8.8' } + let(:message) { 'Added an interesting field from project Gotham' } + let(:additional_details) { { action: :custom } } + + subject(:service) do + described_class.new( + author: author, + scope: scope, + target: target, + message: message, + additional_details: additional_details, + ip_address: ip_address + ) + end + + describe '#execute', :request_store do + subject(:event) { service.execute } + + before do + allow(Gitlab::RequestContext.instance).to receive(:client_ip).and_return(ip_address) + end + + it 'sets correct attributes', :aggregate_failures do + freeze_time do + expect(event).to have_attributes( + author_id: author.id, + author_name: author.name, + entity_id: scope.id, + entity_type: scope.class.name) + + expect(event.details).to eq( + author_name: author.name, + author_class: author.class.name, + target_id: target.id, + target_type: target.class.name, + target_details: target.name, + custom_message: message, + action: :custom) + + expect(event.ip_address).to be_nil + expect(event.created_at).to eq(DateTime.current) + end + end + + context 'when IP address is not provided' do + let(:ip_address) { nil } + + it 'uses author current_sign_in_ip' do + expect(event.ip_address).to be_nil + end + end + + context 'when overriding target details' do + subject(:service) do + described_class.new( + author: author, + scope: scope, + target: target, + message: message, + target_details: "This is my target details" + ) + end + + it 'uses correct target details' do + expect(event.target_details).to eq("This is my target details") + end + end + + context 'when deploy token is passed as author' do + let(:service) do + described_class.new( + author: deploy_token, + scope: scope, + target: target, + message: message + ) + end + + it 'expect author to be user' do + expect(event.author_id).to eq(-2) + expect(event.author_name).to eq(deploy_token.name) + end + end + + context 'when deploy key is passed as author' do + let(:deploy_key) { build_stubbed(:deploy_key, user: author) } + + let(:service) do + described_class.new( + author: deploy_key, + scope: scope, + target: target, + message: message + ) + end + + it 'expect author to be deploy key' do + expect(event.author_id).to eq(-3) + expect(event.author_name).to eq(deploy_key.name) + end + end + + context 'when author is passed as UnauthenticatedAuthor' do + let(:service) do + described_class.new( + author: ::Gitlab::Audit::UnauthenticatedAuthor.new, + scope: scope, + target: target, + message: message + ) + end + + it 'sets author as unauthenticated user' do + expect(event.author).to be_an_instance_of(::Gitlab::Audit::UnauthenticatedAuthor) + expect(event.author_name).to eq('An unauthenticated user') + end + end + + context 'when attributes are missing' do + context 'when author is missing' do + let(:author) { nil } + + it { expect { service }.to raise_error(described_class::MissingAttributeError) } + end + + context 'when scope is missing' do + let(:scope) { nil } + + it { expect { service }.to raise_error(described_class::MissingAttributeError) } + end + + context 'when target is missing' do + let(:target) { nil } + + it { expect { service }.to raise_error(described_class::MissingAttributeError) } + end + + context 'when message is missing' do + let(:message) { nil } + + it { expect { service }.to raise_error(described_class::MissingAttributeError) } + end + end + end +end diff --git a/spec/services/auto_merge/base_service_spec.rb b/spec/services/auto_merge/base_service_spec.rb index 3f535b83788..6c804a14620 100644 --- a/spec/services/auto_merge/base_service_spec.rb +++ b/spec/services/auto_merge/base_service_spec.rb @@ -254,7 +254,7 @@ RSpec.describe AutoMerge::BaseService do subject { service.abort(merge_request, reason) } let(:merge_request) { create(:merge_request, :merge_when_pipeline_succeeds) } - let(:reason) { 'an error'} + let(:reason) { 'an error' } it_behaves_like 'Canceled or Dropped' diff --git a/spec/services/auto_merge_service_spec.rb b/spec/services/auto_merge_service_spec.rb index 335c608c206..043b413acff 100644 --- a/spec/services/auto_merge_service_spec.rb +++ b/spec/services/auto_merge_service_spec.rb @@ -97,7 +97,7 @@ RSpec.describe AutoMergeService do end context 'when strategy is not present' do - let(:strategy) { } + let(:strategy) {} it 'returns nil' do is_expected.to be_nil @@ -140,7 +140,7 @@ RSpec.describe AutoMergeService do end context 'when strategy is not specified' do - let(:strategy) { } + let(:strategy) {} it 'chooses the most preferred strategy' do is_expected.to eq(:merge_when_pipeline_succeeds) diff --git a/spec/services/branches/create_service_spec.rb b/spec/services/branches/create_service_spec.rb index 0d2f5838574..26cc1a0665e 100644 --- a/spec/services/branches/create_service_spec.rb +++ b/spec/services/branches/create_service_spec.rb @@ -2,17 +2,155 @@ require 'spec_helper' -RSpec.describe Branches::CreateService do +RSpec.describe Branches::CreateService, :use_clean_rails_redis_caching do subject(:service) { described_class.new(project, user) } let_it_be(:project) { create(:project_empty_repo) } let_it_be(:user) { create(:user) } + describe '#bulk_create' do + subject { service.bulk_create(branches) } + + let_it_be(:project) { create(:project, :custom_repo, files: { 'foo/a.txt' => 'foo' }) } + + let(:branches) { { 'branch' => 'master', 'another_branch' => 'master' } } + + it 'creates two branches' do + expect(subject[:status]).to eq(:success) + expect(subject[:branches].map(&:name)).to match_array(%w[branch another_branch]) + + expect(project.repository.branch_exists?('branch')).to be_truthy + expect(project.repository.branch_exists?('another_branch')).to be_truthy + end + + context 'when branches are empty' do + let(:branches) { {} } + + it 'is successful' do + expect(subject[:status]).to eq(:success) + expect(subject[:branches]).to eq([]) + end + end + + context 'when incorrect reference is provided' do + let(:branches) { { 'new-feature' => 'unknown' } } + + before do + allow(project.repository).to receive(:add_branch).and_return(false) + end + + it 'returns an error with a reference name' do + err_msg = 'Failed to create branch \'new-feature\': invalid reference name \'unknown\'' + + expect(subject[:status]).to eq(:error) + expect(subject[:message]).to match_array([err_msg]) + end + end + + context 'when branch already exists' do + let(:branches) { { 'master' => 'master' } } + + it 'returns an error' do + expect(subject[:status]).to eq(:error) + expect(subject[:message]).to match_array(['Branch already exists']) + end + end + + context 'when an ambiguous branch name is provided' do + let(:branches) { { 'ambiguous/test' => 'master', 'ambiguous' => 'master' } } + + it 'returns an error that branch could not be created' do + err_msg = 'Failed to create branch \'ambiguous\': 13:reference is ambiguous.' + + expect(subject[:status]).to eq(:error) + expect(subject[:message]).to match_array([err_msg]) + end + end + + context 'when PreReceiveError exception' do + let(:branches) { { 'error' => 'master' } } + + it 'logs and returns an error if there is a PreReceiveError exception' do + error_message = 'pre receive error' + raw_message = "GitLab: #{error_message}" + pre_receive_error = Gitlab::Git::PreReceiveError.new(raw_message) + + allow(project.repository).to receive(:add_branch).and_raise(pre_receive_error) + + expect(Gitlab::ErrorTracking).to receive(:log_exception).with( + pre_receive_error, + pre_receive_message: raw_message, + branch_name: 'error', + ref: 'master' + ) + + expect(subject[:status]).to eq(:error) + expect(subject[:message]).to match_array([error_message]) + end + end + + context 'when multiple errors occur' do + let(:branches) { { 'master' => 'master', '' => 'master', 'failed_branch' => 'master' } } + + it 'returns all errors' do + allow(project.repository).to receive(:add_branch).with( + user, + 'failed_branch', + 'master', + expire_cache: false + ).and_return(false) + + expect(subject[:status]).to eq(:error) + expect(subject[:message]).to match_array( + [ + 'Branch already exists', + 'Branch name is invalid', + "Failed to create branch 'failed_branch': invalid reference name 'master'" + ] + ) + end + end + + context 'without N+1 for Redis cache' do + let(:branches) { { 'branch1' => 'master', 'branch2' => 'master', 'branch3' => 'master' } } + + it 'does not trigger Redis recreation' do + project.repository.expire_branches_cache + + control = RedisCommands::Recorder.new(pattern: ':branch_names:') { subject } + + expect(control.by_command(:sadd).count).to eq(1) + end + end + + context 'without N+1 branch cache expiration' do + let(:branches) { { 'branch_1' => 'master', 'branch_2' => 'master', 'branch_3' => 'master' } } + + it 'triggers branch cache expiration only once' do + expect(project.repository).to receive(:expire_branches_cache).once + + subject + end + + context 'when branches were not added' do + let(:branches) { { 'master' => 'master' } } + + it 'does not trigger branch expiration' do + expect(project.repository).not_to receive(:expire_branches_cache) + + subject + end + end + end + end + describe '#execute' do context 'when repository is empty' do it 'creates master branch' do - service.execute('my-feature', 'master') + result = service.execute('my-feature', 'master') + expect(result[:status]).to eq(:success) + expect(result[:branch].name).to eq('my-feature') expect(project.repository.branch_exists?('master')).to be_truthy end diff --git a/spec/services/bulk_imports/create_service_spec.rb b/spec/services/bulk_imports/create_service_spec.rb index 67ec6fee1ae..4b655dd5d6d 100644 --- a/spec/services/bulk_imports/create_service_spec.rb +++ b/spec/services/bulk_imports/create_service_spec.rb @@ -10,19 +10,19 @@ RSpec.describe BulkImports::CreateService do { source_type: 'group_entity', source_full_path: 'full/path/to/group1', - destination_name: 'destination group 1', + destination_slug: 'destination group 1', destination_namespace: 'full/path/to/destination1' }, { source_type: 'group_entity', source_full_path: 'full/path/to/group2', - destination_name: 'destination group 2', + destination_slug: 'destination group 2', destination_namespace: 'full/path/to/destination2' }, { source_type: 'project_entity', source_full_path: 'full/path/to/project1', - destination_name: 'destination project 1', + destination_slug: 'destination project 1', destination_namespace: 'full/path/to/destination1' } ] diff --git a/spec/services/bulk_imports/file_download_service_spec.rb b/spec/services/bulk_imports/file_download_service_spec.rb index bd664d6e996..81229cc8431 100644 --- a/spec/services/bulk_imports/file_download_service_spec.rb +++ b/spec/services/bulk_imports/file_download_service_spec.rb @@ -136,14 +136,45 @@ RSpec.describe BulkImports::FileDownloadService do end context 'when chunk code is not 200' do - let(:chunk_double) { double('chunk', size: 1000, code: 307) } + let(:chunk_double) { double('chunk', size: 1000, code: 500) } it 'raises an error' do expect { subject.execute }.to raise_error( described_class::ServiceError, - 'File download error 307' + 'File download error 500' ) end + + context 'when chunk code is redirection' do + let(:chunk_double) { double('redirection', size: 1000, code: 303) } + + it 'does not write a redirection chunk' do + expect { subject.execute }.not_to raise_error + + expect(File.read(filepath)).not_to include('redirection') + end + + context 'when redirection chunk appears at a later stage of the download' do + it 'raises an error' do + another_chunk_double = double('another redirection', size: 1000, code: 303) + data_chunk = double('data chunk', size: 1000, code: 200) + + allow_next_instance_of(BulkImports::Clients::HTTP) do |client| + allow(client).to receive(:head).and_return(response_double) + allow(client) + .to receive(:stream) + .and_yield(chunk_double) + .and_yield(data_chunk) + .and_yield(another_chunk_double) + end + + expect { subject.execute }.to raise_error( + described_class::ServiceError, + 'File download error 303' + ) + end + end + end end context 'when file is a symlink' do diff --git a/spec/services/bulk_update_integration_service_spec.rb b/spec/services/bulk_update_integration_service_spec.rb index e3e38aacaa2..7c5bd1db565 100644 --- a/spec/services/bulk_update_integration_service_spec.rb +++ b/spec/services/bulk_update_integration_service_spec.rb @@ -71,7 +71,7 @@ RSpec.describe BulkUpdateIntegrationService do context 'with integration with data fields' do let(:excluded_attributes) do - %w[id service_id created_at updated_at encrypted_properties encrypted_properties_iv] + %w[id integration_id created_at updated_at encrypted_properties encrypted_properties_iv] end it 'updates the data fields from the integration', :aggregate_failures do diff --git a/spec/services/ci/create_pipeline_service/evaluate_runner_tags_spec.rb b/spec/services/ci/create_pipeline_service/evaluate_runner_tags_spec.rb index 9add096d782..7c698242921 100644 --- a/spec/services/ci/create_pipeline_service/evaluate_runner_tags_spec.rb +++ b/spec/services/ci/create_pipeline_service/evaluate_runner_tags_spec.rb @@ -4,7 +4,7 @@ require 'spec_helper' RSpec.describe Ci::CreatePipelineService do let_it_be(:group) { create(:group, :private) } - let_it_be(:group_variable) { create(:ci_group_variable, group: group, key: 'RUNNER_TAG', value: 'group')} + let_it_be(:group_variable) { create(:ci_group_variable, group: group, key: 'RUNNER_TAG', value: 'group') } let_it_be(:project) { create(:project, :repository, group: group) } let_it_be(:user) { create(:user) } diff --git a/spec/services/ci/create_pipeline_service/parent_child_pipeline_spec.rb b/spec/services/ci/create_pipeline_service/parent_child_pipeline_spec.rb index 4326fa5533f..cc808b7e61c 100644 --- a/spec/services/ci/create_pipeline_service/parent_child_pipeline_spec.rb +++ b/spec/services/ci/create_pipeline_service/parent_child_pipeline_spec.rb @@ -36,7 +36,7 @@ RSpec.describe Ci::CreatePipelineService, '#execute' do expect(pipeline.statuses).to match_array [test, bridge] expect(bridge.options).to eq(expected_bridge_options) expect(bridge.yaml_variables) - .to include(key: 'CROSS', value: 'downstream', public: true) + .to include(key: 'CROSS', value: 'downstream') end end diff --git a/spec/services/ci/create_pipeline_service/rules_spec.rb b/spec/services/ci/create_pipeline_service/rules_spec.rb index d0ce1c5aba8..6e48141226d 100644 --- a/spec/services/ci/create_pipeline_service/rules_spec.rb +++ b/spec/services/ci/create_pipeline_service/rules_spec.rb @@ -7,10 +7,38 @@ RSpec.describe Ci::CreatePipelineService do let(:ref) { 'refs/heads/master' } let(:source) { :push } let(:service) { described_class.new(project, user, { ref: ref }) } - let(:pipeline) { service.execute(source).payload } + let(:response) { execute_service } + let(:pipeline) { response.payload } let(:build_names) { pipeline.builds.pluck(:name) } + def execute_service(before: '00000000', variables_attributes: nil) + params = { ref: ref, before: before, after: project.commit(ref).sha, variables_attributes: variables_attributes } + + described_class + .new(project, user, params) + .execute(source) do |pipeline| + yield(pipeline) if block_given? + end + end + context 'job:rules' do + let(:regular_job) { find_job('regular-job') } + let(:rules_job) { find_job('rules-job') } + let(:delayed_job) { find_job('delayed-job') } + + def find_job(name) + pipeline.builds.find_by(name: name) + end + + shared_examples 'rules jobs are excluded' do + it 'only persists the job without rules' do + expect(pipeline).to be_persisted + expect(regular_job).to be_persisted + expect(rules_job).to be_nil + expect(delayed_job).to be_nil + end + end + before do stub_ci_pipeline_yaml_file(config) allow_next_instance_of(Ci::BuildScheduleWorker) do |instance| @@ -95,10 +123,6 @@ RSpec.describe Ci::CreatePipelineService do end context 'with allow_failure and exit_codes', :aggregate_failures do - def find_job(name) - pipeline.builds.find_by(name: name) - end - let(:config) do <<-EOY job-1: @@ -280,6 +304,773 @@ RSpec.describe Ci::CreatePipelineService do end end end + + context 'with simple if: clauses' do + let(:config) do + <<-EOY + regular-job: + script: 'echo Hello, World!' + + master-job: + script: "echo hello world, $CI_COMMIT_REF_NAME" + rules: + - if: $CI_COMMIT_REF_NAME == "nonexistant-branch" + when: never + - if: $CI_COMMIT_REF_NAME =~ /master/ + when: manual + + negligible-job: + script: "exit 1" + rules: + - if: $CI_COMMIT_REF_NAME =~ /master/ + allow_failure: true + + delayed-job: + script: "echo See you later, World!" + rules: + - if: $CI_COMMIT_REF_NAME =~ /master/ + when: delayed + start_in: 1 hour + + never-job: + script: "echo Goodbye, World!" + rules: + - if: $CI_COMMIT_REF_NAME + when: never + EOY + end + + context 'with matches' do + it 'creates a pipeline with the vanilla and manual jobs' do + expect(pipeline).to be_persisted + expect(build_names).to contain_exactly( + 'regular-job', 'delayed-job', 'master-job', 'negligible-job' + ) + end + + it 'assigns job:when values to the builds' do + expect(find_job('regular-job').when).to eq('on_success') + expect(find_job('master-job').when).to eq('manual') + expect(find_job('negligible-job').when).to eq('on_success') + expect(find_job('delayed-job').when).to eq('delayed') + end + + it 'assigns job:allow_failure values to the builds' do + expect(find_job('regular-job').allow_failure).to eq(false) + expect(find_job('master-job').allow_failure).to eq(false) + expect(find_job('negligible-job').allow_failure).to eq(true) + expect(find_job('delayed-job').allow_failure).to eq(false) + end + + it 'assigns start_in for delayed jobs' do + expect(delayed_job.options[:start_in]).to eq('1 hour') + end + end + + context 'with no matches' do + let(:ref) { 'refs/heads/feature' } + + it_behaves_like 'rules jobs are excluded' + end + end + + context 'with complex if: clauses' do + let(:config) do + <<-EOY + regular-job: + script: 'echo Hello, World!' + rules: + - if: $VAR == 'present' && $OTHER || $CI_COMMIT_REF_NAME + when: manual + allow_failure: true + EOY + end + + it 'matches the first rule' do + expect(pipeline).to be_persisted + expect(build_names).to contain_exactly('regular-job') + expect(regular_job.when).to eq('manual') + expect(regular_job.allow_failure).to eq(true) + end + end + end + + context 'changes:' do + let(:config) do + <<-EOY + regular-job: + script: 'echo Hello, World!' + + rules-job: + script: "echo hello world, $CI_COMMIT_REF_NAME" + rules: + - changes: + - README.md + when: manual + - changes: + - app.rb + when: on_success + + delayed-job: + script: "echo See you later, World!" + rules: + - changes: + - README.md + when: delayed + start_in: 4 hours + + negligible-job: + script: "can be failed sometimes" + rules: + - changes: + - README.md + allow_failure: true + + README: + script: "I use variables for changes!" + rules: + - changes: + - $CI_JOB_NAME* + + changes-paths: + script: "I am using a new syntax!" + rules: + - changes: + paths: [README.md] + EOY + end + + context 'and matches' do + before do + allow_next_instance_of(Ci::Pipeline) do |pipeline| + allow(pipeline).to receive(:modified_paths).and_return(%w[README.md]) + end + end + + it 'creates five jobs' do + expect(pipeline).to be_persisted + expect(build_names).to contain_exactly( + 'regular-job', 'rules-job', 'delayed-job', 'negligible-job', 'README', 'changes-paths' + ) + end + + it 'sets when: for all jobs' do + expect(regular_job.when).to eq('on_success') + expect(rules_job.when).to eq('manual') + expect(delayed_job.when).to eq('delayed') + expect(delayed_job.options[:start_in]).to eq('4 hours') + end + + it 'sets allow_failure: for negligible job' do + expect(find_job('negligible-job').allow_failure).to eq(true) + end + end + + context 'and matches the second rule' do + before do + allow_next_instance_of(Ci::Pipeline) do |pipeline| + allow(pipeline).to receive(:modified_paths).and_return(%w[app.rb]) + end + end + + it 'includes both jobs' do + expect(pipeline).to be_persisted + expect(build_names).to contain_exactly('regular-job', 'rules-job') + end + + it 'sets when: for the created rules job based on the second clause' do + expect(regular_job.when).to eq('on_success') + expect(rules_job.when).to eq('on_success') + end + end + + context 'and does not match' do + before do + allow_next_instance_of(Ci::Pipeline) do |pipeline| + allow(pipeline).to receive(:modified_paths).and_return(%w[useless_script.rb]) + end + end + + it_behaves_like 'rules jobs are excluded' + + it 'sets when: for the created job' do + expect(regular_job.when).to eq('on_success') + end + end + + context 'with paths and compare_to' do + let_it_be(:project) { create(:project, :empty_repo) } + let_it_be(:user) { project.first_owner } + + before_all do + project.repository.add_branch(user, 'feature_1', 'master') + + project.repository.create_file( + user, 'file1.txt', 'file 1', message: 'Create file1.txt', branch_name: 'feature_1' + ) + + project.repository.add_branch(user, 'feature_2', 'feature_1') + + project.repository.create_file( + user, 'file2.txt', 'file 2', message: 'Create file2.txt', branch_name: 'feature_2' + ) + end + + let(:changed_file) { 'file2.txt' } + let(:ref) { 'feature_2' } + + let(:response) { execute_service(before: nil) } + + context 'for jobs rules' do + let(:config) do + <<-EOY + job1: + script: exit 0 + rules: + - changes: + paths: [#{changed_file}] + compare_to: #{compare_to} + + job2: + script: exit 0 + EOY + end + + context 'when there is no such compare_to ref' do + let(:compare_to) { 'invalid-branch' } + + it 'returns an error' do + expect(pipeline.errors.full_messages).to eq([ + 'Failed to parse rule for job1: rules:changes:compare_to is not a valid ref' + ]) + end + + context 'when the FF ci_rules_changes_compare is not enabled' do + before do + stub_feature_flags(ci_rules_changes_compare: false) + end + + it 'ignores compare_to and changes is always true' do + expect(build_names).to contain_exactly('job1', 'job2') + end + end + end + + context 'when the compare_to ref exists' do + let(:compare_to) { 'feature_1' } + + context 'when the rule matches' do + it 'creates job1 and job2' do + expect(build_names).to contain_exactly('job1', 'job2') + end + + context 'when the FF ci_rules_changes_compare is not enabled' do + before do + stub_feature_flags(ci_rules_changes_compare: false) + end + + it 'ignores compare_to and changes is always true' do + expect(build_names).to contain_exactly('job1', 'job2') + end + end + end + + context 'when the rule does not match' do + let(:changed_file) { 'file1.txt' } + + it 'does not create job1' do + expect(build_names).to contain_exactly('job2') + end + + context 'when the FF ci_rules_changes_compare is not enabled' do + before do + stub_feature_flags(ci_rules_changes_compare: false) + end + + it 'ignores compare_to and changes is always true' do + expect(build_names).to contain_exactly('job1', 'job2') + end + end + end + end + end + + context 'for workflow rules' do + let(:config) do + <<-EOY + workflow: + rules: + - changes: + paths: [#{changed_file}] + compare_to: #{compare_to} + + job1: + script: exit 0 + EOY + end + + let(:compare_to) { 'feature_1' } + + context 'when the rule matches' do + it 'creates job1' do + expect(pipeline).to be_created_successfully + expect(build_names).to contain_exactly('job1') + end + + context 'when the FF ci_rules_changes_compare is not enabled' do + before do + stub_feature_flags(ci_rules_changes_compare: false) + end + + it 'ignores compare_to and changes is always true' do + expect(pipeline).to be_created_successfully + expect(build_names).to contain_exactly('job1') + end + end + end + + context 'when the rule does not match' do + let(:changed_file) { 'file1.txt' } + + it 'does not create job1' do + expect(pipeline).not_to be_created_successfully + expect(build_names).to be_empty + end + end + end + end + end + + context 'mixed if: and changes: rules' do + let(:config) do + <<-EOY + regular-job: + script: 'echo Hello, World!' + + rules-job: + script: "echo hello world, $CI_COMMIT_REF_NAME" + allow_failure: true + rules: + - changes: + - README.md + when: manual + - if: $CI_COMMIT_REF_NAME == "master" + when: on_success + allow_failure: false + + delayed-job: + script: "echo See you later, World!" + rules: + - changes: + - README.md + when: delayed + start_in: 4 hours + allow_failure: true + - if: $CI_COMMIT_REF_NAME == "master" + when: delayed + start_in: 1 hour + EOY + end + + context 'and changes: matches before if' do + before do + allow_next_instance_of(Ci::Pipeline) do |pipeline| + allow(pipeline).to receive(:modified_paths).and_return(%w[README.md]) + end + end + + it 'creates two jobs' do + expect(pipeline).to be_persisted + expect(build_names) + .to contain_exactly('regular-job', 'rules-job', 'delayed-job') + end + + it 'sets when: for all jobs' do + expect(regular_job.when).to eq('on_success') + expect(rules_job.when).to eq('manual') + expect(delayed_job.when).to eq('delayed') + expect(delayed_job.options[:start_in]).to eq('4 hours') + end + + it 'sets allow_failure: for all jobs' do + expect(regular_job.allow_failure).to eq(false) + expect(rules_job.allow_failure).to eq(true) + expect(delayed_job.allow_failure).to eq(true) + end + end + + context 'and if: matches after changes' do + it 'includes both jobs' do + expect(pipeline).to be_persisted + expect(build_names).to contain_exactly('regular-job', 'rules-job', 'delayed-job') + end + + it 'sets when: for the created rules job based on the second clause' do + expect(regular_job.when).to eq('on_success') + expect(rules_job.when).to eq('on_success') + expect(delayed_job.when).to eq('delayed') + expect(delayed_job.options[:start_in]).to eq('1 hour') + end + end + + context 'and does not match' do + let(:ref) { 'refs/heads/wip' } + + it_behaves_like 'rules jobs are excluded' + + it 'sets when: for the created job' do + expect(regular_job.when).to eq('on_success') + end + end + end + + context 'mixed if: and changes: clauses' do + let(:config) do + <<-EOY + regular-job: + script: 'echo Hello, World!' + + rules-job: + script: "echo hello world, $CI_COMMIT_REF_NAME" + rules: + - if: $CI_COMMIT_REF_NAME =~ /master/ + changes: [README.md] + when: on_success + allow_failure: true + - if: $CI_COMMIT_REF_NAME =~ /master/ + changes: [app.rb] + when: manual + EOY + end + + context 'with if matches and changes matches' do + before do + allow_next_instance_of(Ci::Pipeline) do |pipeline| + allow(pipeline).to receive(:modified_paths).and_return(%w[app.rb]) + end + end + + it 'persists all jobs' do + expect(pipeline).to be_persisted + expect(regular_job).to be_persisted + expect(rules_job).to be_persisted + expect(rules_job.when).to eq('manual') + expect(rules_job.allow_failure).to eq(false) + end + end + + context 'with if matches and no change matches' do + it_behaves_like 'rules jobs are excluded' + end + + context 'with change matches and no if matches' do + let(:ref) { 'refs/heads/feature' } + + before do + allow_next_instance_of(Ci::Pipeline) do |pipeline| + allow(pipeline).to receive(:modified_paths).and_return(%w[README.md]) + end + end + + it_behaves_like 'rules jobs are excluded' + end + + context 'and no matches' do + let(:ref) { 'refs/heads/feature' } + + it_behaves_like 'rules jobs are excluded' + end + end + + context 'complex if: allow_failure usages' do + let(:config) do + <<-EOY + job-1: + script: "exit 1" + allow_failure: true + rules: + - if: $CI_COMMIT_REF_NAME =~ /master/ + allow_failure: false + + job-2: + script: "exit 1" + allow_failure: true + rules: + - if: $CI_COMMIT_REF_NAME =~ /nonexistant-branch/ + allow_failure: false + + job-3: + script: "exit 1" + rules: + - if: $CI_COMMIT_REF_NAME =~ /nonexistant-branch/ + allow_failure: true + + job-4: + script: "exit 1" + rules: + - if: $CI_COMMIT_REF_NAME =~ /master/ + allow_failure: false + + job-5: + script: "exit 1" + allow_failure: false + rules: + - if: $CI_COMMIT_REF_NAME =~ /master/ + allow_failure: true + + job-6: + script: "exit 1" + rules: + - if: $CI_COMMIT_REF_NAME =~ /nonexistant-branch/ + allow_failure: false + - allow_failure: true + EOY + end + + it 'creates a pipeline' do + expect(pipeline).to be_persisted + expect(build_names).to contain_exactly('job-1', 'job-4', 'job-5', 'job-6') + end + + it 'assigns job:allow_failure values to the builds' do + expect(find_job('job-1').allow_failure).to eq(false) + expect(find_job('job-4').allow_failure).to eq(false) + expect(find_job('job-5').allow_failure).to eq(true) + expect(find_job('job-6').allow_failure).to eq(true) + end + end + + context 'complex if: allow_failure & when usages' do + let(:config) do + <<-EOY + job-1: + script: "exit 1" + rules: + - if: $CI_COMMIT_REF_NAME =~ /master/ + when: manual + + job-2: + script: "exit 1" + rules: + - if: $CI_COMMIT_REF_NAME =~ /master/ + when: manual + allow_failure: true + + job-3: + script: "exit 1" + allow_failure: true + rules: + - if: $CI_COMMIT_REF_NAME =~ /master/ + when: manual + + job-4: + script: "exit 1" + allow_failure: true + rules: + - if: $CI_COMMIT_REF_NAME =~ /master/ + when: manual + allow_failure: false + + job-5: + script: "exit 1" + rules: + - if: $CI_COMMIT_REF_NAME =~ /nonexistant-branch/ + when: manual + allow_failure: false + - when: always + allow_failure: true + + job-6: + script: "exit 1" + allow_failure: false + rules: + - if: $CI_COMMIT_REF_NAME =~ /master/ + when: manual + + job-7: + script: "exit 1" + allow_failure: false + rules: + - if: $CI_COMMIT_REF_NAME =~ /nonexistant-branch/ + when: manual + - when: :on_failure + allow_failure: true + EOY + end + + it 'creates a pipeline' do + expect(pipeline).to be_persisted + expect(build_names).to contain_exactly( + 'job-1', 'job-2', 'job-3', 'job-4', 'job-5', 'job-6', 'job-7' + ) + end + + it 'assigns job:allow_failure values to the builds' do + expect(find_job('job-1').allow_failure).to eq(false) + expect(find_job('job-2').allow_failure).to eq(true) + expect(find_job('job-3').allow_failure).to eq(true) + expect(find_job('job-4').allow_failure).to eq(false) + expect(find_job('job-5').allow_failure).to eq(true) + expect(find_job('job-6').allow_failure).to eq(false) + expect(find_job('job-7').allow_failure).to eq(true) + end + + it 'assigns job:when values to the builds' do + expect(find_job('job-1').when).to eq('manual') + expect(find_job('job-2').when).to eq('manual') + expect(find_job('job-3').when).to eq('manual') + expect(find_job('job-4').when).to eq('manual') + expect(find_job('job-5').when).to eq('always') + expect(find_job('job-6').when).to eq('manual') + expect(find_job('job-7').when).to eq('on_failure') + end + end + + context 'deploy freeze period `if:` clause' do + # '0 23 * * 5' == "At 23:00 on Friday."", '0 7 * * 1' == "At 07:00 on Monday."" + let!(:freeze_period) { create(:ci_freeze_period, project: project, freeze_start: '0 23 * * 5', freeze_end: '0 7 * * 1') } + + context 'with 2 jobs' do + let(:config) do + <<-EOY + stages: + - test + - deploy + + test-job: + script: + - echo 'running TEST stage' + + deploy-job: + stage: deploy + script: + - echo 'running DEPLOY stage' + rules: + - if: $CI_DEPLOY_FREEZE == null + EOY + end + + context 'when outside freeze period' do + it 'creates two jobs' do + Timecop.freeze(2020, 4, 10, 22, 59) do + expect(pipeline).to be_persisted + expect(build_names).to contain_exactly('test-job', 'deploy-job') + end + end + end + + context 'when inside freeze period' do + it 'creates one job' do + Timecop.freeze(2020, 4, 10, 23, 1) do + expect(pipeline).to be_persisted + expect(build_names).to contain_exactly('test-job') + end + end + end + end + + context 'with 1 job' do + let(:config) do + <<-EOY + stages: + - deploy + + deploy-job: + stage: deploy + script: + - echo 'running DEPLOY stage' + rules: + - if: $CI_DEPLOY_FREEZE == null + EOY + end + + context 'when outside freeze period' do + it 'creates two jobs' do + Timecop.freeze(2020, 4, 10, 22, 59) do + expect(pipeline).to be_persisted + expect(build_names).to contain_exactly('deploy-job') + end + end + end + + context 'when inside freeze period' do + it 'does not create the pipeline', :aggregate_failures do + Timecop.freeze(2020, 4, 10, 23, 1) do + expect(response).to be_error + expect(pipeline).not_to be_persisted + end + end + end + end + end + + context 'with when:manual' do + let(:config) do + <<-EOY + job-with-rules: + script: 'echo hey' + rules: + - if: $CI_COMMIT_REF_NAME =~ /master/ + + job-when-with-rules: + script: 'echo hey' + when: manual + rules: + - if: $CI_COMMIT_REF_NAME =~ /master/ + + job-when-with-rules-when: + script: 'echo hey' + when: manual + rules: + - if: $CI_COMMIT_REF_NAME =~ /master/ + when: on_success + + job-with-rules-when: + script: 'echo hey' + rules: + - if: $CI_COMMIT_REF_NAME =~ /master/ + when: manual + + job-without-rules: + script: 'echo this is a job with NO rules' + EOY + end + + let(:job_with_rules) { find_job('job-with-rules') } + let(:job_when_with_rules) { find_job('job-when-with-rules') } + let(:job_when_with_rules_when) { find_job('job-when-with-rules-when') } + let(:job_with_rules_when) { find_job('job-with-rules-when') } + let(:job_without_rules) { find_job('job-without-rules') } + + context 'when matching the rules' do + let(:ref) { 'refs/heads/master' } + + it 'adds the job-with-rules with a when:manual' do + expect(job_with_rules).to be_persisted + expect(job_when_with_rules).to be_persisted + expect(job_when_with_rules_when).to be_persisted + expect(job_with_rules_when).to be_persisted + expect(job_without_rules).to be_persisted + + expect(job_with_rules.when).to eq('on_success') + expect(job_when_with_rules.when).to eq('manual') + expect(job_when_with_rules_when.when).to eq('on_success') + expect(job_with_rules_when.when).to eq('manual') + expect(job_without_rules.when).to eq('on_success') + end + end + + context 'when there is no match to the rule' do + let(:ref) { 'refs/heads/wip' } + + it 'does not add job_with_rules' do + expect(job_with_rules).to be_nil + expect(job_when_with_rules).to be_nil + expect(job_when_with_rules_when).to be_nil + expect(job_with_rules_when).to be_nil + expect(job_without_rules).to be_persisted + end + end end end @@ -447,5 +1238,232 @@ RSpec.describe Ci::CreatePipelineService do end end end + + context 'with persisted variables' do + let(:config) do + <<-EOY + workflow: + rules: + - if: $CI_COMMIT_REF_NAME == "master" + + regular-job: + script: 'echo Hello, World!' + EOY + end + + context 'with matches' do + it 'creates a pipeline' do + expect(pipeline).to be_persisted + expect(build_names).to contain_exactly('regular-job') + end + end + + context 'with no matches' do + let(:ref) { 'refs/heads/feature' } + + it 'does not create a pipeline', :aggregate_failures do + expect(response).to be_error + expect(pipeline).not_to be_persisted + end + end + end + + context 'with pipeline variables' do + let(:pipeline) do + execute_service(variables_attributes: variables_attributes).payload + end + + let(:config) do + <<-EOY + workflow: + rules: + - if: $SOME_VARIABLE + + regular-job: + script: 'echo Hello, World!' + EOY + end + + context 'with matches' do + let(:variables_attributes) do + [{ key: 'SOME_VARIABLE', secret_value: 'SOME_VAR' }] + end + + it 'creates a pipeline' do + expect(pipeline).to be_persisted + expect(build_names).to contain_exactly('regular-job') + end + end + + context 'with no matches' do + let(:variables_attributes) { {} } + + it 'does not create a pipeline', :aggregate_failures do + expect(response).to be_error + expect(pipeline).not_to be_persisted + end + end + end + + context 'with trigger variables' do + let(:pipeline) do + execute_service do |pipeline| + pipeline.variables.build(variables) + end.payload + end + + let(:config) do + <<-EOY + workflow: + rules: + - if: $SOME_VARIABLE + + regular-job: + script: 'echo Hello, World!' + EOY + end + + context 'with matches' do + let(:variables) do + [{ key: 'SOME_VARIABLE', secret_value: 'SOME_VAR' }] + end + + it 'creates a pipeline' do + expect(pipeline).to be_persisted + expect(build_names).to contain_exactly('regular-job') + end + + context 'when a job requires the same variable' do + let(:config) do + <<-EOY + workflow: + rules: + - if: $SOME_VARIABLE + + build: + stage: build + script: 'echo build' + rules: + - if: $SOME_VARIABLE + + test1: + stage: test + script: 'echo test1' + needs: [build] + + test2: + stage: test + script: 'echo test2' + EOY + end + + it 'creates a pipeline' do + expect(pipeline).to be_persisted + expect(build_names).to contain_exactly('build', 'test1', 'test2') + end + end + end + + context 'with no matches' do + let(:variables) { {} } + + it 'does not create a pipeline', :aggregate_failures do + expect(response).to be_error + expect(pipeline).not_to be_persisted + end + + context 'when a job requires the same variable' do + let(:config) do + <<-EOY + workflow: + rules: + - if: $SOME_VARIABLE + + build: + stage: build + script: 'echo build' + rules: + - if: $SOME_VARIABLE + + test1: + stage: test + script: 'echo test1' + needs: [build] + + test2: + stage: test + script: 'echo test2' + EOY + end + + it 'does not create a pipeline', :aggregate_failures do + expect(response).to be_error + expect(pipeline).not_to be_persisted + end + end + end + end + + context 'changes' do + shared_examples 'comparing file changes with workflow rules' do + context 'when matches' do + before do + allow_next_instance_of(Ci::Pipeline) do |pipeline| + allow(pipeline).to receive(:modified_paths).and_return(%w[file1.md]) + end + end + + it 'creates the pipeline with a job' do + expect(pipeline).to be_persisted + expect(build_names).to contain_exactly('job') + end + end + + context 'when does not match' do + before do + allow_next_instance_of(Ci::Pipeline) do |pipeline| + allow(pipeline).to receive(:modified_paths).and_return(%w[unknown]) + end + end + + it 'creates the pipeline with a job' do + expect(pipeline.errors.full_messages).to eq(['Pipeline filtered out by workflow rules.']) + expect(response).to be_error + expect(pipeline).not_to be_persisted + end + end + end + + context 'changes is an array' do + let(:config) do + <<-EOY + workflow: + rules: + - changes: [file1.md] + + job: + script: exit 0 + EOY + end + + it_behaves_like 'comparing file changes with workflow rules' + end + + context 'changes:paths is an array' do + let(:config) do + <<-EOY + workflow: + rules: + - changes: + paths: [file1.md] + + job: + script: exit 0 + EOY + end + + it_behaves_like 'comparing file changes with workflow rules' + end + end end end diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb index 9cef7f7dadb..a9442b0dc68 100644 --- a/spec/services/ci/create_pipeline_service_spec.rb +++ b/spec/services/ci/create_pipeline_service_spec.rb @@ -18,6 +18,7 @@ RSpec.describe Ci::CreatePipelineService do # rubocop:disable Metrics/ParameterLists def execute_service( source: :push, + before: '00000000', after: project.commit.id, ref: ref_name, trigger_request: nil, @@ -29,7 +30,7 @@ RSpec.describe Ci::CreatePipelineService do target_sha: nil, save_on_errors: true) params = { ref: ref, - before: '00000000', + before: before, after: after, variables_attributes: variables_attributes, push_options: push_options, @@ -1865,818 +1866,6 @@ RSpec.describe Ci::CreatePipelineService do end end end - - context 'when rules are used' do - let(:ref_name) { 'refs/heads/master' } - let(:response) { execute_service } - let(:pipeline) { response.payload } - let(:build_names) { pipeline.builds.pluck(:name) } - let(:regular_job) { find_job('regular-job') } - let(:rules_job) { find_job('rules-job') } - let(:delayed_job) { find_job('delayed-job') } - - context 'with when:manual' do - let(:config) do - <<-EOY - job-with-rules: - script: 'echo hey' - rules: - - if: $CI_COMMIT_REF_NAME =~ /master/ - - job-when-with-rules: - script: 'echo hey' - when: manual - rules: - - if: $CI_COMMIT_REF_NAME =~ /master/ - - job-when-with-rules-when: - script: 'echo hey' - when: manual - rules: - - if: $CI_COMMIT_REF_NAME =~ /master/ - when: on_success - - job-with-rules-when: - script: 'echo hey' - rules: - - if: $CI_COMMIT_REF_NAME =~ /master/ - when: manual - - job-without-rules: - script: 'echo this is a job with NO rules' - EOY - end - - let(:job_with_rules) { find_job('job-with-rules') } - let(:job_when_with_rules) { find_job('job-when-with-rules') } - let(:job_when_with_rules_when) { find_job('job-when-with-rules-when') } - let(:job_with_rules_when) { find_job('job-with-rules-when') } - let(:job_without_rules) { find_job('job-without-rules') } - - context 'when matching the rules' do - let(:ref_name) { 'refs/heads/master' } - - it 'adds the job-with-rules with a when:manual' do - expect(job_with_rules).to be_persisted - expect(job_when_with_rules).to be_persisted - expect(job_when_with_rules_when).to be_persisted - expect(job_with_rules_when).to be_persisted - expect(job_without_rules).to be_persisted - - expect(job_with_rules.when).to eq('on_success') - expect(job_when_with_rules.when).to eq('manual') - expect(job_when_with_rules_when.when).to eq('on_success') - expect(job_with_rules_when.when).to eq('manual') - expect(job_without_rules.when).to eq('on_success') - end - end - - context 'when there is no match to the rule' do - let(:ref_name) { 'refs/heads/wip' } - - it 'does not add job_with_rules' do - expect(job_with_rules).to be_nil - expect(job_when_with_rules).to be_nil - expect(job_when_with_rules_when).to be_nil - expect(job_with_rules_when).to be_nil - expect(job_without_rules).to be_persisted - end - end - end - - shared_examples 'rules jobs are excluded' do - it 'only persists the job without rules' do - expect(pipeline).to be_persisted - expect(regular_job).to be_persisted - expect(rules_job).to be_nil - expect(delayed_job).to be_nil - end - end - - def find_job(name) - pipeline.builds.find_by(name: name) - end - - before do - stub_ci_pipeline_yaml_file(config) - allow_any_instance_of(Ci::BuildScheduleWorker).to receive(:perform).and_return(true) - end - - context 'with simple if: clauses' do - let(:config) do - <<-EOY - regular-job: - script: 'echo Hello, World!' - - master-job: - script: "echo hello world, $CI_COMMIT_REF_NAME" - rules: - - if: $CI_COMMIT_REF_NAME == "nonexistant-branch" - when: never - - if: $CI_COMMIT_REF_NAME =~ /master/ - when: manual - - negligible-job: - script: "exit 1" - rules: - - if: $CI_COMMIT_REF_NAME =~ /master/ - allow_failure: true - - delayed-job: - script: "echo See you later, World!" - rules: - - if: $CI_COMMIT_REF_NAME =~ /master/ - when: delayed - start_in: 1 hour - - never-job: - script: "echo Goodbye, World!" - rules: - - if: $CI_COMMIT_REF_NAME - when: never - EOY - end - - context 'with matches' do - it 'creates a pipeline with the vanilla and manual jobs' do - expect(pipeline).to be_persisted - expect(build_names).to contain_exactly( - 'regular-job', 'delayed-job', 'master-job', 'negligible-job' - ) - end - - it 'assigns job:when values to the builds' do - expect(find_job('regular-job').when).to eq('on_success') - expect(find_job('master-job').when).to eq('manual') - expect(find_job('negligible-job').when).to eq('on_success') - expect(find_job('delayed-job').when).to eq('delayed') - end - - it 'assigns job:allow_failure values to the builds' do - expect(find_job('regular-job').allow_failure).to eq(false) - expect(find_job('master-job').allow_failure).to eq(false) - expect(find_job('negligible-job').allow_failure).to eq(true) - expect(find_job('delayed-job').allow_failure).to eq(false) - end - - it 'assigns start_in for delayed jobs' do - expect(delayed_job.options[:start_in]).to eq('1 hour') - end - end - - context 'with no matches' do - let(:ref_name) { 'refs/heads/feature' } - - it_behaves_like 'rules jobs are excluded' - end - end - - context 'with complex if: clauses' do - let(:config) do - <<-EOY - regular-job: - script: 'echo Hello, World!' - rules: - - if: $VAR == 'present' && $OTHER || $CI_COMMIT_REF_NAME - when: manual - allow_failure: true - EOY - end - - it 'matches the first rule' do - expect(pipeline).to be_persisted - expect(build_names).to contain_exactly('regular-job') - expect(regular_job.when).to eq('manual') - expect(regular_job.allow_failure).to eq(true) - end - end - - context 'with changes:' do - let(:config) do - <<-EOY - regular-job: - script: 'echo Hello, World!' - - rules-job: - script: "echo hello world, $CI_COMMIT_REF_NAME" - rules: - - changes: - - README.md - when: manual - - changes: - - app.rb - when: on_success - - delayed-job: - script: "echo See you later, World!" - rules: - - changes: - - README.md - when: delayed - start_in: 4 hours - - negligible-job: - script: "can be failed sometimes" - rules: - - changes: - - README.md - allow_failure: true - - README: - script: "I use variables for changes!" - rules: - - changes: - - $CI_JOB_NAME* - - changes-paths: - script: "I am using a new syntax!" - rules: - - changes: - paths: [README.md] - EOY - end - - context 'and matches' do - before do - allow_any_instance_of(Ci::Pipeline) - .to receive(:modified_paths).and_return(%w[README.md]) - end - - it 'creates five jobs' do - expect(pipeline).to be_persisted - expect(build_names).to contain_exactly( - 'regular-job', 'rules-job', 'delayed-job', 'negligible-job', 'README', 'changes-paths' - ) - end - - it 'sets when: for all jobs' do - expect(regular_job.when).to eq('on_success') - expect(rules_job.when).to eq('manual') - expect(delayed_job.when).to eq('delayed') - expect(delayed_job.options[:start_in]).to eq('4 hours') - end - - it 'sets allow_failure: for negligible job' do - expect(find_job('negligible-job').allow_failure).to eq(true) - end - end - - context 'and matches the second rule' do - before do - allow_any_instance_of(Ci::Pipeline) - .to receive(:modified_paths).and_return(%w[app.rb]) - end - - it 'includes both jobs' do - expect(pipeline).to be_persisted - expect(build_names).to contain_exactly('regular-job', 'rules-job') - end - - it 'sets when: for the created rules job based on the second clause' do - expect(regular_job.when).to eq('on_success') - expect(rules_job.when).to eq('on_success') - end - end - - context 'and does not match' do - before do - allow_any_instance_of(Ci::Pipeline) - .to receive(:modified_paths).and_return(%w[useless_script.rb]) - end - - it_behaves_like 'rules jobs are excluded' - - it 'sets when: for the created job' do - expect(regular_job.when).to eq('on_success') - end - end - end - - context 'with mixed if: and changes: rules' do - let(:config) do - <<-EOY - regular-job: - script: 'echo Hello, World!' - - rules-job: - script: "echo hello world, $CI_COMMIT_REF_NAME" - allow_failure: true - rules: - - changes: - - README.md - when: manual - - if: $CI_COMMIT_REF_NAME == "master" - when: on_success - allow_failure: false - - delayed-job: - script: "echo See you later, World!" - rules: - - changes: - - README.md - when: delayed - start_in: 4 hours - allow_failure: true - - if: $CI_COMMIT_REF_NAME == "master" - when: delayed - start_in: 1 hour - EOY - end - - context 'and changes: matches before if' do - before do - allow_any_instance_of(Ci::Pipeline) - .to receive(:modified_paths).and_return(%w[README.md]) - end - - it 'creates two jobs' do - expect(pipeline).to be_persisted - expect(build_names) - .to contain_exactly('regular-job', 'rules-job', 'delayed-job') - end - - it 'sets when: for all jobs' do - expect(regular_job.when).to eq('on_success') - expect(rules_job.when).to eq('manual') - expect(delayed_job.when).to eq('delayed') - expect(delayed_job.options[:start_in]).to eq('4 hours') - end - - it 'sets allow_failure: for all jobs' do - expect(regular_job.allow_failure).to eq(false) - expect(rules_job.allow_failure).to eq(true) - expect(delayed_job.allow_failure).to eq(true) - end - end - - context 'and if: matches after changes' do - it 'includes both jobs' do - expect(pipeline).to be_persisted - expect(build_names).to contain_exactly('regular-job', 'rules-job', 'delayed-job') - end - - it 'sets when: for the created rules job based on the second clause' do - expect(regular_job.when).to eq('on_success') - expect(rules_job.when).to eq('on_success') - expect(delayed_job.when).to eq('delayed') - expect(delayed_job.options[:start_in]).to eq('1 hour') - end - end - - context 'and does not match' do - let(:ref_name) { 'refs/heads/wip' } - - it_behaves_like 'rules jobs are excluded' - - it 'sets when: for the created job' do - expect(regular_job.when).to eq('on_success') - end - end - end - - context 'with mixed if: and changes: clauses' do - let(:config) do - <<-EOY - regular-job: - script: 'echo Hello, World!' - - rules-job: - script: "echo hello world, $CI_COMMIT_REF_NAME" - rules: - - if: $CI_COMMIT_REF_NAME =~ /master/ - changes: [README.md] - when: on_success - allow_failure: true - - if: $CI_COMMIT_REF_NAME =~ /master/ - changes: [app.rb] - when: manual - EOY - end - - context 'with if matches and changes matches' do - before do - allow_any_instance_of(Ci::Pipeline) - .to receive(:modified_paths).and_return(%w[app.rb]) - end - - it 'persists all jobs' do - expect(pipeline).to be_persisted - expect(regular_job).to be_persisted - expect(rules_job).to be_persisted - expect(rules_job.when).to eq('manual') - expect(rules_job.allow_failure).to eq(false) - end - end - - context 'with if matches and no change matches' do - it_behaves_like 'rules jobs are excluded' - end - - context 'with change matches and no if matches' do - let(:ref_name) { 'refs/heads/feature' } - - before do - allow_any_instance_of(Ci::Pipeline) - .to receive(:modified_paths).and_return(%w[README.md]) - end - - it_behaves_like 'rules jobs are excluded' - end - - context 'and no matches' do - let(:ref_name) { 'refs/heads/feature' } - - it_behaves_like 'rules jobs are excluded' - end - end - - context 'with complex if: allow_failure usages' do - let(:config) do - <<-EOY - job-1: - script: "exit 1" - allow_failure: true - rules: - - if: $CI_COMMIT_REF_NAME =~ /master/ - allow_failure: false - - job-2: - script: "exit 1" - allow_failure: true - rules: - - if: $CI_COMMIT_REF_NAME =~ /nonexistant-branch/ - allow_failure: false - - job-3: - script: "exit 1" - rules: - - if: $CI_COMMIT_REF_NAME =~ /nonexistant-branch/ - allow_failure: true - - job-4: - script: "exit 1" - rules: - - if: $CI_COMMIT_REF_NAME =~ /master/ - allow_failure: false - - job-5: - script: "exit 1" - allow_failure: false - rules: - - if: $CI_COMMIT_REF_NAME =~ /master/ - allow_failure: true - - job-6: - script: "exit 1" - rules: - - if: $CI_COMMIT_REF_NAME =~ /nonexistant-branch/ - allow_failure: false - - allow_failure: true - EOY - end - - it 'creates a pipeline' do - expect(pipeline).to be_persisted - expect(build_names).to contain_exactly('job-1', 'job-4', 'job-5', 'job-6') - end - - it 'assigns job:allow_failure values to the builds' do - expect(find_job('job-1').allow_failure).to eq(false) - expect(find_job('job-4').allow_failure).to eq(false) - expect(find_job('job-5').allow_failure).to eq(true) - expect(find_job('job-6').allow_failure).to eq(true) - end - end - - context 'with complex if: allow_failure & when usages' do - let(:config) do - <<-EOY - job-1: - script: "exit 1" - rules: - - if: $CI_COMMIT_REF_NAME =~ /master/ - when: manual - - job-2: - script: "exit 1" - rules: - - if: $CI_COMMIT_REF_NAME =~ /master/ - when: manual - allow_failure: true - - job-3: - script: "exit 1" - allow_failure: true - rules: - - if: $CI_COMMIT_REF_NAME =~ /master/ - when: manual - - job-4: - script: "exit 1" - allow_failure: true - rules: - - if: $CI_COMMIT_REF_NAME =~ /master/ - when: manual - allow_failure: false - - job-5: - script: "exit 1" - rules: - - if: $CI_COMMIT_REF_NAME =~ /nonexistant-branch/ - when: manual - allow_failure: false - - when: always - allow_failure: true - - job-6: - script: "exit 1" - allow_failure: false - rules: - - if: $CI_COMMIT_REF_NAME =~ /master/ - when: manual - - job-7: - script: "exit 1" - allow_failure: false - rules: - - if: $CI_COMMIT_REF_NAME =~ /nonexistant-branch/ - when: manual - - when: :on_failure - allow_failure: true - EOY - end - - it 'creates a pipeline' do - expect(pipeline).to be_persisted - expect(build_names).to contain_exactly( - 'job-1', 'job-2', 'job-3', 'job-4', 'job-5', 'job-6', 'job-7' - ) - end - - it 'assigns job:allow_failure values to the builds' do - expect(find_job('job-1').allow_failure).to eq(false) - expect(find_job('job-2').allow_failure).to eq(true) - expect(find_job('job-3').allow_failure).to eq(true) - expect(find_job('job-4').allow_failure).to eq(false) - expect(find_job('job-5').allow_failure).to eq(true) - expect(find_job('job-6').allow_failure).to eq(false) - expect(find_job('job-7').allow_failure).to eq(true) - end - - it 'assigns job:when values to the builds' do - expect(find_job('job-1').when).to eq('manual') - expect(find_job('job-2').when).to eq('manual') - expect(find_job('job-3').when).to eq('manual') - expect(find_job('job-4').when).to eq('manual') - expect(find_job('job-5').when).to eq('always') - expect(find_job('job-6').when).to eq('manual') - expect(find_job('job-7').when).to eq('on_failure') - end - end - - context 'with deploy freeze period `if:` clause' do - # '0 23 * * 5' == "At 23:00 on Friday."", '0 7 * * 1' == "At 07:00 on Monday."" - let!(:freeze_period) { create(:ci_freeze_period, project: project, freeze_start: '0 23 * * 5', freeze_end: '0 7 * * 1') } - - context 'with 2 jobs' do - let(:config) do - <<-EOY - stages: - - test - - deploy - - test-job: - script: - - echo 'running TEST stage' - - deploy-job: - stage: deploy - script: - - echo 'running DEPLOY stage' - rules: - - if: $CI_DEPLOY_FREEZE == null - EOY - end - - context 'when outside freeze period' do - it 'creates two jobs' do - Timecop.freeze(2020, 4, 10, 22, 59) do - expect(pipeline).to be_persisted - expect(build_names).to contain_exactly('test-job', 'deploy-job') - end - end - end - - context 'when inside freeze period' do - it 'creates one job' do - Timecop.freeze(2020, 4, 10, 23, 1) do - expect(pipeline).to be_persisted - expect(build_names).to contain_exactly('test-job') - end - end - end - end - - context 'with 1 job' do - let(:config) do - <<-EOY - stages: - - deploy - - deploy-job: - stage: deploy - script: - - echo 'running DEPLOY stage' - rules: - - if: $CI_DEPLOY_FREEZE == null - EOY - end - - context 'when outside freeze period' do - it 'creates two jobs' do - Timecop.freeze(2020, 4, 10, 22, 59) do - expect(pipeline).to be_persisted - expect(build_names).to contain_exactly('deploy-job') - end - end - end - - context 'when inside freeze period' do - it 'does not create the pipeline', :aggregate_failures do - Timecop.freeze(2020, 4, 10, 23, 1) do - expect(response).to be_error - expect(pipeline).not_to be_persisted - end - end - end - end - end - - context 'with workflow rules with persisted variables' do - let(:config) do - <<-EOY - workflow: - rules: - - if: $CI_COMMIT_REF_NAME == "master" - - regular-job: - script: 'echo Hello, World!' - EOY - end - - context 'with matches' do - it 'creates a pipeline' do - expect(pipeline).to be_persisted - expect(build_names).to contain_exactly('regular-job') - end - end - - context 'with no matches' do - let(:ref_name) { 'refs/heads/feature' } - - it 'does not create a pipeline', :aggregate_failures do - expect(response).to be_error - expect(pipeline).not_to be_persisted - end - end - end - - context 'with workflow rules with pipeline variables' do - let(:pipeline) do - execute_service(variables_attributes: variables_attributes).payload - end - - let(:config) do - <<-EOY - workflow: - rules: - - if: $SOME_VARIABLE - - regular-job: - script: 'echo Hello, World!' - EOY - end - - context 'with matches' do - let(:variables_attributes) do - [{ key: 'SOME_VARIABLE', secret_value: 'SOME_VAR' }] - end - - it 'creates a pipeline' do - expect(pipeline).to be_persisted - expect(build_names).to contain_exactly('regular-job') - end - end - - context 'with no matches' do - let(:variables_attributes) { {} } - - it 'does not create a pipeline', :aggregate_failures do - expect(response).to be_error - expect(pipeline).not_to be_persisted - end - end - end - - context 'with workflow rules with trigger variables' do - let(:pipeline) do - execute_service do |pipeline| - pipeline.variables.build(variables) - end.payload - end - - let(:config) do - <<-EOY - workflow: - rules: - - if: $SOME_VARIABLE - - regular-job: - script: 'echo Hello, World!' - EOY - end - - context 'with matches' do - let(:variables) do - [{ key: 'SOME_VARIABLE', secret_value: 'SOME_VAR' }] - end - - it 'creates a pipeline' do - expect(pipeline).to be_persisted - expect(build_names).to contain_exactly('regular-job') - end - - context 'when a job requires the same variable' do - let(:config) do - <<-EOY - workflow: - rules: - - if: $SOME_VARIABLE - - build: - stage: build - script: 'echo build' - rules: - - if: $SOME_VARIABLE - - test1: - stage: test - script: 'echo test1' - needs: [build] - - test2: - stage: test - script: 'echo test2' - EOY - end - - it 'creates a pipeline' do - expect(pipeline).to be_persisted - expect(build_names).to contain_exactly('build', 'test1', 'test2') - end - end - end - - context 'with no matches' do - let(:variables) { {} } - - it 'does not create a pipeline', :aggregate_failures do - expect(response).to be_error - expect(pipeline).not_to be_persisted - end - - context 'when a job requires the same variable' do - let(:config) do - <<-EOY - workflow: - rules: - - if: $SOME_VARIABLE - - build: - stage: build - script: 'echo build' - rules: - - if: $SOME_VARIABLE - - test1: - stage: test - script: 'echo test1' - needs: [build] - - test2: - stage: test - script: 'echo test2' - EOY - end - - it 'does not create a pipeline', :aggregate_failures do - expect(response).to be_error - expect(pipeline).not_to be_persisted - end - end - end - end - end end describe '#execute!' do diff --git a/spec/services/ci/deployments/destroy_service_spec.rb b/spec/services/ci/deployments/destroy_service_spec.rb new file mode 100644 index 00000000000..60a57c05728 --- /dev/null +++ b/spec/services/ci/deployments/destroy_service_spec.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ::Ci::Deployments::DestroyService do + let_it_be(:project) { create(:project, :repository) } + + let(:environment) { create(:environment, project: project) } + let(:commits) { project.repository.commits(nil, { limit: 3 }) } + let!(:deploy) do + create( + :deployment, + :success, + project: project, + environment: environment, + deployable: nil, + sha: commits[2].sha + ) + end + + let!(:running_deploy) do + create( + :deployment, + :running, + project: project, + environment: environment, + deployable: nil, + sha: commits[1].sha + ) + end + + let!(:old_deploy) do + create( + :deployment, + :success, + project: project, + environment: environment, + deployable: nil, + sha: commits[0].sha, + finished_at: 1.year.ago + ) + end + + let(:user) { project.first_owner } + + subject { described_class.new(project, user) } + + context 'when deleting a deployment' do + it 'delete is accepted for old deployment' do + expect(subject.execute(old_deploy)).to be_success + end + + it 'does not delete a running deployment' do + response = subject.execute(running_deploy) + expect(response).to be_an_error + expect(response.message).to eq("Cannot destroy running deployment") + end + + it 'does not delete the last deployment' do + response = subject.execute(deploy) + expect(response).to be_an_error + expect(response.message).to eq("Deployment currently deployed to environment") + end + end +end diff --git a/spec/services/ci/destroy_pipeline_service_spec.rb b/spec/services/ci/destroy_pipeline_service_spec.rb index 045051c7152..6bd7fe7559c 100644 --- a/spec/services/ci/destroy_pipeline_service_spec.rb +++ b/spec/services/ci/destroy_pipeline_service_spec.rb @@ -90,15 +90,23 @@ RSpec.describe ::Ci::DestroyPipelineService do end end - context 'when pipeline is in cancelable state' do - before do - allow(pipeline).to receive(:cancelable?).and_return(true) - end + context 'when pipeline is in cancelable state', :sidekiq_inline do + let!(:build) { create(:ci_build, :running, pipeline: pipeline) } + let!(:child_pipeline) { create(:ci_pipeline, :running, child_of: pipeline) } + let!(:child_build) { create(:ci_build, :running, pipeline: child_pipeline) } + + it 'cancels the pipelines sync' do + # turn off deletion for all instances of pipeline to allow for testing cancellation + allow(pipeline).to receive_message_chain(:reset, :destroy!) + allow_next_found_instance_of(Ci::Pipeline) { |p| allow(p).to receive_message_chain(:reset, :destroy!) } - it 'cancels the pipeline' do - expect(pipeline).to receive(:cancel_running) + # ensure cancellation happens sync so we accumulate minutes + expect(::Ci::CancelPipelineWorker).not_to receive(:perform) subject + + expect(build.reload.status).to eq('canceled') + expect(child_build.reload.status).to eq('canceled') end end end diff --git a/spec/services/ci/job_artifacts/create_service_spec.rb b/spec/services/ci/job_artifacts/create_service_spec.rb index b7a810ce47e..7b3f67b192f 100644 --- a/spec/services/ci/job_artifacts/create_service_spec.rb +++ b/spec/services/ci/job_artifacts/create_service_spec.rb @@ -34,6 +34,14 @@ RSpec.describe Ci::JobArtifacts::CreateService do subject { service.execute(artifacts_file, params, metadata_file: metadata_file) } context 'when artifacts file is uploaded' do + it 'logs the created artifact' do + expect(Gitlab::Ci::Artifacts::Logger) + .to receive(:log_created) + .with(an_instance_of(Ci::JobArtifact)) + + subject + end + it 'returns artifact in the response' do response = subject new_artifact = job.job_artifacts.last diff --git a/spec/services/ci/job_artifacts/destroy_batch_service_spec.rb b/spec/services/ci/job_artifacts/destroy_batch_service_spec.rb index 05069054483..9ca39d4d32e 100644 --- a/spec/services/ci/job_artifacts/destroy_batch_service_spec.rb +++ b/spec/services/ci/job_artifacts/destroy_batch_service_spec.rb @@ -40,7 +40,14 @@ RSpec.describe Ci::JobArtifacts::DestroyBatchService do expect { execute }.not_to change { artifact_with_file.file.exists? } end - it 'deletes the artifact records' do + it 'deletes the artifact records and logs them' do + expect(Gitlab::Ci::Artifacts::Logger) + .to receive(:log_deleted) + .with( + match_array([artifact_with_file, artifact_without_file]), + 'Ci::JobArtifacts::DestroyBatchService#execute' + ) + expect { subject }.to change { Ci::JobArtifact.count }.by(-2) end diff --git a/spec/services/ci/list_config_variables_service_spec.rb b/spec/services/ci/list_config_variables_service_spec.rb index 1735f4cfc97..4953b18bfcc 100644 --- a/spec/services/ci/list_config_variables_service_spec.rb +++ b/spec/services/ci/list_config_variables_service_spec.rb @@ -40,8 +40,8 @@ RSpec.describe Ci::ListConfigVariablesService, :use_clean_rails_memory_store_cac it 'returns variable list' do expect(subject['KEY1']).to eq({ value: 'val 1', description: 'description 1' }) expect(subject['KEY2']).to eq({ value: 'val 2', description: '' }) - expect(subject['KEY3']).to eq({ value: 'val 3', description: nil }) - expect(subject['KEY4']).to eq({ value: 'val 4', description: nil }) + expect(subject['KEY3']).to eq({ value: 'val 3' }) + expect(subject['KEY4']).to eq({ value: 'val 4' }) end end diff --git a/spec/services/ci/parse_dotenv_artifact_service_spec.rb b/spec/services/ci/parse_dotenv_artifact_service_spec.rb index aaab849cd93..7b3af33ac72 100644 --- a/spec/services/ci/parse_dotenv_artifact_service_spec.rb +++ b/spec/services/ci/parse_dotenv_artifact_service_spec.rb @@ -292,7 +292,7 @@ RSpec.describe Ci::ParseDotenvArtifactService do end context 'when build does not have a dotenv artifact' do - let!(:artifact) { } + let!(:artifact) {} it 'raises an error' do expect { subject }.to raise_error(ArgumentError) diff --git a/spec/services/ci/pipeline_processing/atomic_processing_service/status_collection_spec.rb b/spec/services/ci/pipeline_processing/atomic_processing_service/status_collection_spec.rb index 7868629d34d..289e004fcce 100644 --- a/spec/services/ci/pipeline_processing/atomic_processing_service/status_collection_spec.rb +++ b/spec/services/ci/pipeline_processing/atomic_processing_service/status_collection_spec.rb @@ -87,7 +87,7 @@ RSpec.describe Ci::PipelineProcessing::AtomicProcessingService::StatusCollection describe '#processing_processables' do it 'returns processables marked as processing' do - expect(collection.processing_processables.map { |processable| processable[:id]} ) + expect(collection.processing_processables.map { |processable| processable[:id] } ) .to contain_exactly(build_a.id, build_b.id, test_a.id, test_b.id, deploy.id) end end diff --git a/spec/services/ci/process_build_service_spec.rb b/spec/services/ci/process_build_service_spec.rb index b54fc45d36a..2fcb4ce73ff 100644 --- a/spec/services/ci/process_build_service_spec.rb +++ b/spec/services/ci/process_build_service_spec.rb @@ -101,7 +101,7 @@ RSpec.describe Ci::ProcessBuildService, '#execute' do context 'when build has delayed option' do before do - allow(Ci::BuildScheduleWorker).to receive(:perform_at) { } + allow(Ci::BuildScheduleWorker).to receive(:perform_at) {} end let(:build) { create(:ci_build, :created, :schedulable, user: user, project: project) } diff --git a/spec/services/ci/register_job_service_spec.rb b/spec/services/ci/register_job_service_spec.rb index 2316575f164..cabd60a22d1 100644 --- a/spec/services/ci/register_job_service_spec.rb +++ b/spec/services/ci/register_job_service_spec.rb @@ -129,6 +129,12 @@ module Ci let!(:build2_project2) { create(:ci_build, :pending, :queued, pipeline: pipeline2) } let!(:build1_project3) { create(:ci_build, :pending, :queued, pipeline: pipeline3) } + it 'picks builds one-by-one' do + expect(Ci::Build).to receive(:find).with(pending_job.id).and_call_original + + expect(execute(shared_runner)).to eq(build1_project1) + end + context 'when using fair scheduling' do context 'when all builds are pending' do it 'prefers projects without builds first' do @@ -485,6 +491,48 @@ module Ci end context 'when "dependencies" keyword is specified' do + let!(:pre_stage_job) do + create(:ci_build, :success, :artifacts, pipeline: pipeline, name: 'test', stage_idx: 0) + end + + let!(:pending_job) do + create(:ci_build, :pending, :queued, + pipeline: pipeline, stage_idx: 1, + options: { script: ["bash"], dependencies: dependencies }) + end + + let(:dependencies) { %w[test] } + + subject { execute(specific_runner) } + + it 'picks a build with a dependency' do + picked_build = execute(specific_runner) + + expect(picked_build).to be_present + end + + context 'when there are multiple dependencies with artifacts' do + let!(:pre_stage_job_second) do + create(:ci_build, :success, :artifacts, pipeline: pipeline, name: 'deploy', stage_idx: 0) + end + + let(:dependencies) { %w[test deploy] } + + it 'logs build artifacts size' do + execute(specific_runner) + + artifacts_size = [pre_stage_job, pre_stage_job_second].sum do |job| + job.job_artifacts_archive.size + end + + expect(artifacts_size).to eq 107464 * 2 + expect(Gitlab::ApplicationContext.current).to include({ + 'meta.artifacts_dependencies_size' => artifacts_size, + 'meta.artifacts_dependencies_count' => 2 + }) + end + end + shared_examples 'not pick' do it 'does not pick the build and drops the build' do expect(subject).to be_nil @@ -572,16 +620,6 @@ module Ci end end - let!(:pre_stage_job) { create(:ci_build, :success, pipeline: pipeline, name: 'test', stage_idx: 0) } - - let!(:pending_job) do - create(:ci_build, :pending, :queued, - pipeline: pipeline, stage_idx: 1, - options: { script: ["bash"], dependencies: ['test'] }) - end - - subject { execute(specific_runner) } - it_behaves_like 'validation is active' end @@ -739,16 +777,6 @@ module Ci end end - context 'when a long queue is created' do - it 'picks builds one-by-one' do - expect(Ci::Build).to receive(:find).with(pending_job.id).and_call_original - - expect(execute(specific_runner)).to eq(pending_job) - end - - include_examples 'handles runner assignment' - end - context 'when using pending builds table' do include_examples 'handles runner assignment' diff --git a/spec/services/ci/retry_job_service_spec.rb b/spec/services/ci/retry_job_service_spec.rb index f042471bd1f..b14e4187c7a 100644 --- a/spec/services/ci/retry_job_service_spec.rb +++ b/spec/services/ci/retry_job_service_spec.rb @@ -17,6 +17,7 @@ RSpec.describe Ci::RetryJobService do name: 'test') end + let(:job_variables_attributes) { [{ key: 'MANUAL_VAR', value: 'manual test var' }] } let(:user) { developer } let(:service) { described_class.new(project, user) } @@ -206,6 +207,14 @@ RSpec.describe Ci::RetryJobService do include_context 'retryable bridge' it_behaves_like 'clones the job' + + context 'when given variables' do + let(:new_job) { service.clone!(job, variables: job_variables_attributes) } + + it 'does not give variables to the new bridge' do + expect { new_job }.not_to raise_error + end + end end context 'when the job to be cloned is a build' do @@ -250,6 +259,28 @@ RSpec.describe Ci::RetryJobService do expect { new_job }.not_to change { Environment.count } end end + + context 'when given variables' do + let(:new_job) { service.clone!(job, variables: job_variables_attributes) } + + context 'when the build is actionable' do + let_it_be_with_refind(:job) { create(:ci_build, :actionable, pipeline: pipeline) } + + it 'gives variables to the new build' do + expect(new_job.job_variables.count).to be(1) + expect(new_job.job_variables.first.key).to eq('MANUAL_VAR') + expect(new_job.job_variables.first.value).to eq('manual test var') + end + end + + context 'when the build is not actionable' do + let_it_be_with_refind(:job) { create(:ci_build, pipeline: pipeline) } + + it 'does not give variables to the new build' do + expect(new_job.job_variables.count).to be_zero + end + end + end end end @@ -260,6 +291,14 @@ RSpec.describe Ci::RetryJobService do include_context 'retryable bridge' it_behaves_like 'retries the job' + + context 'when given variables' do + let(:new_job) { service.clone!(job, variables: job_variables_attributes) } + + it 'does not give variables to the new bridge' do + expect { new_job }.not_to raise_error + end + end end context 'when the job to be retried is a build' do @@ -288,6 +327,28 @@ RSpec.describe Ci::RetryJobService do expect { service.execute(job) }.not_to exceed_all_query_limit(control_count) end end + + context 'when given variables' do + let(:new_job) { service.clone!(job, variables: job_variables_attributes) } + + context 'when the build is actionable' do + let_it_be_with_refind(:job) { create(:ci_build, :actionable, pipeline: pipeline) } + + it 'gives variables to the new build' do + expect(new_job.job_variables.count).to be(1) + expect(new_job.job_variables.first.key).to eq('MANUAL_VAR') + expect(new_job.job_variables.first.value).to eq('manual test var') + end + end + + context 'when the build is not actionable' do + let_it_be_with_refind(:job) { create(:ci_build, pipeline: pipeline) } + + it 'does not give variables to the new build' do + expect(new_job.job_variables.count).to be_zero + end + end + end end end end diff --git a/spec/services/ci/runners/assign_runner_service_spec.rb b/spec/services/ci/runners/assign_runner_service_spec.rb index 00b176bb759..08bb99830fb 100644 --- a/spec/services/ci/runners/assign_runner_service_spec.rb +++ b/spec/services/ci/runners/assign_runner_service_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' RSpec.describe ::Ci::Runners::AssignRunnerService, '#execute' do - subject { described_class.new(runner, project, user).execute } + subject(:execute) { described_class.new(runner, project, user).execute } let_it_be(:runner) { create(:ci_runner, :project, projects: [project]) } let_it_be(:project) { create(:project) } @@ -11,30 +11,32 @@ RSpec.describe ::Ci::Runners::AssignRunnerService, '#execute' do context 'without user' do let(:user) { nil } - it 'does not call assign_to on runner and returns false' do + it 'does not call assign_to on runner and returns error response', :aggregate_failures do expect(runner).not_to receive(:assign_to) - is_expected.to eq(false) + is_expected.to be_error + expect(execute.message).to eq('user not allowed to assign runner') end end context 'with unauthorized user' do let(:user) { build(:user) } - it 'does not call assign_to on runner and returns false' do + it 'does not call assign_to on runner and returns error message' do expect(runner).not_to receive(:assign_to) - is_expected.to eq(false) + is_expected.to be_error + expect(execute.message).to eq('user not allowed to assign runner') end end context 'with admin user', :enable_admin_mode do let(:user) { create_default(:user, :admin) } - it 'calls assign_to on runner and returns value unchanged' do - expect(runner).to receive(:assign_to).with(project, user).once.and_return('assign_to return value') + it 'calls assign_to on runner and returns success response' do + expect(runner).to receive(:assign_to).with(project, user).once.and_call_original - is_expected.to eq('assign_to return value') + is_expected.to be_success end end end diff --git a/spec/services/ci/runners/bulk_delete_runners_service_spec.rb b/spec/services/ci/runners/bulk_delete_runners_service_spec.rb new file mode 100644 index 00000000000..8e9fc4e3012 --- /dev/null +++ b/spec/services/ci/runners/bulk_delete_runners_service_spec.rb @@ -0,0 +1,83 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ::Ci::Runners::BulkDeleteRunnersService, '#execute' do + subject(:execute) { described_class.new(**service_args).execute } + + let(:service_args) { { runners: runners_arg } } + let(:runners_arg) {} + + context 'with runners specified' do + let!(:instance_runner) { create(:ci_runner) } + let!(:group_runner) { create(:ci_runner, :group) } + let!(:project_runner) { create(:ci_runner, :project) } + + shared_examples 'a service deleting runners in bulk' do + it 'destroys runners', :aggregate_failures do + expect { subject }.to change { Ci::Runner.count }.by(-2) + + is_expected.to be_success + expect(execute.payload).to eq({ deleted_count: 2, deleted_ids: [instance_runner.id, project_runner.id] }) + expect(instance_runner[:errors]).to be_nil + expect(project_runner[:errors]).to be_nil + expect { project_runner.runner_projects.first.reload }.to raise_error(ActiveRecord::RecordNotFound) + expect { group_runner.reload }.not_to raise_error + expect { instance_runner.reload }.to raise_error(ActiveRecord::RecordNotFound) + expect { project_runner.reload }.to raise_error(ActiveRecord::RecordNotFound) + end + + context 'with some runners already deleted' do + before do + instance_runner.destroy! + end + + let(:runners_arg) { [instance_runner.id, project_runner.id] } + + it 'destroys runners and returns only deleted runners', :aggregate_failures do + expect { subject }.to change { Ci::Runner.count }.by(-1) + + is_expected.to be_success + expect(execute.payload).to eq({ deleted_count: 1, deleted_ids: [project_runner.id] }) + expect(instance_runner[:errors]).to be_nil + expect(project_runner[:errors]).to be_nil + expect { project_runner.reload }.to raise_error(ActiveRecord::RecordNotFound) + end + end + + context 'with too many runners specified' do + before do + stub_const("#{described_class}::RUNNER_LIMIT", 1) + end + + it 'deletes only first RUNNER_LIMIT runners' do + expect { subject }.to change { Ci::Runner.count }.by(-1) + + is_expected.to be_success + expect(execute.payload).to eq({ deleted_count: 1, deleted_ids: [instance_runner.id] }) + end + end + end + + context 'with runners specified as relation' do + let(:runners_arg) { Ci::Runner.not_group_type } + + include_examples 'a service deleting runners in bulk' + end + + context 'with runners specified as array of IDs' do + let(:runners_arg) { Ci::Runner.not_group_type.ids } + + include_examples 'a service deleting runners in bulk' + end + + context 'with no arguments specified' do + let(:runners_arg) { nil } + + it 'returns 0 deleted runners' do + is_expected.to be_success + expect(execute.payload).to eq({ deleted_count: 0, deleted_ids: [] }) + end + end + end +end diff --git a/spec/services/ci/runners/process_runner_version_update_service_spec.rb b/spec/services/ci/runners/process_runner_version_update_service_spec.rb new file mode 100644 index 00000000000..b885138fc7a --- /dev/null +++ b/spec/services/ci/runners/process_runner_version_update_service_spec.rb @@ -0,0 +1,80 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Ci::Runners::ProcessRunnerVersionUpdateService do + subject(:service) { described_class.new(version) } + + let(:version) { '1.0.0' } + let(:available_runner_releases) { %w[1.0.0 1.0.1] } + + describe '#execute' do + subject(:execute) { service.execute } + + context 'with upgrade check returning error' do + let(:service_double) { instance_double(Gitlab::Ci::RunnerUpgradeCheck) } + + before do + allow(service_double).to receive(:check_runner_upgrade_suggestion).with(version) + .and_return([version, :error]) + allow(service).to receive(:upgrade_check_service).and_return(service_double) + end + + it 'does not update ci_runner_versions records', :aggregate_failures do + expect do + expect(execute).to be_error + expect(execute.message).to eq 'upgrade version check failed' + end.not_to change(Ci::RunnerVersion, :count).from(0) + expect(service_double).to have_received(:check_runner_upgrade_suggestion).with(version).once + end + end + + context 'with successful result from upgrade check' do + before do + url = ::Gitlab::CurrentSettings.current_application_settings.public_runner_releases_url + + WebMock.stub_request(:get, url).to_return( + body: available_runner_releases.map { |v| { name: v } }.to_json, + status: 200, + headers: { 'Content-Type' => 'application/json' } + ) + end + + context 'with no existing ci_runner_version record' do + it 'creates ci_runner_versions record', :aggregate_failures do + expect do + expect(execute).to be_success + expect(execute.http_status).to eq :ok + expect(execute.payload).to eq({ upgrade_status: 'recommended' }) + end.to change(Ci::RunnerVersion, :all).to contain_exactly( + an_object_having_attributes(version: version, status: 'recommended') + ) + end + end + + context 'with existing ci_runner_version record' do + let!(:runner_version) { create(:ci_runner_version, version: '1.0.0', status: :not_available) } + + it 'updates ci_runner_versions record', :aggregate_failures do + expect do + expect(execute).to be_success + expect(execute.http_status).to eq :ok + expect(execute.payload).to eq({ upgrade_status: 'recommended' }) + end.to change { runner_version.reload.status }.from('not_available').to('recommended') + end + end + + context 'with up-to-date ci_runner_version record' do + let!(:runner_version) { create(:ci_runner_version, version: '1.0.0', status: :recommended) } + + it 'does not update ci_runner_versions record', :aggregate_failures do + expect do + expect(execute).to be_success + expect(execute.http_status).to eq :ok + expect(execute.payload).to eq({ upgrade_status: 'recommended' }) + end.not_to change { runner_version.reload.status } + end + end + end + end +end diff --git a/spec/services/ci/runners/reconcile_existing_runner_versions_service_spec.rb b/spec/services/ci/runners/reconcile_existing_runner_versions_service_spec.rb index f8313eaab90..1690190320a 100644 --- a/spec/services/ci/runners/reconcile_existing_runner_versions_service_spec.rb +++ b/spec/services/ci/runners/reconcile_existing_runner_versions_service_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' RSpec.describe ::Ci::Runners::ReconcileExistingRunnerVersionsService, '#execute' do + include RunnerReleasesHelper + subject(:execute) { described_class.new.execute } let_it_be(:runner_14_0_1) { create(:ci_runner, version: '14.0.1') } @@ -11,12 +13,12 @@ RSpec.describe ::Ci::Runners::ReconcileExistingRunnerVersionsService, '#execute' end context 'with RunnerUpgradeCheck recommending 14.0.2' do + let(:upgrade_check) { instance_double(::Gitlab::Ci::RunnerUpgradeCheck) } + before do stub_const('Ci::Runners::ReconcileExistingRunnerVersionsService::VERSION_BATCH_SIZE', 1) - allow(::Gitlab::Ci::RunnerUpgradeCheck.instance) - .to receive(:check_runner_upgrade_status) - .and_return({ recommended: ::Gitlab::VersionInfo.new(14, 0, 2) }) + allow(::Gitlab::Ci::RunnerUpgradeCheck).to receive(:new).and_return(upgrade_check).once end context 'with runner with new version' do @@ -25,10 +27,11 @@ RSpec.describe ::Ci::Runners::ReconcileExistingRunnerVersionsService, '#execute' let!(:runner_14_0_0) { create(:ci_runner, version: '14.0.0') } before do - allow(::Gitlab::Ci::RunnerUpgradeCheck.instance) - .to receive(:check_runner_upgrade_status) + allow(upgrade_check).to receive(:check_runner_upgrade_suggestion) + .and_return([::Gitlab::VersionInfo.new(14, 0, 2), :recommended]) + allow(upgrade_check).to receive(:check_runner_upgrade_suggestion) .with('14.0.2') - .and_return({ not_available: ::Gitlab::VersionInfo.new(14, 0, 2) }) + .and_return([::Gitlab::VersionInfo.new(14, 0, 2), :not_available]) .once end @@ -39,14 +42,13 @@ RSpec.describe ::Ci::Runners::ReconcileExistingRunnerVersionsService, '#execute' .once .and_call_original - result = nil - expect { result = execute } + expect { execute } .to change { runner_version_14_0_0.reload.status }.from('not_available').to('recommended') .and change { runner_version_14_0_1.reload.status }.from('not_available').to('recommended') .and change { ::Ci::RunnerVersion.find_by(version: '14.0.2')&.status }.from(nil).to('not_available') - expect(result).to eq({ - status: :success, + expect(execute).to be_success + expect(execute.payload).to eq({ total_inserted: 1, # 14.0.2 is inserted total_updated: 3, # 14.0.0, 14.0.1 are updated, and newly inserted 14.0.2's status is calculated total_deleted: 0 @@ -58,19 +60,17 @@ RSpec.describe ::Ci::Runners::ReconcileExistingRunnerVersionsService, '#execute' let!(:runner_version_14_0_2) { create(:ci_runner_version, version: '14.0.2', status: :not_available) } before do - allow(::Gitlab::Ci::RunnerUpgradeCheck.instance) - .to receive(:check_runner_upgrade_status) - .and_return({ not_available: ::Gitlab::VersionInfo.new(14, 0, 2) }) + allow(upgrade_check).to receive(:check_runner_upgrade_suggestion) + .and_return([::Gitlab::VersionInfo.new(14, 0, 2), :not_available]) end it 'deletes orphan ci_runner_versions entry', :aggregate_failures do - result = nil - expect { result = execute } + expect { execute } .to change { ::Ci::RunnerVersion.find_by_version('14.0.2')&.status }.from('not_available').to(nil) .and not_change { runner_version_14_0_1.reload.status }.from('not_available') - expect(result).to eq({ - status: :success, + expect(execute).to be_success + expect(execute.payload).to eq({ total_inserted: 0, total_updated: 0, total_deleted: 1 # 14.0.2 is deleted @@ -80,17 +80,15 @@ RSpec.describe ::Ci::Runners::ReconcileExistingRunnerVersionsService, '#execute' context 'with no runner version changes' do before do - allow(::Gitlab::Ci::RunnerUpgradeCheck.instance) - .to receive(:check_runner_upgrade_status) - .and_return({ not_available: ::Gitlab::VersionInfo.new(14, 0, 1) }) + allow(upgrade_check).to receive(:check_runner_upgrade_suggestion) + .and_return([::Gitlab::VersionInfo.new(14, 0, 1), :not_available]) end it 'does not modify ci_runner_versions entries', :aggregate_failures do - result = nil - expect { result = execute }.not_to change { runner_version_14_0_1.reload.status }.from('not_available') + expect { execute }.not_to change { runner_version_14_0_1.reload.status }.from('not_available') - expect(result).to eq({ - status: :success, + expect(execute).to be_success + expect(execute.payload).to eq({ total_inserted: 0, total_updated: 0, total_deleted: 0 @@ -100,17 +98,15 @@ RSpec.describe ::Ci::Runners::ReconcileExistingRunnerVersionsService, '#execute' context 'with failing version check' do before do - allow(::Gitlab::Ci::RunnerUpgradeCheck.instance) - .to receive(:check_runner_upgrade_status) - .and_return({ error: ::Gitlab::VersionInfo.new(14, 0, 1) }) + allow(upgrade_check).to receive(:check_runner_upgrade_suggestion) + .and_return([::Gitlab::VersionInfo.new(14, 0, 1), :error]) end it 'makes no changes to ci_runner_versions', :aggregate_failures do - result = nil - expect { result = execute }.not_to change { runner_version_14_0_1.reload.status }.from('not_available') + expect { execute }.not_to change { runner_version_14_0_1.reload.status }.from('not_available') - expect(result).to eq({ - status: :success, + expect(execute).to be_success + expect(execute.payload).to eq({ total_inserted: 0, total_updated: 0, total_deleted: 0 @@ -120,26 +116,15 @@ RSpec.describe ::Ci::Runners::ReconcileExistingRunnerVersionsService, '#execute' end context 'integration testing with Gitlab::Ci::RunnerUpgradeCheck' do - let(:available_runner_releases) do - %w[14.0.0 14.0.1] - end - before do - url = ::Gitlab::CurrentSettings.current_application_settings.public_runner_releases_url - - WebMock.stub_request(:get, url).to_return( - body: available_runner_releases.map { |v| { name: v } }.to_json, - status: 200, - headers: { 'Content-Type' => 'application/json' } - ) + stub_runner_releases(%w[14.0.0 14.0.1]) end it 'does not modify ci_runner_versions entries', :aggregate_failures do - result = nil - expect { result = execute }.not_to change { runner_version_14_0_1.reload.status }.from('not_available') + expect { execute }.not_to change { runner_version_14_0_1.reload.status }.from('not_available') - expect(result).to eq({ - status: :success, + expect(execute).to be_success + expect(execute.payload).to eq({ total_inserted: 0, total_updated: 0, total_deleted: 0 diff --git a/spec/services/ci/runners/register_runner_service_spec.rb b/spec/services/ci/runners/register_runner_service_spec.rb index 03dcf851e53..6d7b39de21e 100644 --- a/spec/services/ci/runners/register_runner_service_spec.rb +++ b/spec/services/ci/runners/register_runner_service_spec.rb @@ -4,8 +4,9 @@ require 'spec_helper' RSpec.describe ::Ci::Runners::RegisterRunnerService, '#execute' do let(:registration_token) { 'abcdefg123456' } - let(:token) { } + let(:token) {} let(:args) { {} } + let(:runner) { execute.payload[:runner] } before do stub_feature_flags(runner_registration_control: false) @@ -13,21 +14,25 @@ RSpec.describe ::Ci::Runners::RegisterRunnerService, '#execute' do stub_application_setting(valid_runner_registrars: ApplicationSetting::VALID_RUNNER_REGISTRAR_TYPES) end - subject(:runner) { described_class.new.execute(token, args) } + subject(:execute) { described_class.new.execute(token, args) } context 'when no token is provided' do let(:token) { '' } - it 'returns nil' do - is_expected.to be_nil + it 'returns error response' do + expect(execute).to be_error + expect(execute.message).to eq 'invalid token supplied' + expect(execute.http_status).to eq :forbidden end end context 'when invalid token is provided' do let(:token) { 'invalid' } - it 'returns nil' do - is_expected.to be_nil + it 'returns error response' do + expect(execute).to be_error + expect(execute.message).to eq 'invalid token supplied' + expect(execute.http_status).to eq :forbidden end end @@ -36,12 +41,14 @@ RSpec.describe ::Ci::Runners::RegisterRunnerService, '#execute' do let(:token) { registration_token } it 'creates runner with default values' do - is_expected.to be_an_instance_of(::Ci::Runner) - expect(subject.persisted?).to be_truthy - expect(subject.run_untagged).to be true - expect(subject.active).to be true - expect(subject.token).not_to eq(registration_token) - expect(subject).to be_instance_type + expect(execute).to be_success + + expect(runner).to be_an_instance_of(::Ci::Runner) + expect(runner.persisted?).to be_truthy + expect(runner.run_untagged).to be true + expect(runner.active).to be true + expect(runner.token).not_to eq(registration_token) + expect(runner).to be_instance_type end context 'with non-default arguments' do @@ -67,25 +74,27 @@ RSpec.describe ::Ci::Runners::RegisterRunnerService, '#execute' do end it 'creates runner with specified values', :aggregate_failures do - is_expected.to be_an_instance_of(::Ci::Runner) - expect(subject.active).to eq args[:active] - expect(subject.locked).to eq args[:locked] - expect(subject.run_untagged).to eq args[:run_untagged] - expect(subject.tags).to contain_exactly( + expect(execute).to be_success + + expect(runner).to be_an_instance_of(::Ci::Runner) + expect(runner.active).to eq args[:active] + expect(runner.locked).to eq args[:locked] + expect(runner.run_untagged).to eq args[:run_untagged] + expect(runner.tags).to contain_exactly( an_object_having_attributes(name: 'tag1'), an_object_having_attributes(name: 'tag2') ) - expect(subject.access_level).to eq args[:access_level] - expect(subject.maximum_timeout).to eq args[:maximum_timeout] - expect(subject.name).to eq args[:name] - expect(subject.version).to eq args[:version] - expect(subject.revision).to eq args[:revision] - expect(subject.platform).to eq args[:platform] - expect(subject.architecture).to eq args[:architecture] - expect(subject.ip_address).to eq args[:ip_address] - - expect(Ci::Runner.tagged_with('tag1')).to include(subject) - expect(Ci::Runner.tagged_with('tag2')).to include(subject) + expect(runner.access_level).to eq args[:access_level] + expect(runner.maximum_timeout).to eq args[:maximum_timeout] + expect(runner.name).to eq args[:name] + expect(runner.version).to eq args[:version] + expect(runner.revision).to eq args[:revision] + expect(runner.platform).to eq args[:platform] + expect(runner.architecture).to eq args[:architecture] + expect(runner.ip_address).to eq args[:ip_address] + + expect(Ci::Runner.tagged_with('tag1')).to include(runner) + expect(Ci::Runner.tagged_with('tag2')).to include(runner) end end @@ -95,8 +104,10 @@ RSpec.describe ::Ci::Runners::RegisterRunnerService, '#execute' do end it 'creates runner with token expiration' do - is_expected.to be_an_instance_of(::Ci::Runner) - expect(subject.token_expires_at).to eq(5.days.from_now) + expect(execute).to be_success + + expect(runner).to be_an_instance_of(::Ci::Runner) + expect(runner.token_expires_at).to eq(5.days.from_now) end end end @@ -106,12 +117,14 @@ RSpec.describe ::Ci::Runners::RegisterRunnerService, '#execute' do let(:token) { project.runners_token } it 'creates project runner' do - is_expected.to be_an_instance_of(::Ci::Runner) + expect(execute).to be_success + + expect(runner).to be_an_instance_of(::Ci::Runner) expect(project.runners.size).to eq(1) - is_expected.to eq(project.runners.first) - expect(subject.token).not_to eq(registration_token) - expect(subject.token).not_to eq(project.runners_token) - expect(subject).to be_project_type + expect(runner).to eq(project.runners.first) + expect(runner.token).not_to eq(registration_token) + expect(runner.token).not_to eq(project.runners_token) + expect(runner).to be_project_type end context 'when it exceeds the application limits' do @@ -121,9 +134,13 @@ RSpec.describe ::Ci::Runners::RegisterRunnerService, '#execute' do end it 'does not create runner' do - is_expected.to be_an_instance_of(::Ci::Runner) - expect(subject.persisted?).to be_falsey - expect(subject.errors.messages).to eq('runner_projects.base': ['Maximum number of ci registered project runners (1) exceeded']) + expect(execute).to be_success + + expect(runner).to be_an_instance_of(::Ci::Runner) + expect(runner.persisted?).to be_falsey + expect(runner.errors.messages).to eq( + 'runner_projects.base': ['Maximum number of ci registered project runners (1) exceeded'] + ) expect(project.runners.reload.size).to eq(1) end end @@ -135,8 +152,10 @@ RSpec.describe ::Ci::Runners::RegisterRunnerService, '#execute' do end it 'creates runner' do - is_expected.to be_an_instance_of(::Ci::Runner) - expect(subject.errors).to be_empty + expect(execute).to be_success + + expect(runner).to be_an_instance_of(::Ci::Runner) + expect(runner.errors).to be_empty expect(project.runners.reload.size).to eq(2) expect(project.runners.recent.size).to eq(1) end @@ -153,15 +172,18 @@ RSpec.describe ::Ci::Runners::RegisterRunnerService, '#execute' do end it 'returns 403 error' do - is_expected.to be_nil + expect(execute).to be_error + expect(execute.http_status).to eq :forbidden end end context 'when feature flag is disabled' do it 'registers the runner' do - is_expected.to be_an_instance_of(::Ci::Runner) - expect(subject.errors).to be_empty - expect(subject.active).to be true + expect(execute).to be_success + + expect(runner).to be_an_instance_of(::Ci::Runner) + expect(runner.errors).to be_empty + expect(runner.active).to be true end end end @@ -172,12 +194,14 @@ RSpec.describe ::Ci::Runners::RegisterRunnerService, '#execute' do let(:token) { group.runners_token } it 'creates a group runner' do - is_expected.to be_an_instance_of(::Ci::Runner) - expect(subject.errors).to be_empty + expect(execute).to be_success + + expect(runner).to be_an_instance_of(::Ci::Runner) + expect(runner.errors).to be_empty expect(group.runners.reload.size).to eq(1) - expect(subject.token).not_to eq(registration_token) - expect(subject.token).not_to eq(group.runners_token) - expect(subject).to be_group_type + expect(runner.token).not_to eq(registration_token) + expect(runner.token).not_to eq(group.runners_token) + expect(runner).to be_group_type end context 'when it exceeds the application limits' do @@ -187,9 +211,13 @@ RSpec.describe ::Ci::Runners::RegisterRunnerService, '#execute' do end it 'does not create runner' do - is_expected.to be_an_instance_of(::Ci::Runner) - expect(subject.persisted?).to be_falsey - expect(subject.errors.messages).to eq('runner_namespaces.base': ['Maximum number of ci registered group runners (1) exceeded']) + expect(execute).to be_success + + expect(runner).to be_an_instance_of(::Ci::Runner) + expect(runner.persisted?).to be_falsey + expect(runner.errors.messages).to eq( + 'runner_namespaces.base': ['Maximum number of ci registered group runners (1) exceeded'] + ) expect(group.runners.reload.size).to eq(1) end end @@ -202,8 +230,10 @@ RSpec.describe ::Ci::Runners::RegisterRunnerService, '#execute' do end it 'creates runner' do - is_expected.to be_an_instance_of(::Ci::Runner) - expect(subject.errors).to be_empty + expect(execute).to be_success + + expect(runner).to be_an_instance_of(::Ci::Runner) + expect(runner.errors).to be_empty expect(group.runners.reload.size).to eq(3) expect(group.runners.recent.size).to eq(1) end @@ -219,16 +249,18 @@ RSpec.describe ::Ci::Runners::RegisterRunnerService, '#execute' do stub_feature_flags(runner_registration_control: true) end - it 'returns nil' do - is_expected.to be_nil + it 'returns error response' do + is_expected.to be_error end end context 'when feature flag is disabled' do it 'registers the runner' do - is_expected.to be_an_instance_of(::Ci::Runner) - expect(subject.errors).to be_empty - expect(subject.active).to be true + expect(execute).to be_success + + expect(runner).to be_an_instance_of(::Ci::Runner) + expect(runner.errors).to be_empty + expect(runner.active).to be true end end end diff --git a/spec/services/ci/runners/reset_registration_token_service_spec.rb b/spec/services/ci/runners/reset_registration_token_service_spec.rb index c4bfff51cc8..79059712032 100644 --- a/spec/services/ci/runners/reset_registration_token_service_spec.rb +++ b/spec/services/ci/runners/reset_registration_token_service_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' RSpec.describe ::Ci::Runners::ResetRegistrationTokenService, '#execute' do - subject { described_class.new(scope, current_user).execute } + subject(:execute) { described_class.new(scope, current_user).execute } let_it_be(:user) { build(:user) } let_it_be(:admin_user) { create(:user, :admin) } @@ -12,20 +12,20 @@ RSpec.describe ::Ci::Runners::ResetRegistrationTokenService, '#execute' do context 'without user' do let(:current_user) { nil } - it 'does not reset registration token and returns nil' do + it 'does not reset registration token and returns error response' do expect(scope).not_to receive(token_reset_method_name) - is_expected.to be_nil + expect(execute).to be_error end end context 'with unauthorized user' do let(:current_user) { user } - it 'does not reset registration token and returns nil' do + it 'does not reset registration token and returns error response' do expect(scope).not_to receive(token_reset_method_name) - is_expected.to be_nil + expect(execute).to be_error end end @@ -37,7 +37,8 @@ RSpec.describe ::Ci::Runners::ResetRegistrationTokenService, '#execute' do expect(scope).to receive(token_method_name).once.and_return("#{token_method_name} return value") end - is_expected.to eq("#{token_method_name} return value") + expect(execute).to be_success + expect(execute.payload[:new_registration_token]).to eq("#{token_method_name} return value") end end end diff --git a/spec/services/ci/runners/unassign_runner_service_spec.rb b/spec/services/ci/runners/unassign_runner_service_spec.rb index 3fb6925f4bd..cf710cf6893 100644 --- a/spec/services/ci/runners/unassign_runner_service_spec.rb +++ b/spec/services/ci/runners/unassign_runner_service_spec.rb @@ -3,21 +3,21 @@ require 'spec_helper' RSpec.describe ::Ci::Runners::UnassignRunnerService, '#execute' do - subject(:service) { described_class.new(runner_project, user).execute } - - let_it_be(:runner) { create(:ci_runner, :project, projects: [project]) } let_it_be(:project) { create(:project) } + let_it_be(:runner) { create(:ci_runner, :project, projects: [project]) } let(:runner_project) { runner.runner_projects.last } + subject(:execute) { described_class.new(runner_project, user).execute } + context 'without user' do let(:user) { nil } it 'does not destroy runner_project', :aggregate_failures do expect(runner_project).not_to receive(:destroy) - expect { service }.not_to change { runner.runner_projects.count }.from(1) + expect { execute }.not_to change { runner.runner_projects.count }.from(1) - is_expected.to eq(false) + is_expected.to be_error end end @@ -27,17 +27,27 @@ RSpec.describe ::Ci::Runners::UnassignRunnerService, '#execute' do it 'does not call destroy on runner_project' do expect(runner).not_to receive(:destroy) - service + is_expected.to be_error end end context 'with admin user', :enable_admin_mode do let(:user) { create_default(:user, :admin) } - it 'destroys runner_project' do - expect(runner_project).to receive(:destroy).once + context 'with destroy returning false' do + it 'returns error response' do + expect(runner_project).to receive(:destroy).once.and_return(false) + + is_expected.to be_error + end + end + + context 'with destroy returning true' do + it 'returns success response' do + expect(runner_project).to receive(:destroy).once.and_return(true) - service + is_expected.to be_success + end end end end diff --git a/spec/services/ci/runners/unregister_runner_service_spec.rb b/spec/services/ci/runners/unregister_runner_service_spec.rb index df1a0a90067..77fc299e4e1 100644 --- a/spec/services/ci/runners/unregister_runner_service_spec.rb +++ b/spec/services/ci/runners/unregister_runner_service_spec.rb @@ -3,13 +3,16 @@ require 'spec_helper' RSpec.describe ::Ci::Runners::UnregisterRunnerService, '#execute' do - subject { described_class.new(runner, 'some_token').execute } + subject(:execute) { described_class.new(runner, 'some_token').execute } let(:runner) { create(:ci_runner) } it 'destroys runner' do expect(runner).to receive(:destroy).once.and_call_original - expect { subject }.to change { Ci::Runner.count }.by(-1) + + expect do + expect(execute).to be_success + end.to change { Ci::Runner.count }.by(-1) expect(runner[:errors]).to be_nil end end diff --git a/spec/services/ci/runners/update_runner_service_spec.rb b/spec/services/ci/runners/update_runner_service_spec.rb index b02ea8f58b0..e008fde9982 100644 --- a/spec/services/ci/runners/update_runner_service_spec.rb +++ b/spec/services/ci/runners/update_runner_service_spec.rb @@ -38,7 +38,7 @@ RSpec.describe Ci::Runners::UpdateRunnerService do end context 'with cost factor params' do - let(:params) { { public_projects_minutes_cost_factor: 1.1, private_projects_minutes_cost_factor: 2.2 }} + let(:params) { { public_projects_minutes_cost_factor: 1.1, private_projects_minutes_cost_factor: 2.2 } } it 'updates the runner cost factors' do expect(update).to be_truthy diff --git a/spec/services/ci/stuck_builds/drop_pending_service_spec.rb b/spec/services/ci/stuck_builds/drop_pending_service_spec.rb index ebc57af77a0..a452a65829a 100644 --- a/spec/services/ci/stuck_builds/drop_pending_service_spec.rb +++ b/spec/services/ci/stuck_builds/drop_pending_service_spec.rb @@ -9,8 +9,8 @@ RSpec.describe Ci::StuckBuilds::DropPendingService do create(:ci_build, pipeline: pipeline, runner: runner) end - let(:created_at) { } - let(:updated_at) { } + let(:created_at) {} + let(:updated_at) {} subject(:service) { described_class.new } diff --git a/spec/services/ci/stuck_builds/drop_scheduled_service_spec.rb b/spec/services/ci/stuck_builds/drop_scheduled_service_spec.rb index 1416fab3d25..a4f9f97fffc 100644 --- a/spec/services/ci/stuck_builds/drop_scheduled_service_spec.rb +++ b/spec/services/ci/stuck_builds/drop_scheduled_service_spec.rb @@ -44,7 +44,7 @@ RSpec.describe Ci::StuckBuilds::DropScheduledService do end context 'when there are no stale scheduled builds' do - let(:job) { } + let(:job) {} it 'does not drop the stale scheduled build yet' do expect { service.execute }.not_to raise_error diff --git a/spec/services/ci/track_failed_build_service_spec.rb b/spec/services/ci/track_failed_build_service_spec.rb new file mode 100644 index 00000000000..d83e56f0669 --- /dev/null +++ b/spec/services/ci/track_failed_build_service_spec.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Ci::TrackFailedBuildService do + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project, :public) } + let_it_be(:pipeline) { create(:ci_pipeline, project: project, user: user) } + + let_it_be(:exit_code) { 42 } + let_it_be(:failure_reason) { "script_failure" } + + describe '#execute' do + context 'when a build has failed' do + let_it_be(:build) { create(:ci_build, :failed, :sast_report, pipeline: pipeline, user: user) } + + subject { described_class.new(build: build, exit_code: exit_code, failure_reason: failure_reason) } + + it 'tracks the build failed event', :snowplow do + response = subject.execute + + expect(response.success?).to be true + + expect_snowplow_event( + category: 'ci::build', + action: 'failed', + context: [{ + schema: described_class::SCHEMA_URL, + data: { + build_id: build.id, + build_name: build.name, + build_artifact_types: ["sast"], + exit_code: exit_code, + failure_reason: failure_reason + } + }], + user: user, + project: project.id) + end + end + + context 'when a build has not failed' do + let_it_be(:build) { create(:ci_build, :success, :sast_report, pipeline: pipeline, user: user) } + + subject { described_class.new(build: build, exit_code: nil, failure_reason: nil) } + + it 'does not track the build failed event', :snowplow do + response = subject.execute + + expect(response.error?).to be true + + expect_no_snowplow_event + end + end + end +end diff --git a/spec/services/ci/update_build_state_service_spec.rb b/spec/services/ci/update_build_state_service_spec.rb index 937b19beff5..90a86e7ae59 100644 --- a/spec/services/ci/update_build_state_service_spec.rb +++ b/spec/services/ci/update_build_state_service_spec.rb @@ -33,6 +33,24 @@ RSpec.describe Ci::UpdateBuildStateService do end end + context 'when build has failed' do + let(:params) do + { + output: { checksum: 'crc32:12345678', bytesize: 123 }, + state: 'failed', + failure_reason: 'script_failure', + exit_code: 7 + } + end + + it 'sends a build failed event to Snowplow' do + expect(::Ci::TrackFailedBuildWorker) + .to receive(:perform_async).with(build.id, params[:exit_code], params[:failure_reason]) + + subject.execute + end + end + context 'when build does not have checksum' do context 'when state has changed' do let(:params) { { state: 'success' } } diff --git a/spec/services/clusters/integrations/create_service_spec.rb b/spec/services/clusters/integrations/create_service_spec.rb index 016511a3c01..9104e07504d 100644 --- a/spec/services/clusters/integrations/create_service_spec.rb +++ b/spec/services/clusters/integrations/create_service_spec.rb @@ -68,7 +68,7 @@ RSpec.describe Clusters::Integrations::CreateService, '#execute' do end it 'errors' do - expect { service.execute}.to raise_error(ArgumentError) + expect { service.execute }.to raise_error(ArgumentError) end end diff --git a/spec/services/clusters/integrations/prometheus_health_check_service_spec.rb b/spec/services/clusters/integrations/prometheus_health_check_service_spec.rb index 7147f1b9b28..526462931a6 100644 --- a/spec/services/clusters/integrations/prometheus_health_check_service_spec.rb +++ b/spec/services/clusters/integrations/prometheus_health_check_service_spec.rb @@ -51,6 +51,7 @@ RSpec.describe Clusters::Integrations::PrometheusHealthCheckService, '#execute' let(:prometheus_enabled) { false } it { expect(subject).to eq(nil) } + include_examples 'no alert' end diff --git a/spec/services/clusters/kubernetes/create_or_update_service_account_service_spec.rb b/spec/services/clusters/kubernetes/create_or_update_service_account_service_spec.rb index a4f018aec0c..064f9e42e96 100644 --- a/spec/services/clusters/kubernetes/create_or_update_service_account_service_spec.rb +++ b/spec/services/clusters/kubernetes/create_or_update_service_account_service_spec.rb @@ -136,7 +136,7 @@ RSpec.describe Clusters::Kubernetes::CreateOrUpdateServiceAccountService do context 'With RBAC enabled cluster' do let(:rbac) { true } - let(:role_binding_name) { "gitlab-#{namespace}"} + let(:role_binding_name) { "gitlab-#{namespace}" } before do cluster.platform_kubernetes.rbac! diff --git a/spec/services/database/consistency_check_service_spec.rb b/spec/services/database/consistency_check_service_spec.rb index 2e642451432..6695e4b5e9f 100644 --- a/spec/services/database/consistency_check_service_spec.rb +++ b/spec/services/database/consistency_check_service_spec.rb @@ -24,9 +24,27 @@ RSpec.describe Database::ConsistencyCheckService do ) end - describe '#random_start_id' do - let(:batch_size) { 5 } + describe '#min_id' do + before do + create_list(:namespace, 3) + end + it 'returns the id of the first record in the database' do + expect(subject.send(:min_id)).to eq(Namespace.first.id) + end + end + + describe '#max_id' do + before do + create_list(:namespace, 3) + end + + it 'returns the id of the first record in the database' do + expect(subject.send(:max_id)).to eq(Namespace.last.id) + end + end + + describe '#random_start_id' do before do create_list(:namespace, 50) # This will also create Ci::NameSpaceMirror objects end @@ -58,12 +76,11 @@ RSpec.describe Database::ConsistencyCheckService do end context 'no cursor has been saved before' do - let(:selected_start_id) { Namespace.order(:id).limit(5).pluck(:id).last } - let(:expected_next_start_id) { selected_start_id + batch_size * max_batches } + let(:min_id) { Namespace.first.id } + let(:max_id) { Namespace.last.id } before do create_list(:namespace, 50) # This will also create Ci::NameSpaceMirror objects - expect(consistency_check_service).to receive(:random_start_id).and_return(selected_start_id) end it 'picks a random start_id' do @@ -72,17 +89,21 @@ RSpec.describe Database::ConsistencyCheckService do matches: 10, mismatches: 0, mismatches_details: [], - start_id: selected_start_id, - next_start_id: expected_next_start_id + start_id: be_between(min_id, max_id), + next_start_id: be_between(min_id, max_id) } - expect(consistency_check_service.execute).to eq(expected_result) + expect(consistency_check_service).to receive(:rand).with(min_id..max_id).and_call_original + result = consistency_check_service.execute + expect(result).to match(expected_result) end it 'calls the ConsistencyCheckService with the expected parameters' do + expect(consistency_check_service).to receive(:random_start_id).and_return(min_id) + allow_next_instance_of(Gitlab::Database::ConsistencyChecker) do |instance| - expect(instance).to receive(:execute).with(start_id: selected_start_id).and_return({ + expect(instance).to receive(:execute).with(start_id: min_id).and_return({ batches: 2, - next_start_id: expected_next_start_id, + next_start_id: min_id + batch_size, matches: 10, mismatches: 0, mismatches_details: [] @@ -98,17 +119,19 @@ RSpec.describe Database::ConsistencyCheckService do expected_result = { batches: 2, - start_id: selected_start_id, - next_start_id: expected_next_start_id, matches: 10, mismatches: 0, - mismatches_details: [] + mismatches_details: [], + start_id: be_between(min_id, max_id), + next_start_id: be_between(min_id, max_id) } - expect(consistency_check_service.execute).to eq(expected_result) + result = consistency_check_service.execute + expect(result).to match(expected_result) end it 'saves the next_start_id in Redis for he next iteration' do - expect(consistency_check_service).to receive(:save_next_start_id).with(expected_next_start_id).and_call_original + expect(consistency_check_service).to receive(:save_next_start_id) + .with(be_between(min_id, max_id)).and_call_original consistency_check_service.execute end end diff --git a/spec/services/deployments/create_for_build_service_spec.rb b/spec/services/deployments/create_for_build_service_spec.rb index 38d94580512..a2e1acadcc1 100644 --- a/spec/services/deployments/create_for_build_service_spec.rb +++ b/spec/services/deployments/create_for_build_service_spec.rb @@ -41,7 +41,7 @@ RSpec.describe Deployments::CreateForBuildService do end context 'when the corresponding environment does not exist' do - let!(:environment) { } + let!(:environment) {} it 'does not create a deployment record' do expect { subject }.not_to change { Deployment.count } diff --git a/spec/services/deployments/update_environment_service_spec.rb b/spec/services/deployments/update_environment_service_spec.rb index 8ab53a37a33..4485ce585bb 100644 --- a/spec/services/deployments/update_environment_service_spec.rb +++ b/spec/services/deployments/update_environment_service_spec.rb @@ -112,7 +112,7 @@ RSpec.describe Deployments::UpdateEnvironmentService do end context 'when external URL is invalid' do - let(:external_url) { 'google.com' } + let(:external_url) { 'javascript:alert("hello")' } it 'fails to update the tier due to validation error' do expect { subject.execute }.not_to change { environment.tier } @@ -123,7 +123,7 @@ RSpec.describe Deployments::UpdateEnvironmentService do .with(an_instance_of(described_class::EnvironmentUpdateFailure), project_id: project.id, environment_id: environment.id, - reason: %q{External url is blocked: Only allowed schemes are http, https}) + reason: %q{External url javascript scheme is not allowed}) .once subject.execute @@ -307,14 +307,6 @@ RSpec.describe Deployments::UpdateEnvironmentService do end it { is_expected.to eq('http://appname-master.example.com') } - - context 'when the FF ci_expand_environment_name_and_url is disabled' do - before do - stub_feature_flags(ci_expand_environment_name_and_url: false) - end - - it { is_expected.to eq('http://${STACK_NAME}.example.com') } - end end context 'when yaml environment does not have url' do diff --git a/spec/services/design_management/delete_designs_service_spec.rb b/spec/services/design_management/delete_designs_service_spec.rb index bc7625d7c28..a0e049ea42a 100644 --- a/spec/services/design_management/delete_designs_service_spec.rb +++ b/spec/services/design_management/delete_designs_service_spec.rb @@ -59,7 +59,11 @@ RSpec.describe DesignManagement::DeleteDesignsService do it_behaves_like "a service error" it 'does not create any events in the activity stream' do - expect { run_service rescue nil }.not_to change { Event.count } + expect do + run_service + rescue StandardError + nil + end.not_to change { Event.count } end end @@ -78,7 +82,11 @@ RSpec.describe DesignManagement::DeleteDesignsService do it 'does not log any events' do counter = ::Gitlab::UsageDataCounters::DesignsCounter - expect { run_service rescue nil } + expect do + run_service + rescue StandardError + nil + end .not_to change { [counter.totals, Event.count] } end @@ -86,10 +94,18 @@ RSpec.describe DesignManagement::DeleteDesignsService do redis_hll = ::Gitlab::UsageDataCounters::HLLRedisCounter event = Gitlab::UsageDataCounters::IssueActivityUniqueCounter::ISSUE_DESIGNS_REMOVED - expect { run_service rescue nil } + expect do + run_service + rescue StandardError + nil + end .not_to change { redis_hll.unique_events(event_names: event, start_date: 1.day.ago, end_date: 1.day.from_now) } - run_service rescue nil + begin + run_service + rescue StandardError + nil + end end end diff --git a/spec/services/design_management/generate_image_versions_service_spec.rb b/spec/services/design_management/generate_image_versions_service_spec.rb index e06b6fbf116..5409ec12016 100644 --- a/spec/services/design_management/generate_image_versions_service_spec.rb +++ b/spec/services/design_management/generate_image_versions_service_spec.rb @@ -16,7 +16,7 @@ RSpec.describe DesignManagement::GenerateImageVersionsService do end it 'skips generating image versions if the mime type is not whitelisted' do - stub_const('DesignManagement::DesignV432x230Uploader::MIME_TYPE_WHITELIST', []) + stub_const('DesignManagement::DesignV432x230Uploader::MIME_TYPE_ALLOWLIST', []) described_class.new(version).execute diff --git a/spec/services/git/branch_push_service_spec.rb b/spec/services/git/branch_push_service_spec.rb index 8d41b20c8a9..6280f1263c3 100644 --- a/spec/services/git/branch_push_service_spec.rb +++ b/spec/services/git/branch_push_service_spec.rb @@ -6,7 +6,7 @@ RSpec.describe Git::BranchPushService, services: true do include RepoHelpers let_it_be(:user) { create(:user) } - let_it_be(:project, reload: true) { create(:project, :repository) } + let_it_be_with_refind(:project) { create(:project, :repository) } let(:blankrev) { Gitlab::Git::BLANK_SHA } let(:oldrev) { sample_commit.parent_id } @@ -573,7 +573,7 @@ RSpec.describe Git::BranchPushService, services: true do before do allow(project).to receive(:default_branch).and_return('feature') - expect(project).to receive(:change_head) { 'feature'} + expect(project).to receive(:change_head) { 'feature' } end it 'push to first branch updates HEAD' do diff --git a/spec/services/google_cloud/create_cloudsql_instance_service_spec.rb b/spec/services/google_cloud/create_cloudsql_instance_service_spec.rb new file mode 100644 index 00000000000..cd0dd75e576 --- /dev/null +++ b/spec/services/google_cloud/create_cloudsql_instance_service_spec.rb @@ -0,0 +1,90 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe GoogleCloud::CreateCloudsqlInstanceService do + let(:project) { create(:project) } + let(:user) { create(:user) } + let(:gcp_project_id) { 'gcp_project_120' } + let(:environment_name) { 'test_env_42' } + let(:database_version) { 'POSTGRES_8000' } + let(:tier) { 'REIT_TIER' } + let(:service) do + described_class.new(project, user, { + gcp_project_id: gcp_project_id, + environment_name: environment_name, + database_version: database_version, + tier: tier + }) + end + + describe '#execute' do + before do + allow_next_instance_of(::Ci::VariablesFinder) do |variable_finder| + allow(variable_finder).to receive(:execute).and_return([]) + end + end + + it 'triggers creation of a cloudsql instance' do + expect_next_instance_of(GoogleApi::CloudPlatform::Client) do |client| + expected_instance_name = "gitlab-#{project.id}-postgres-8000-test-env-42" + expect(client).to receive(:create_cloudsql_instance) + .with(gcp_project_id, + expected_instance_name, + String, + database_version, + 'us-east1', + tier) + end + + result = service.execute + expect(result[:status]).to be(:success) + end + + it 'triggers worker to manage cloudsql instance creation operation results' do + expect_next_instance_of(GoogleApi::CloudPlatform::Client) do |client| + expect(client).to receive(:create_cloudsql_instance) + end + + expect(GoogleCloud::CreateCloudsqlInstanceWorker).to receive(:perform_in) + + result = service.execute + expect(result[:status]).to be(:success) + end + + context 'when google APIs fail' do + it 'returns error' do + expect_next_instance_of(GoogleApi::CloudPlatform::Client) do |client| + expect(client).to receive(:create_cloudsql_instance).and_raise(Google::Apis::Error.new('mock-error')) + end + + result = service.execute + expect(result[:status]).to eq(:error) + end + end + + context 'when project has GCP_REGION defined' do + let(:gcp_region) { instance_double(::Ci::Variable, key: 'GCP_REGION', value: 'user-defined-region') } + + before do + allow_next_instance_of(::Ci::VariablesFinder) do |variable_finder| + allow(variable_finder).to receive(:execute).and_return([gcp_region]) + end + end + + it 'uses defined region' do + expect_next_instance_of(GoogleApi::CloudPlatform::Client) do |client| + expect(client).to receive(:create_cloudsql_instance) + .with(gcp_project_id, + String, + String, + database_version, + 'user-defined-region', + tier) + end + + service.execute + end + end + end +end diff --git a/spec/services/google_cloud/enable_cloudsql_service_spec.rb b/spec/services/google_cloud/enable_cloudsql_service_spec.rb new file mode 100644 index 00000000000..e54e5a8d446 --- /dev/null +++ b/spec/services/google_cloud/enable_cloudsql_service_spec.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe GoogleCloud::EnableCloudsqlService do + let_it_be(:project) { create(:project) } + + subject(:result) { described_class.new(project).execute } + + context 'when a project does not have any GCP_PROJECT_IDs configured' do + it 'returns error' do + message = 'No GCP projects found. Configure a service account or GCP_PROJECT_ID CI variable.' + + expect(result[:status]).to eq(:error) + expect(result[:message]).to eq(message) + end + end + + context 'when a project has GCP_PROJECT_IDs configured' do + before do + project.variables.build(environment_scope: 'production', key: 'GCP_PROJECT_ID', value: 'prj-prod') + project.variables.build(environment_scope: 'staging', key: 'GCP_PROJECT_ID', value: 'prj-staging') + project.save! + end + + it 'enables cloudsql, compute and service networking Google APIs', :aggregate_failures do + expect_next_instance_of(GoogleApi::CloudPlatform::Client) do |instance| + expect(instance).to receive(:enable_cloud_sql_admin).with('prj-prod') + expect(instance).to receive(:enable_compute).with('prj-prod') + expect(instance).to receive(:enable_service_networking).with('prj-prod') + expect(instance).to receive(:enable_cloud_sql_admin).with('prj-staging') + expect(instance).to receive(:enable_compute).with('prj-staging') + expect(instance).to receive(:enable_service_networking).with('prj-staging') + end + + expect(result[:status]).to eq(:success) + end + end +end diff --git a/spec/services/google_cloud/get_cloudsql_instances_service_spec.rb b/spec/services/google_cloud/get_cloudsql_instances_service_spec.rb new file mode 100644 index 00000000000..4587a5077c0 --- /dev/null +++ b/spec/services/google_cloud/get_cloudsql_instances_service_spec.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe GoogleCloud::GetCloudsqlInstancesService do + let(:service) { described_class.new(project) } + let(:project) { create(:project) } + + context 'when project has no registered cloud sql instances' do + it 'result is empty' do + expect(service.execute.length).to eq(0) + end + end + + context 'when project has registered cloud sql instance' do + before do + keys = %w[ + GCP_PROJECT_ID + GCP_CLOUDSQL_INSTANCE_NAME + GCP_CLOUDSQL_CONNECTION_NAME + GCP_CLOUDSQL_PRIMARY_IP_ADDRESS + GCP_CLOUDSQL_VERSION + GCP_CLOUDSQL_DATABASE_NAME + GCP_CLOUDSQL_DATABASE_USER + GCP_CLOUDSQL_DATABASE_PASS + ] + + envs = %w[ + * + STG + PRD + ] + + keys.each do |key| + envs.each do |env| + project.variables.build(protected: false, environment_scope: env, key: key, value: "value-#{key}-#{env}") + end + end + end + + it 'result is grouped by environment', :aggregate_failures do + expect(service.execute).to contain_exactly({ + ref: '*', + gcp_project: 'value-GCP_PROJECT_ID-*', + instance_name: 'value-GCP_CLOUDSQL_INSTANCE_NAME-*', + version: 'value-GCP_CLOUDSQL_VERSION-*' + }, + { + ref: 'STG', + gcp_project: 'value-GCP_PROJECT_ID-STG', + instance_name: 'value-GCP_CLOUDSQL_INSTANCE_NAME-STG', + version: 'value-GCP_CLOUDSQL_VERSION-STG' + }, + { + ref: 'PRD', + gcp_project: 'value-GCP_PROJECT_ID-PRD', + instance_name: 'value-GCP_CLOUDSQL_INSTANCE_NAME-PRD', + version: 'value-GCP_CLOUDSQL_VERSION-PRD' + }) + end + end +end diff --git a/spec/services/google_cloud/setup_cloudsql_instance_service_spec.rb b/spec/services/google_cloud/setup_cloudsql_instance_service_spec.rb index 55553097423..e0a622bfa4a 100644 --- a/spec/services/google_cloud/setup_cloudsql_instance_service_spec.rb +++ b/spec/services/google_cloud/setup_cloudsql_instance_service_spec.rb @@ -5,6 +5,21 @@ require 'spec_helper' RSpec.describe GoogleCloud::SetupCloudsqlInstanceService do let(:random_user) { create(:user) } let(:project) { create(:project) } + let(:list_databases_empty) { Google::Apis::SqladminV1beta4::ListDatabasesResponse.new(items: []) } + let(:list_users_empty) { Google::Apis::SqladminV1beta4::ListUsersResponse.new(items: []) } + let(:list_databases) do + Google::Apis::SqladminV1beta4::ListDatabasesResponse.new(items: [ + Google::Apis::SqladminV1beta4::Database.new(name: 'postgres'), + Google::Apis::SqladminV1beta4::Database.new(name: 'main_db') + ]) + end + + let(:list_users) do + Google::Apis::SqladminV1beta4::ListUsersResponse.new(items: [ + Google::Apis::SqladminV1beta4::User.new(name: 'postgres'), + Google::Apis::SqladminV1beta4::User.new(name: 'main_user') + ]) + end context 'when unauthorized user triggers worker' do subject do @@ -76,6 +91,8 @@ RSpec.describe GoogleCloud::SetupCloudsqlInstanceService do allow_next_instance_of(GoogleApi::CloudPlatform::Client) do |google_api_client| expect(google_api_client).to receive(:get_cloudsql_instance).and_return(get_instance_response_runnable) expect(google_api_client).to receive(:create_cloudsql_database).and_return(operation_fail) + expect(google_api_client).to receive(:list_cloudsql_databases).and_return(list_databases_empty) + expect(google_api_client).to receive(:list_cloudsql_users).and_return(list_users_empty) end message = subject[:message] @@ -92,6 +109,8 @@ RSpec.describe GoogleCloud::SetupCloudsqlInstanceService do expect(google_api_client).to receive(:get_cloudsql_instance).and_return(get_instance_response_runnable) expect(google_api_client).to receive(:create_cloudsql_database).and_return(operation_done) expect(google_api_client).to receive(:create_cloudsql_user).and_return(operation_fail) + expect(google_api_client).to receive(:list_cloudsql_databases).and_return(list_databases_empty) + expect(google_api_client).to receive(:list_cloudsql_users).and_return(list_users_empty) end message = subject[:message] @@ -102,12 +121,59 @@ RSpec.describe GoogleCloud::SetupCloudsqlInstanceService do end end + context 'when database and user already exist' do + it 'does not try to create a database or user' do + allow_next_instance_of(GoogleApi::CloudPlatform::Client) do |google_api_client| + expect(google_api_client).to receive(:get_cloudsql_instance).and_return(get_instance_response_runnable) + expect(google_api_client).not_to receive(:create_cloudsql_database) + expect(google_api_client).not_to receive(:create_cloudsql_user) + expect(google_api_client).to receive(:list_cloudsql_databases).and_return(list_databases) + expect(google_api_client).to receive(:list_cloudsql_users).and_return(list_users) + end + + status = subject[:status] + expect(status).to eq(:success) + end + end + + context 'when database already exists' do + it 'does not try to create a database' do + allow_next_instance_of(GoogleApi::CloudPlatform::Client) do |google_api_client| + expect(google_api_client).to receive(:get_cloudsql_instance).and_return(get_instance_response_runnable) + expect(google_api_client).not_to receive(:create_cloudsql_database) + expect(google_api_client).to receive(:create_cloudsql_user).and_return(operation_done) + expect(google_api_client).to receive(:list_cloudsql_databases).and_return(list_databases) + expect(google_api_client).to receive(:list_cloudsql_users).and_return(list_users_empty) + end + + status = subject[:status] + expect(status).to eq(:success) + end + end + + context 'when user already exists' do + it 'does not try to create a user' do + allow_next_instance_of(GoogleApi::CloudPlatform::Client) do |google_api_client| + expect(google_api_client).to receive(:get_cloudsql_instance).and_return(get_instance_response_runnable) + expect(google_api_client).to receive(:create_cloudsql_database).and_return(operation_done) + expect(google_api_client).not_to receive(:create_cloudsql_user) + expect(google_api_client).to receive(:list_cloudsql_databases).and_return(list_databases_empty) + expect(google_api_client).to receive(:list_cloudsql_users).and_return(list_users) + end + + status = subject[:status] + expect(status).to eq(:success) + end + end + context 'when database and user creation succeeds' do it 'stores project CI vars' do allow_next_instance_of(GoogleApi::CloudPlatform::Client) do |google_api_client| expect(google_api_client).to receive(:get_cloudsql_instance).and_return(get_instance_response_runnable) expect(google_api_client).to receive(:create_cloudsql_database).and_return(operation_done) expect(google_api_client).to receive(:create_cloudsql_user).and_return(operation_done) + expect(google_api_client).to receive(:list_cloudsql_databases).and_return(list_databases_empty) + expect(google_api_client).to receive(:list_cloudsql_users).and_return(list_users_empty) end subject @@ -143,6 +209,8 @@ RSpec.describe GoogleCloud::SetupCloudsqlInstanceService do expect(google_api_client).to receive(:get_cloudsql_instance).and_return(get_instance_response_runnable) expect(google_api_client).to receive(:create_cloudsql_database).and_return(operation_done) expect(google_api_client).to receive(:create_cloudsql_user).and_return(operation_done) + expect(google_api_client).to receive(:list_cloudsql_databases).and_return(list_databases_empty) + expect(google_api_client).to receive(:list_cloudsql_users).and_return(list_users_empty) end subject diff --git a/spec/services/groups/create_service_spec.rb b/spec/services/groups/create_service_spec.rb index 6e074f451c4..0cfde9ef434 100644 --- a/spec/services/groups/create_service_spec.rb +++ b/spec/services/groups/create_service_spec.rb @@ -176,6 +176,15 @@ RSpec.describe Groups::CreateService, '#execute' do end end + describe 'creating a details record' do + let(:service) { described_class.new(user, group_params) } + + it 'create the details record connected to the group' do + group = subject + expect(group.namespace_details).to be_persisted + end + end + describe 'create service for the group' do let(:service) { described_class.new(user, group_params) } let(:created_group) { service.execute } diff --git a/spec/services/groups/destroy_service_spec.rb b/spec/services/groups/destroy_service_spec.rb index f43f64fdf89..0d699dd447b 100644 --- a/spec/services/groups/destroy_service_spec.rb +++ b/spec/services/groups/destroy_service_spec.rb @@ -7,7 +7,7 @@ RSpec.describe Groups::DestroyService do let!(:group) { create(:group) } let!(:nested_group) { create(:group, parent: group) } let!(:project) { create(:project, :repository, :legacy_storage, namespace: group) } - let!(:notification_setting) { create(:notification_setting, source: group)} + let!(:notification_setting) { create(:notification_setting, source: group) } let(:gitlab_shell) { Gitlab::Shell.new } let(:remove_path) { group.path + "+#{group.id}+deleted" } @@ -74,6 +74,17 @@ RSpec.describe Groups::DestroyService do end end end + + context 'event store', :sidekiq_might_not_need_inline do + it 'publishes a GroupDeletedEvent' do + expect { destroy_group(group, user, async) } + .to publish_event(Groups::GroupDeletedEvent) + .with( + group_id: group.id, + root_namespace_id: group.root_ancestor.id + ) + end + end end describe 'asynchronous delete' do @@ -271,7 +282,7 @@ RSpec.describe Groups::DestroyService do end context 'the shared_with group is deleted' do - let!(:group2_subgroup) { create(:group, :private, parent: group2)} + let!(:group2_subgroup) { create(:group, :private, parent: group2) } let!(:group2_subgroup_project) { create(:project, :private, group: group2_subgroup) } it 'updates project authorizations so users of both groups lose access', :aggregate_failures do diff --git a/spec/services/groups/merge_requests_count_service_spec.rb b/spec/services/groups/merge_requests_count_service_spec.rb index 10c7ba5fca4..8bd350d6f0e 100644 --- a/spec/services/groups/merge_requests_count_service_spec.rb +++ b/spec/services/groups/merge_requests_count_service_spec.rb @@ -4,7 +4,7 @@ require 'spec_helper' RSpec.describe Groups::MergeRequestsCountService, :use_clean_rails_memory_store_caching do let_it_be(:user) { create(:user) } - let_it_be(:group) { create(:group, :public)} + let_it_be(:group) { create(:group, :public) } let_it_be(:project) { create(:project, :repository, namespace: group) } let_it_be(:merge_request) { create(:merge_request, source_project: project, target_project: project) } diff --git a/spec/services/groups/open_issues_count_service_spec.rb b/spec/services/groups/open_issues_count_service_spec.rb index fca09bfdebe..923caa6c150 100644 --- a/spec/services/groups/open_issues_count_service_spec.rb +++ b/spec/services/groups/open_issues_count_service_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' RSpec.describe Groups::OpenIssuesCountService, :use_clean_rails_memory_store_caching do - let_it_be(:group) { create(:group, :public)} + let_it_be(:group) { create(:group, :public) } let_it_be(:project) { create(:project, :public, namespace: group) } let_it_be(:user) { create(:user) } let_it_be(:issue) { create(:issue, :opened, project: project) } diff --git a/spec/services/groups/transfer_service_spec.rb b/spec/services/groups/transfer_service_spec.rb index fbcca215282..b543661e9a0 100644 --- a/spec/services/groups/transfer_service_spec.rb +++ b/spec/services/groups/transfer_service_spec.rb @@ -22,6 +22,18 @@ RSpec.describe Groups::TransferService, :sidekiq_inline do let!(:group_member) { create(:group_member, :owner, group: group, user: user) } let(:transfer_service) { described_class.new(group, user) } + shared_examples 'publishes a GroupTransferedEvent' do + it do + expect { transfer_service.execute(target) } + .to publish_event(Groups::GroupTransferedEvent) + .with( + group_id: group.id, + old_root_namespace_id: group.root_ancestor.id, + new_root_namespace_id: target.root_ancestor.id + ) + end + end + context 'handling packages' do let_it_be(:group) { create(:group, :public) } let_it_be(:new_group) { create(:group, :public) } @@ -895,6 +907,10 @@ RSpec.describe Groups::TransferService, :sidekiq_inline do expect { transfer_service.execute(root_group) } .not_to change { CustomerRelations::IssueContact.count } end + + it_behaves_like 'publishes a GroupTransferedEvent' do + let(:target) { root_group } + end end context 'moving down' do @@ -904,6 +920,10 @@ RSpec.describe Groups::TransferService, :sidekiq_inline do expect { transfer_service.execute(another_subgroup) } .not_to change { CustomerRelations::IssueContact.count } end + + it_behaves_like 'publishes a GroupTransferedEvent' do + let(:target) { another_subgroup } + end end context 'moving sideways' do @@ -913,6 +933,10 @@ RSpec.describe Groups::TransferService, :sidekiq_inline do expect { transfer_service.execute(another_subgroup) } .not_to change { CustomerRelations::IssueContact.count } end + + it_behaves_like 'publishes a GroupTransferedEvent' do + let(:target) { another_subgroup } + end end context 'moving to new root group' do @@ -932,6 +956,10 @@ RSpec.describe Groups::TransferService, :sidekiq_inline do expect { transfer_service.execute(new_parent_group) } .not_to change { CustomerRelations::IssueContact.count } end + + it_behaves_like 'publishes a GroupTransferedEvent' do + let(:target) { new_parent_group } + end end context 'moving to a subgroup within a new root group' do @@ -953,6 +981,10 @@ RSpec.describe Groups::TransferService, :sidekiq_inline do expect { transfer_service.execute(subgroup_in_new_parent_group) } .not_to change { CustomerRelations::IssueContact.count } end + + it_behaves_like 'publishes a GroupTransferedEvent' do + let(:target) { subgroup_in_new_parent_group } + end end context 'with permission on the subgroup' do @@ -965,6 +997,11 @@ RSpec.describe Groups::TransferService, :sidekiq_inline do expect(transfer_service.error).to eq("Transfer failed: Group contains contacts/organizations and you don't have enough permissions to move them to the new root group.") end + + it 'does not publish a GroupTransferedEvent' do + expect { transfer_service.execute(subgroup_in_new_parent_group) } + .not_to publish_event(Groups::GroupTransferedEvent) + end end end end diff --git a/spec/services/groups/update_service_spec.rb b/spec/services/groups/update_service_spec.rb index 856dd4a2567..5c87b9ac8bb 100644 --- a/spec/services/groups/update_service_spec.rb +++ b/spec/services/groups/update_service_spec.rb @@ -339,8 +339,44 @@ RSpec.describe Groups::UpdateService do end end + context 'EventStore' do + let(:service) { described_class.new(group, user, **params) } + let(:root_group) { create(:group, path: 'root') } + let(:group) do + create(:group, parent: root_group, path: 'old-path').tap { |g| g.add_owner(user) } + end + + context 'when changing a group path' do + let(:new_path) { SecureRandom.hex } + let(:params) { { path: new_path } } + + it 'publishes a GroupPathChangedEvent' do + old_path = group.full_path + + expect { service.execute } + .to publish_event(Groups::GroupPathChangedEvent) + .with( + group_id: group.id, + root_namespace_id: group.root_ancestor.id, + old_path: old_path, + new_path: "root/#{new_path}" + ) + end + end + + context 'when not changing a group path' do + let(:params) { { name: 'very-new-name' } } + + it 'does not publish a GroupPathChangedEvent' do + expect { service.execute } + .not_to publish_event(Groups::GroupPathChangedEvent) + end + end + end + context 'rename group' do - let!(:service) { described_class.new(internal_group, user, path: SecureRandom.hex) } + let(:new_path) { SecureRandom.hex } + let!(:service) { described_class.new(internal_group, user, path: new_path) } before do internal_group.add_member(user, Gitlab::Access::MAINTAINER) @@ -371,7 +407,7 @@ RSpec.describe Groups::UpdateService do end it "hasn't changed the path" do - expect { service.execute}.not_to change { internal_group.reload.path} + expect { service.execute }.not_to change { internal_group.reload.path } end end end diff --git a/spec/services/groups/update_statistics_service_spec.rb b/spec/services/groups/update_statistics_service_spec.rb index 5bef51c2727..84b18b670a7 100644 --- a/spec/services/groups/update_statistics_service_spec.rb +++ b/spec/services/groups/update_statistics_service_spec.rb @@ -7,7 +7,7 @@ RSpec.describe Groups::UpdateStatisticsService do let(:statistics) { %w(wiki_size) } - subject(:service) { described_class.new(group, statistics: statistics)} + subject(:service) { described_class.new(group, statistics: statistics) } describe '#execute', :aggregate_failures do context 'when group is nil' do diff --git a/spec/services/import/fogbugz_service_spec.rb b/spec/services/import/fogbugz_service_spec.rb index c9477dba7a5..7b86c5c45b0 100644 --- a/spec/services/import/fogbugz_service_spec.rb +++ b/spec/services/import/fogbugz_service_spec.rb @@ -119,7 +119,7 @@ RSpec.describe Import::FogbugzService do let(:error_messages_array) { instance_double(Array, join: "something went wrong") } let(:errors_double) { instance_double(ActiveModel::Errors, full_messages: error_messages_array, :[] => nil) } let(:project_double) { instance_double(Project, persisted?: false, errors: errors_double) } - let(:project_creator) { instance_double(Gitlab::FogbugzImport::ProjectCreator, execute: project_double )} + let(:project_creator) { instance_double(Gitlab::FogbugzImport::ProjectCreator, execute: project_double ) } before do allow(Gitlab::FogbugzImport::ProjectCreator).to receive(:new).and_return(project_creator) diff --git a/spec/services/import/prepare_service_spec.rb b/spec/services/import/prepare_service_spec.rb new file mode 100644 index 00000000000..0097198f7a9 --- /dev/null +++ b/spec/services/import/prepare_service_spec.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Import::PrepareService do + let_it_be(:project) { create(:project) } + let_it_be(:user) { create(:user) } + + let(:file) { double } + let(:upload_service) { double } + let(:uploader) { double } + let(:upload) { double } + + let(:service) { described_class.new(project, user, file: file) } + + subject { service.execute } + + context 'when file is uploaded correctly' do + let(:upload_id) { 99 } + + before do + mock_upload + end + + it 'raises NotImplemented error for worker' do + expect { subject }.to raise_error(NotImplementedError) + end + + context 'when a job is enqueued' do + before do + worker = double + + allow(service).to receive(:worker).and_return(worker) + allow(worker).to receive(:perform_async) + end + + it 'raises NotImplemented error for success_message when a job is enqueued' do + expect { subject }.to raise_error(NotImplementedError) + end + + it 'returns a success respnse when a success_message is implemented' do + message = 'It works!' + + allow(service).to receive(:success_message).and_return(message) + + result = subject + + expect(result).to be_success + expect(result.message).to eq(message) + end + end + end + + context 'when file upload fails' do + before do + mock_upload(false) + end + + it 'returns an error message' do + result = subject + + expect(result[:status]).to eq(:error) + expect(result[:message]).to eq('File upload error.') + end + end +end diff --git a/spec/services/import/validate_remote_git_endpoint_service_spec.rb b/spec/services/import/validate_remote_git_endpoint_service_spec.rb index 9dc862b6ca3..221ac2cd73a 100644 --- a/spec/services/import/validate_remote_git_endpoint_service_spec.rb +++ b/spec/services/import/validate_remote_git_endpoint_service_spec.rb @@ -25,7 +25,7 @@ RSpec.describe Import::ValidateRemoteGitEndpointService do end context 'when uri is using git:// protocol' do - subject { described_class.new(url: 'git://demo.host/repo')} + subject { described_class.new(url: 'git://demo.host/repo') } it 'returns success' do result = subject.execute diff --git a/spec/services/incident_management/timeline_events/create_service_spec.rb b/spec/services/incident_management/timeline_events/create_service_spec.rb index a4e928b98f4..b999403e168 100644 --- a/spec/services/incident_management/timeline_events/create_service_spec.rb +++ b/spec/services/incident_management/timeline_events/create_service_spec.rb @@ -244,5 +244,88 @@ RSpec.describe IncidentManagement::TimelineEvents::CreateService do it_behaves_like 'successfully created timeline event' end + + describe '.change_labels' do + subject(:execute) do + described_class.change_labels(incident, current_user, added_labels: added, removed_labels: removed) + end + + let_it_be(:labels) { create_list(:label, 4, project: project) } + + let(:expected_action) { 'label' } + + context 'when there are neither added nor removed labels' do + let(:added) { [] } + let(:removed) { [] } + + it 'responds with error', :aggregate_failures do + expect(execute).to be_error + expect(execute.message).to eq(_('There are no changed labels')) + end + + it 'does not create timeline event' do + expect { execute }.not_to change { incident.incident_management_timeline_events.count } + end + end + + context 'when there are only added labels' do + let(:added) { [labels[0], labels[1]] } + let(:removed) { [] } + + let(:expected_note) { "@#{current_user.username} added #{added.map(&:to_reference).join(' ')} labels" } + + it_behaves_like 'successfully created timeline event' + end + + context 'when there are only removed labels' do + let(:added) { [] } + let(:removed) { [labels[2], labels[3]] } + + let(:expected_note) { "@#{current_user.username} removed #{removed.map(&:to_reference).join(' ')} labels" } + + it_behaves_like 'successfully created timeline event' + end + + context 'when there are both added and removed labels' do + let(:added) { [labels[0], labels[1]] } + let(:removed) { [labels[2], labels[3]] } + + let(:expected_note) do + added_note = "added #{added.map(&:to_reference).join(' ')} labels" + removed_note = "removed #{removed.map(&:to_reference).join(' ')} labels" + + "@#{current_user.username} #{added_note} and #{removed_note}" + end + + it_behaves_like 'successfully created timeline event' + end + + context 'when there is a single added and single removed labels' do + let(:added) { [labels[0]] } + let(:removed) { [labels[3]] } + + let(:expected_note) do + added_note = "added #{added.first.to_reference} label" + removed_note = "removed #{removed.first.to_reference} label" + + "@#{current_user.username} #{added_note} and #{removed_note}" + end + + it_behaves_like 'successfully created timeline event' + end + + context 'when feature flag is disabled' do + let(:added) { [labels[0], labels[1]] } + let(:removed) { [labels[2], labels[3]] } + + before do + stub_feature_flags(incident_timeline_events_from_labels: false) + end + + it 'does not create timeline event' do + expect { execute }.not_to change { incident.incident_management_timeline_events.count } + end + end + end end end diff --git a/spec/services/incident_management/timeline_events/update_service_spec.rb b/spec/services/incident_management/timeline_events/update_service_spec.rb index 728f2fa3e9d..f612c72e2a8 100644 --- a/spec/services/incident_management/timeline_events/update_service_spec.rb +++ b/spec/services/incident_management/timeline_events/update_service_spec.rb @@ -32,6 +32,10 @@ RSpec.describe IncidentManagement::TimelineEvents::UpdateService do expect(execute.message).to eq(message) end + it 'does not update the note' do + expect { execute }.not_to change { timeline_event.reload.note } + end + it_behaves_like 'does not track incident management event', :incident_management_timeline_event_edited end @@ -94,16 +98,7 @@ RSpec.describe IncidentManagement::TimelineEvents::UpdateService do context 'when note is blank' do let(:params) { { note: '', occurred_at: occurred_at } } - it_behaves_like 'successful response' - it_behaves_like 'passing the correct was_changed value', :occurred_at - - it 'does not update the note' do - expect { execute }.not_to change { timeline_event.reload.note } - end - - it 'updates occurred_at' do - expect { execute }.to change { timeline_event.occurred_at }.to(params[:occurred_at]) - end + it_behaves_like 'error response', "Note can't be blank" end context 'when occurred_at is nil' do @@ -121,6 +116,12 @@ RSpec.describe IncidentManagement::TimelineEvents::UpdateService do end end + context 'when occurred_at is blank' do + let(:params) { { note: 'Updated note', occurred_at: '' } } + + it_behaves_like 'error response', "Occurred at can't be blank" + end + context 'when both occurred_at and note is nil' do let(:params) { {} } diff --git a/spec/services/issuable/common_system_notes_service_spec.rb b/spec/services/issuable/common_system_notes_service_spec.rb index 1426ef2a1f6..0d2b8a4ac3c 100644 --- a/spec/services/issuable/common_system_notes_service_spec.rb +++ b/spec/services/issuable/common_system_notes_service_spec.rb @@ -8,6 +8,37 @@ RSpec.describe Issuable::CommonSystemNotesService do let(:issuable) { create(:issue, project: project) } + shared_examples 'system note for issuable date changes' do + it 'creates a system note for due_date set' do + issuable.update!(due_date: Date.today) + + expect { subject }.to change(issuable.notes, :count).from(0).to(1) + expect(issuable.notes.last.note).to match('changed due date to') + end + + it 'creates a system note for start_date set' do + issuable.update!(start_date: Date.today) + + expect { subject }.to change(issuable.notes, :count).from(0).to(1) + expect(issuable.notes.last.note).to match('changed start date to') + end + + it 'creates a note when both start and due date are changed' do + issuable.update!(start_date: Date.today, due_date: 1.week.from_now) + + expect { subject }.to change { issuable.notes.count }.from(0).to(1) + expect(issuable.notes.last.note).to match(/changed start date to.+and changed due date to/) + end + + it 'does not call SystemNoteService if no dates are changed' do + issuable.update!(title: 'new title') + + expect(SystemNoteService).not_to receive(:change_start_date_or_due_date) + + subject + end + end + context 'on issuable update' do it_behaves_like 'system note creation', { title: 'New title' }, 'changed title' it_behaves_like 'system note creation', { description: 'New description' }, 'changed the description' @@ -61,6 +92,12 @@ RSpec.describe Issuable::CommonSystemNotesService do end end end + + context 'when changing dates' do + it_behaves_like 'system note for issuable date changes' do + subject { described_class.new(project: project, current_user: user).execute(issuable) } + end + end end context 'on issuable create' do @@ -102,12 +139,8 @@ RSpec.describe Issuable::CommonSystemNotesService do end end - it 'creates a system note for due_date set' do - issuable.due_date = Date.today - issuable.save! - - expect { subject }.to change { issuable.notes.count }.from(0).to(1) - expect(issuable.notes.last.note).to match('changed due date') + context 'when changing dates' do + it_behaves_like 'system note for issuable date changes' end end end diff --git a/spec/services/issues/clone_service_spec.rb b/spec/services/issues/clone_service_spec.rb index 858dfc4ab3a..435488b7f66 100644 --- a/spec/services/issues/clone_service_spec.rb +++ b/spec/services/issues/clone_service_spec.rb @@ -57,8 +57,20 @@ RSpec.describe Issues::CloneService do expect(old_issue.notes.last.note).to start_with 'cloned to' end - it 'adds system note to new issue at the end' do - expect(new_issue.notes.last.note).to start_with 'cloned from' + it 'adds system note to new issue at the start' do + # We set an assignee so an assignee system note will be generated and + # we can assert that the "cloned from" note is the first one + assignee = create(:user) + new_project.add_developer(assignee) + old_issue.assignees = [assignee] + + new_issue = clone_service.execute(old_issue, new_project) + + expect(new_issue.notes.size).to eq(2) + + cloned_from_note = new_issue.notes.last + expect(cloned_from_note.note).to start_with 'cloned from' + expect(new_issue.notes.fresh.first).to eq(cloned_from_note) end it 'keeps old issue open' do @@ -128,11 +140,11 @@ RSpec.describe Issues::CloneService do context 'issue with award emoji' do let!(:award_emoji) { create(:award_emoji, awardable: old_issue) } - it 'copies the award emoji' do + it 'does not copy the award emoji' do old_issue.reload new_issue = clone_service.execute(old_issue, new_project) - expect(old_issue.award_emoji.first.name).to eq new_issue.reload.award_emoji.first.name + expect(new_issue.reload.award_emoji).to be_empty end end @@ -170,19 +182,21 @@ RSpec.describe Issues::CloneService do context 'issue with due date' do let(:date) { Date.parse('2020-01-10') } + let(:new_date) { date + 1.week } let(:old_issue) do create(:issue, title: title, description: description, project: old_project, author: author, due_date: date) end before do - SystemNoteService.change_due_date(old_issue, old_project, author, old_issue.due_date) + old_issue.update!(due_date: new_date) + SystemNoteService.change_start_date_or_due_date(old_issue, old_project, author, old_issue.previous_changes.slice('due_date')) end it 'keeps the same due date' do new_issue = clone_service.execute(old_issue, new_project) - expect(new_issue.due_date).to eq(date) + expect(new_issue.due_date).to eq(old_issue.due_date) end end diff --git a/spec/services/issues/create_service_spec.rb b/spec/services/issues/create_service_spec.rb index 0bc8511e3e3..80c455e72b0 100644 --- a/spec/services/issues/create_service_spec.rb +++ b/spec/services/issues/create_service_spec.rb @@ -69,6 +69,12 @@ RSpec.describe Issues::CreateService do expect(issue.issue_customer_relations_contacts).to be_empty end + it 'calls NewIssueWorker with correct arguments' do + expect(NewIssueWorker).to receive(:perform_async).with(Integer, user.id, 'Issue') + + issue + end + context 'when a build_service is provided' do let(:issue) { described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params, build_service: build_service).execute } @@ -143,6 +149,12 @@ RSpec.describe Issues::CreateService do issue end + it 'calls NewIssueWorker with correct arguments' do + expect(NewIssueWorker).to receive(:perform_async).with(Integer, reporter.id, 'Issue') + + issue + end + context 'when invalid' do before do opts.merge!(title: '') diff --git a/spec/services/issues/move_service_spec.rb b/spec/services/issues/move_service_spec.rb index 5a1bb2e8b74..863df810d01 100644 --- a/spec/services/issues/move_service_spec.rb +++ b/spec/services/issues/move_service_spec.rb @@ -16,7 +16,7 @@ RSpec.describe Issues::MoveService do let_it_be(:new_project) { create(:project, namespace: sub_group_2) } let(:old_issue) do - create(:issue, title: title, description: description, project: old_project, author: author) + create(:issue, title: title, description: description, project: old_project, author: author, created_at: 1.day.ago, updated_at: 1.day.ago) end subject(:move_service) do @@ -62,8 +62,11 @@ RSpec.describe Issues::MoveService do expect(old_issue.notes.last.note).to start_with 'moved to' end - it 'adds system note to new issue at the end' do - expect(new_issue.notes.last.note).to start_with 'moved from' + it 'adds system note to new issue at the end', :freeze_time do + system_note = new_issue.notes.last + + expect(system_note.note).to start_with 'moved from' + expect(system_note.created_at).to be_like_time(Time.current) end it 'closes old issue' do @@ -137,7 +140,8 @@ RSpec.describe Issues::MoveService do end before do - SystemNoteService.change_due_date(old_issue, old_project, author, old_issue.due_date) + old_issue.update!(due_date: Date.today) + SystemNoteService.change_start_date_or_due_date(old_issue, old_project, author, old_issue.previous_changes.slice('due_date')) end it 'does not create extra system notes' do diff --git a/spec/services/issues/prepare_import_csv_service_spec.rb b/spec/services/issues/prepare_import_csv_service_spec.rb new file mode 100644 index 00000000000..ded23ee43b9 --- /dev/null +++ b/spec/services/issues/prepare_import_csv_service_spec.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Issues::PrepareImportCsvService do + let_it_be(:project) { create(:project) } + let_it_be(:user) { create(:user) } + + let(:file) { double } + let(:upload_service) { double } + let(:uploader) { double } + let(:upload) { double } + + let(:subject) do + described_class.new(project, user, file: file).execute + end + + context 'when file is uploaded correctly' do + let(:upload_id) { 99 } + + before do + mock_upload + end + + it 'returns a success message' do + result = subject + + expect(result[:status]).to eq(:success) + expect(result[:message]).to eq("Your issues are being imported. Once finished, you'll get a confirmation email.") + end + + it 'enqueues the ImportRequirementsCsvWorker' do + expect(ImportIssuesCsvWorker).to receive(:perform_async).with(user.id, project.id, upload_id) + + subject + end + end + + context 'when file upload fails' do + before do + mock_upload(false) + end + + it 'returns an error message' do + result = subject + + expect(result[:status]).to eq(:error) + expect(result[:message]).to eq('File upload error.') + end + end +end diff --git a/spec/services/issues/referenced_merge_requests_service_spec.rb b/spec/services/issues/referenced_merge_requests_service_spec.rb index dc55ba8ebea..16166c1fa33 100644 --- a/spec/services/issues/referenced_merge_requests_service_spec.rb +++ b/spec/services/issues/referenced_merge_requests_service_spec.rb @@ -106,7 +106,7 @@ RSpec.describe Issues::ReferencedMergeRequestsService do end describe '#closed_by_merge_requests' do - let(:closed_issue) { build(:issue, :closed, project: project)} + let(:closed_issue) { build(:issue, :closed, project: project) } it 'returns the open merge requests that close this issue' do create_closing_mr(source_project: project, state: 'closed') diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb index e2e8828ae89..aef3608831c 100644 --- a/spec/services/issues/update_service_spec.rb +++ b/spec/services/issues/update_service_spec.rb @@ -849,8 +849,8 @@ RSpec.describe Issues::UpdateService, :mailer do end it 'creates system note about task status change' do - note1 = find_note('marked the task **Task 1** as completed') - note2 = find_note('marked the task **Task 2** as completed') + note1 = find_note('marked the checklist item **Task 1** as completed') + note2 = find_note('marked the checklist item **Task 2** as completed') expect(note1).not_to be_nil expect(note2).not_to be_nil @@ -867,8 +867,8 @@ RSpec.describe Issues::UpdateService, :mailer do end it 'creates system note about task status change' do - note1 = find_note('marked the task **Task 1** as incomplete') - note2 = find_note('marked the task **Task 2** as incomplete') + note1 = find_note('marked the checklist item **Task 1** as incomplete') + note2 = find_note('marked the checklist item **Task 2** as incomplete') expect(note1).not_to be_nil expect(note2).not_to be_nil @@ -885,7 +885,7 @@ RSpec.describe Issues::UpdateService, :mailer do end it 'does not create a system note for the task' do - task_note = find_note('marked the task **Task 2** as incomplete') + task_note = find_note('marked the checklist item **Task 2** as incomplete') description_notes = find_notes('description') expect(task_note).to be_nil @@ -900,7 +900,7 @@ RSpec.describe Issues::UpdateService, :mailer do end it 'does not create a system note referencing the position the old item' do - task_note = find_note('marked the task **Two** as incomplete') + task_note = find_note('marked the checklist item **Two** as incomplete') description_notes = find_notes('description') expect(task_note).to be_nil @@ -988,6 +988,52 @@ RSpec.describe Issues::UpdateService, :mailer do end end + context 'updating dates' do + subject(:result) { described_class.new(project: project, current_user: user, params: params).execute(issue) } + + let(:updated_date) { 1.week.from_now.to_date } + + shared_examples 'issue update service that triggers date updates' do + it 'triggers graphql date updated subscription' do + expect(GraphqlTriggers).to receive(:issuable_dates_updated).with(issue).and_call_original + + result + end + end + + shared_examples 'issue update service that does not trigger date updates' do + it 'does not trigger date updated subscriptions' do + expect(GraphqlTriggers).not_to receive(:issuable_dates_updated) + + result + end + end + + context 'when due_date is updated' do + let(:params) { { due_date: updated_date } } + + it_behaves_like 'issue update service that triggers date updates' + end + + context 'when start_date is updated' do + let(:params) { { start_date: updated_date } } + + it_behaves_like 'issue update service that triggers date updates' + end + + context 'when no date is updated' do + let(:params) { { title: 'should not trigger date updates' } } + + it_behaves_like 'issue update service that does not trigger date updates' + end + + context 'when update is not successful but date is provided' do + let(:params) { { title: '', due_date: updated_date } } + + it_behaves_like 'issue update service that does not trigger date updates' + end + end + context 'updating asssignee_id' do it 'does not update assignee when assignee_id is invalid' do update_issue(assignee_ids: [-1]) diff --git a/spec/services/jira_import/start_import_service_spec.rb b/spec/services/jira_import/start_import_service_spec.rb index 510f58f0e75..c0db3012a30 100644 --- a/spec/services/jira_import/start_import_service_spec.rb +++ b/spec/services/jira_import/start_import_service_spec.rb @@ -136,7 +136,7 @@ RSpec.describe JiraImport::StartImportService do end context 'when multiple Jira imports for same Jira project' do - let!(:jira_imports) { create_list(:jira_import_state, 3, :finished, project: project, jira_project_key: fake_key)} + let!(:jira_imports) { create_list(:jira_import_state, 3, :finished, project: project, jira_project_key: fake_key) } it 'creates Jira label title with correct number' do jira_import = subject.payload[:import_data] diff --git a/spec/services/lfs/push_service_spec.rb b/spec/services/lfs/push_service_spec.rb index e1564ca2359..f52bba94eea 100644 --- a/spec/services/lfs/push_service_spec.rb +++ b/spec/services/lfs/push_service_spec.rb @@ -98,7 +98,7 @@ RSpec.describe Lfs::PushService do end def batch_spec(*objects, upload: true, verify: false) - { 'transfer' => 'basic', 'objects' => objects.map {|o| object_spec(o, upload: upload) } } + { 'transfer' => 'basic', 'objects' => objects.map { |o| object_spec(o, upload: upload) } } end def object_spec(object, upload: true, verify: false) diff --git a/spec/services/markdown_content_rewriter_service_spec.rb b/spec/services/markdown_content_rewriter_service_spec.rb index 91a117536ca..d94289856cf 100644 --- a/spec/services/markdown_content_rewriter_service_spec.rb +++ b/spec/services/markdown_content_rewriter_service_spec.rb @@ -8,7 +8,7 @@ RSpec.describe MarkdownContentRewriterService do let_it_be(:target_parent) { create(:project, :public) } let(:content) { 'My content' } - let(:issue) { create(:issue, project: source_parent, description: content)} + let(:issue) { create(:issue, project: source_parent, description: content) } describe '#initialize' do it 'raises an error if source_parent is not a Project' do diff --git a/spec/services/members/groups/creator_service_spec.rb b/spec/services/members/groups/creator_service_spec.rb index 4130fbd44fa..fced7195046 100644 --- a/spec/services/members/groups/creator_service_spec.rb +++ b/spec/services/members/groups/creator_service_spec.rb @@ -27,7 +27,10 @@ RSpec.describe Members::Groups::CreatorService do context 'authorized projects update' do it 'schedules a single project authorization update job when called multiple times' do - expect(AuthorizedProjectsWorker).to receive(:bulk_perform_and_wait).once + # this is inline with the overridden behaviour in stubbed_member.rb + worker_instance = AuthorizedProjectsWorker.new + expect(AuthorizedProjectsWorker).to receive(:new).once.and_return(worker_instance) + expect(worker_instance).to receive(:perform).with(user.id) 1.upto(3) do described_class.add_member(source, user, :maintainer) diff --git a/spec/services/members/invite_service_spec.rb b/spec/services/members/invite_service_spec.rb index d25c8996931..6dbe161ee02 100644 --- a/spec/services/members/invite_service_spec.rb +++ b/spec/services/members/invite_service_spec.rb @@ -455,7 +455,7 @@ RSpec.describe Members::InviteService, :aggregate_failures, :clean_gitlab_redis_ end context 'when access_level is lower than inheriting member' do - let(:params) { { user_id: group_member.user_id, access_level: ProjectMember::GUEST }} + let(:params) { { user_id: group_member.user_id, access_level: ProjectMember::GUEST } } it 'does not add the member and returns an error' do msg = "Access level should be greater than or equal " \ @@ -467,7 +467,7 @@ RSpec.describe Members::InviteService, :aggregate_failures, :clean_gitlab_redis_ end context 'when access_level is the same as the inheriting member' do - let(:params) { { user_id: group_member.user_id, access_level: ProjectMember::DEVELOPER }} + let(:params) { { user_id: group_member.user_id, access_level: ProjectMember::DEVELOPER } } it 'adds the member with correct access_level' do expect_to_create_members(count: 1) @@ -477,7 +477,7 @@ RSpec.describe Members::InviteService, :aggregate_failures, :clean_gitlab_redis_ end context 'when access_level is greater than the inheriting member' do - let(:params) { { user_id: group_member.user_id, access_level: ProjectMember::MAINTAINER }} + let(:params) { { user_id: group_member.user_id, access_level: ProjectMember::MAINTAINER } } it 'adds the member with correct access_level' do expect_to_create_members(count: 1) diff --git a/spec/services/merge_requests/approval_service_spec.rb b/spec/services/merge_requests/approval_service_spec.rb index e1fbb945ee3..ab98fad5d73 100644 --- a/spec/services/merge_requests/approval_service_spec.rb +++ b/spec/services/merge_requests/approval_service_spec.rb @@ -20,79 +20,111 @@ RSpec.describe MergeRequests::ApprovalService do allow(merge_request.approvals).to receive(:new).and_return(double(save: false)) end - it 'does not create an approval note' do - expect(SystemNoteService).not_to receive(:approve_mr) + it 'does not reset approvals' do + expect(merge_request.approvals).not_to receive(:reset) service.execute(merge_request) end - it 'does not mark pending todos as done' do - service.execute(merge_request) - - expect(todo.reload).to be_pending - end - it 'does not track merge request approve action' do expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter) .not_to receive(:track_approve_mr_action).with(user: user) service.execute(merge_request) end - end - - context 'with valid approval' do - let(:notification_service) { NotificationService.new } - before do - allow(service).to receive(:notification_service).and_return(notification_service) + it 'does not publish MergeRequests::ApprovedEvent' do + expect { service.execute(merge_request) }.not_to publish_event(MergeRequests::ApprovedEvent) end - it 'creates an approval note and marks pending todos as done' do - expect(SystemNoteService).to receive(:approve_mr).with(merge_request, user) - expect(merge_request.approvals).to receive(:reset) + context 'async_after_approval feature flag is disabled' do + before do + stub_feature_flags(async_after_approval: false) + end - service.execute(merge_request) + it 'does not create approve MR event' do + expect(EventCreateService).not_to receive(:new) - expect(todo.reload).to be_done - end + service.execute(merge_request) + end - it 'creates approve MR event' do - expect_next_instance_of(EventCreateService) do |instance| - expect(instance).to receive(:approve_mr) - .with(merge_request, user) + it 'does not create an approval note' do + expect(SystemNoteService).not_to receive(:approve_mr) + + service.execute(merge_request) end - service.execute(merge_request) + it 'does not mark pending todos as done' do + service.execute(merge_request) + + expect(todo.reload).to be_pending + end end + end - it 'sends a notification when approving' do - expect(notification_service).to receive_message_chain(:async, :approve_mr) - .with(merge_request, user) + context 'with valid approval' do + it 'resets approvals' do + expect(merge_request.approvals).to receive(:reset) service.execute(merge_request) end - it 'removes attention requested state' do - expect(MergeRequests::RemoveAttentionRequestedService).to receive(:new) - .with(project: project, current_user: user, merge_request: merge_request, user: user) - .and_call_original + it 'tracks merge request approve action' do + expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter) + .to receive(:track_approve_mr_action).with(user: user, merge_request: merge_request) service.execute(merge_request) end - context 'with remaining approvals' do - it 'fires an approval webhook' do - expect(service).to receive(:execute_hooks).with(merge_request, 'approved') + it 'publishes MergeRequests::ApprovedEvent' do + expect { service.execute(merge_request) } + .to publish_event(MergeRequests::ApprovedEvent) + .with(current_user_id: user.id, merge_request_id: merge_request.id) + end + + context 'async_after_approval feature flag is disabled' do + let(:notification_service) { NotificationService.new } + + before do + stub_feature_flags(async_after_approval: false) + allow(service).to receive(:notification_service).and_return(notification_service) + end + + it 'creates approve MR event' do + expect_next_instance_of(EventCreateService) do |instance| + expect(instance).to receive(:approve_mr) + .with(merge_request, user) + end service.execute(merge_request) end - end - it 'tracks merge request approve action' do - expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter) - .to receive(:track_approve_mr_action).with(user: user, merge_request: merge_request) + it 'creates an approval note' do + expect(SystemNoteService).to receive(:approve_mr).with(merge_request, user) - service.execute(merge_request) + service.execute(merge_request) + end + + it 'marks pending todos as done' do + service.execute(merge_request) + + expect(todo.reload).to be_done + end + + it 'sends a notification when approving' do + expect(notification_service).to receive_message_chain(:async, :approve_mr) + .with(merge_request, user) + + service.execute(merge_request) + end + + context 'with remaining approvals' do + it 'fires an approval webhook' do + expect(service).to receive(:execute_hooks).with(merge_request, 'approved') + + service.execute(merge_request) + end + end end end diff --git a/spec/services/merge_requests/bulk_remove_attention_requested_service_spec.rb b/spec/services/merge_requests/bulk_remove_attention_requested_service_spec.rb deleted file mode 100644 index b2326a28e63..00000000000 --- a/spec/services/merge_requests/bulk_remove_attention_requested_service_spec.rb +++ /dev/null @@ -1,49 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe MergeRequests::BulkRemoveAttentionRequestedService do - let(:current_user) { create(:user) } - let(:user) { create(:user) } - let(:assignee_user) { create(:user) } - let(:merge_request) { create(:merge_request, reviewers: [user], assignees: [assignee_user]) } - let(:reviewer) { merge_request.find_reviewer(user) } - let(:assignee) { merge_request.find_assignee(assignee_user) } - let(:project) { merge_request.project } - let(:service) { described_class.new(project: project, current_user: current_user, merge_request: merge_request, users: [user, assignee_user]) } - let(:result) { service.execute } - - before do - project.add_developer(current_user) - project.add_developer(user) - end - - describe '#execute' do - context 'invalid permissions' do - let(:service) { described_class.new(project: project, current_user: create(:user), merge_request: merge_request, users: [user]) } - - it 'returns an error' do - expect(result[:status]).to eq :error - end - end - - context 'updates reviewers and assignees' do - it 'returns success' do - expect(result[:status]).to eq :success - end - - it 'updates reviewers state' do - service.execute - reviewer.reload - assignee.reload - - expect(reviewer.state).to eq 'reviewed' - expect(assignee.state).to eq 'reviewed' - end - - it_behaves_like 'invalidates attention request cache' do - let(:users) { [assignee_user, user] } - end - end - end -end diff --git a/spec/services/merge_requests/close_service_spec.rb b/spec/services/merge_requests/close_service_spec.rb index cd1c362a19f..8f448184b45 100644 --- a/spec/services/merge_requests/close_service_spec.rb +++ b/spec/services/merge_requests/close_service_spec.rb @@ -54,10 +54,6 @@ RSpec.describe MergeRequests::CloseService do expect(todo.reload).to be_done end - it 'removes attention requested state' do - expect(merge_request.find_assignee(user2).attention_requested?).to eq(false) - end - context 'when auto merge is enabled' do let(:merge_request) { create(:merge_request, :merge_when_pipeline_succeeds) } diff --git a/spec/services/merge_requests/create_approval_event_service_spec.rb b/spec/services/merge_requests/create_approval_event_service_spec.rb new file mode 100644 index 00000000000..3d41ace11a7 --- /dev/null +++ b/spec/services/merge_requests/create_approval_event_service_spec.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe MergeRequests::CreateApprovalEventService do + let(:user) { create(:user) } + let(:merge_request) { create(:merge_request) } + let(:project) { merge_request.project } + + subject(:service) { described_class.new(project: project, current_user: user) } + + describe '#execute' do + it 'creates approve MR event' do + expect_next_instance_of(EventCreateService) do |instance| + expect(instance).to receive(:approve_mr) + .with(merge_request, user) + end + + service.execute(merge_request) + end + end +end diff --git a/spec/services/merge_requests/create_pipeline_service_spec.rb b/spec/services/merge_requests/create_pipeline_service_spec.rb index 03a37ea59a3..c443d758a77 100644 --- a/spec/services/merge_requests/create_pipeline_service_spec.rb +++ b/spec/services/merge_requests/create_pipeline_service_spec.rb @@ -74,6 +74,16 @@ RSpec.describe MergeRequests::CreatePipelineService do expect(response.payload.project).to eq(project) end + context 'when the feature is disabled in CI/CD settings' do + before do + project.update!(ci_allow_fork_pipelines_to_run_in_parent_project: false) + end + + it 'creates a pipeline in the source project' do + expect(response.payload.project).to eq(source_project) + end + end + context 'when source branch is protected' do context 'when actor does not have permission to update the protected branch in target project' do let!(:protected_branch) { create(:protected_branch, name: '*', project: project) } diff --git a/spec/services/merge_requests/execute_approval_hooks_service_spec.rb b/spec/services/merge_requests/execute_approval_hooks_service_spec.rb new file mode 100644 index 00000000000..863c47e8191 --- /dev/null +++ b/spec/services/merge_requests/execute_approval_hooks_service_spec.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe MergeRequests::ExecuteApprovalHooksService do + let(:user) { create(:user) } + let(:merge_request) { create(:merge_request) } + let(:project) { merge_request.project } + + subject(:service) { described_class.new(project: project, current_user: user) } + + describe '#execute' do + let(:notification_service) { NotificationService.new } + + before do + allow(service).to receive(:notification_service).and_return(notification_service) + end + it 'sends a notification when approving' do + expect(notification_service).to receive_message_chain(:async, :approve_mr) + .with(merge_request, user) + + service.execute(merge_request) + end + + context 'with remaining approvals' do + it 'fires an approval webhook' do + expect(service).to receive(:execute_hooks).with(merge_request, 'approved') + + service.execute(merge_request) + end + end + end +end diff --git a/spec/services/merge_requests/handle_assignees_change_service_spec.rb b/spec/services/merge_requests/handle_assignees_change_service_spec.rb index fa3b1614e21..c43f5db6059 100644 --- a/spec/services/merge_requests/handle_assignees_change_service_spec.rb +++ b/spec/services/merge_requests/handle_assignees_change_service_spec.rb @@ -87,14 +87,6 @@ RSpec.describe MergeRequests::HandleAssigneesChangeService do expect(todo).to be_pending end - it 'removes attention requested state' do - expect(MergeRequests::RemoveAttentionRequestedService).to receive(:new) - .with(project: project, current_user: user, merge_request: merge_request, user: user) - .and_call_original - - execute - end - it 'tracks users assigned event' do expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter) .to receive(:track_users_assigned_to_mr).once.with(users: [assignee]) diff --git a/spec/services/merge_requests/mergeability/check_broken_status_service_spec.rb b/spec/services/merge_requests/mergeability/check_broken_status_service_spec.rb index 9e178c121ef..6cc1079c94a 100644 --- a/spec/services/merge_requests/mergeability/check_broken_status_service_spec.rb +++ b/spec/services/merge_requests/mergeability/check_broken_status_service_spec.rb @@ -8,6 +8,8 @@ RSpec.describe MergeRequests::Mergeability::CheckBrokenStatusService do let(:merge_request) { build(:merge_request) } describe '#execute' do + let(:result) { check_broken_status.execute } + before do expect(merge_request).to receive(:broken?).and_return(broken) end @@ -16,7 +18,8 @@ RSpec.describe MergeRequests::Mergeability::CheckBrokenStatusService do let(:broken) { true } it 'returns a check result with status failed' do - expect(check_broken_status.execute.status).to eq Gitlab::MergeRequests::Mergeability::CheckResult::FAILED_STATUS + expect(result.status).to eq Gitlab::MergeRequests::Mergeability::CheckResult::FAILED_STATUS + expect(result.payload[:reason]).to eq(:broken_status) end end @@ -24,7 +27,7 @@ RSpec.describe MergeRequests::Mergeability::CheckBrokenStatusService do let(:broken) { false } it 'returns a check result with status success' do - expect(check_broken_status.execute.status).to eq Gitlab::MergeRequests::Mergeability::CheckResult::SUCCESS_STATUS + expect(result.status).to eq Gitlab::MergeRequests::Mergeability::CheckResult::SUCCESS_STATUS end end end diff --git a/spec/services/merge_requests/mergeability/check_ci_status_service_spec.rb b/spec/services/merge_requests/mergeability/check_ci_status_service_spec.rb index 6fbbecd7c0e..def3cb0ca28 100644 --- a/spec/services/merge_requests/mergeability/check_ci_status_service_spec.rb +++ b/spec/services/merge_requests/mergeability/check_ci_status_service_spec.rb @@ -10,6 +10,8 @@ RSpec.describe MergeRequests::Mergeability::CheckCiStatusService do let(:skip_check) { false } describe '#execute' do + let(:result) { check_ci_status.execute } + before do expect(merge_request).to receive(:mergeable_ci_state?).and_return(mergeable) end @@ -18,7 +20,7 @@ RSpec.describe MergeRequests::Mergeability::CheckCiStatusService do let(:mergeable) { true } it 'returns a check result with status success' do - expect(check_ci_status.execute.status).to eq Gitlab::MergeRequests::Mergeability::CheckResult::SUCCESS_STATUS + expect(result.status).to eq Gitlab::MergeRequests::Mergeability::CheckResult::SUCCESS_STATUS end end @@ -26,7 +28,8 @@ RSpec.describe MergeRequests::Mergeability::CheckCiStatusService do let(:mergeable) { false } it 'returns a check result with status failed' do - expect(check_ci_status.execute.status).to eq Gitlab::MergeRequests::Mergeability::CheckResult::FAILED_STATUS + expect(result.status).to eq Gitlab::MergeRequests::Mergeability::CheckResult::FAILED_STATUS + expect(result.payload[:reason]).to eq :ci_must_pass end end end diff --git a/spec/services/merge_requests/mergeability/check_discussions_status_service_spec.rb b/spec/services/merge_requests/mergeability/check_discussions_status_service_spec.rb index c24d40967c4..9f107ce046a 100644 --- a/spec/services/merge_requests/mergeability/check_discussions_status_service_spec.rb +++ b/spec/services/merge_requests/mergeability/check_discussions_status_service_spec.rb @@ -10,6 +10,8 @@ RSpec.describe MergeRequests::Mergeability::CheckDiscussionsStatusService do let(:skip_check) { false } describe '#execute' do + let(:result) { check_discussions_status.execute } + before do expect(merge_request).to receive(:mergeable_discussions_state?).and_return(mergeable) end @@ -18,7 +20,7 @@ RSpec.describe MergeRequests::Mergeability::CheckDiscussionsStatusService do let(:mergeable) { true } it 'returns a check result with status success' do - expect(check_discussions_status.execute.status).to eq Gitlab::MergeRequests::Mergeability::CheckResult::SUCCESS_STATUS + expect(result.status).to eq Gitlab::MergeRequests::Mergeability::CheckResult::SUCCESS_STATUS end end @@ -26,7 +28,8 @@ RSpec.describe MergeRequests::Mergeability::CheckDiscussionsStatusService do let(:mergeable) { false } it 'returns a check result with status failed' do - expect(check_discussions_status.execute.status).to eq Gitlab::MergeRequests::Mergeability::CheckResult::FAILED_STATUS + expect(result.status).to eq Gitlab::MergeRequests::Mergeability::CheckResult::FAILED_STATUS + expect(result.payload[:reason]).to eq(:discussions_not_resolved) end end end diff --git a/spec/services/merge_requests/mergeability/check_draft_status_service_spec.rb b/spec/services/merge_requests/mergeability/check_draft_status_service_spec.rb index 923cff220ef..e9363e5d676 100644 --- a/spec/services/merge_requests/mergeability/check_draft_status_service_spec.rb +++ b/spec/services/merge_requests/mergeability/check_draft_status_service_spec.rb @@ -8,6 +8,8 @@ RSpec.describe MergeRequests::Mergeability::CheckDraftStatusService do let(:merge_request) { build(:merge_request) } describe '#execute' do + let(:result) { check_draft_status.execute } + before do expect(merge_request).to receive(:draft?).and_return(draft) end @@ -16,7 +18,8 @@ RSpec.describe MergeRequests::Mergeability::CheckDraftStatusService do let(:draft) { true } it 'returns a check result with status failed' do - expect(check_draft_status.execute.status).to eq Gitlab::MergeRequests::Mergeability::CheckResult::FAILED_STATUS + expect(result.status).to eq Gitlab::MergeRequests::Mergeability::CheckResult::FAILED_STATUS + expect(result.payload[:reason]).to eq(:draft_status) end end @@ -24,7 +27,7 @@ RSpec.describe MergeRequests::Mergeability::CheckDraftStatusService do let(:draft) { false } it 'returns a check result with status success' do - expect(check_draft_status.execute.status).to eq Gitlab::MergeRequests::Mergeability::CheckResult::SUCCESS_STATUS + expect(result.status).to eq Gitlab::MergeRequests::Mergeability::CheckResult::SUCCESS_STATUS end end end diff --git a/spec/services/merge_requests/mergeability/check_open_status_service_spec.rb b/spec/services/merge_requests/mergeability/check_open_status_service_spec.rb index b1c9a930317..936524b020a 100644 --- a/spec/services/merge_requests/mergeability/check_open_status_service_spec.rb +++ b/spec/services/merge_requests/mergeability/check_open_status_service_spec.rb @@ -8,6 +8,8 @@ RSpec.describe MergeRequests::Mergeability::CheckOpenStatusService do let(:merge_request) { build(:merge_request) } describe '#execute' do + let(:result) { check_open_status.execute } + before do expect(merge_request).to receive(:open?).and_return(open) end @@ -16,7 +18,7 @@ RSpec.describe MergeRequests::Mergeability::CheckOpenStatusService do let(:open) { true } it 'returns a check result with status success' do - expect(check_open_status.execute.status).to eq Gitlab::MergeRequests::Mergeability::CheckResult::SUCCESS_STATUS + expect(result.status).to eq Gitlab::MergeRequests::Mergeability::CheckResult::SUCCESS_STATUS end end @@ -24,7 +26,8 @@ RSpec.describe MergeRequests::Mergeability::CheckOpenStatusService do let(:open) { false } it 'returns a check result with status failed' do - expect(check_open_status.execute.status).to eq Gitlab::MergeRequests::Mergeability::CheckResult::FAILED_STATUS + expect(result.status).to eq Gitlab::MergeRequests::Mergeability::CheckResult::FAILED_STATUS + expect(result.payload[:reason]).to eq(:not_open) end end end diff --git a/spec/services/merge_requests/mergeability/run_checks_service_spec.rb b/spec/services/merge_requests/mergeability/run_checks_service_spec.rb index 2bb7dc3eef7..afea3e952a1 100644 --- a/spec/services/merge_requests/mergeability/run_checks_service_spec.rb +++ b/spec/services/merge_requests/mergeability/run_checks_service_spec.rb @@ -5,11 +5,11 @@ require 'spec_helper' RSpec.describe MergeRequests::Mergeability::RunChecksService do subject(:run_checks) { described_class.new(merge_request: merge_request, params: {}) } - let_it_be(:merge_request) { create(:merge_request) } - describe '#execute' do subject(:execute) { run_checks.execute } + let_it_be(:merge_request) { create(:merge_request) } + let(:params) { {} } let(:success_result) { Gitlab::MergeRequests::Mergeability::CheckResult.success } @@ -23,7 +23,7 @@ RSpec.describe MergeRequests::Mergeability::RunChecksService do end it 'is still a success' do - expect(execute.all?(&:success?)).to eq(true) + expect(execute.success?).to eq(true) end end @@ -41,13 +41,7 @@ RSpec.describe MergeRequests::Mergeability::RunChecksService do expect(service).not_to receive(:execute) end - # Since we're only marking one check to be skipped, we expect to receive - # `# of checks - 1` success result objects in return - # - check_count = merge_request.mergeability_checks.count - 1 - success_array = (1..check_count).each_with_object([]) { |_, array| array << success_result } - - expect(execute).to match_array(success_array) + expect(execute.success?).to eq(true) end end @@ -75,7 +69,7 @@ RSpec.describe MergeRequests::Mergeability::RunChecksService do expect(service).to receive(:read).with(merge_check: merge_check).and_return(success_result) end - expect(execute).to match_array([success_result]) + expect(execute.success?).to eq(true) end end @@ -86,7 +80,7 @@ RSpec.describe MergeRequests::Mergeability::RunChecksService do expect(service).to receive(:write).with(merge_check: merge_check, result_hash: success_result.to_hash).and_return(true) end - expect(execute).to match_array([success_result]) + expect(execute.success?).to eq(true) end end end @@ -97,7 +91,7 @@ RSpec.describe MergeRequests::Mergeability::RunChecksService do it 'does not call the results store' do expect(Gitlab::MergeRequests::Mergeability::ResultsStore).not_to receive(:new) - expect(execute).to match_array([success_result]) + expect(execute.success?).to eq(true) end end @@ -109,9 +103,81 @@ RSpec.describe MergeRequests::Mergeability::RunChecksService do it 'does not call the results store' do expect(Gitlab::MergeRequests::Mergeability::ResultsStore).not_to receive(:new) - expect(execute).to match_array([success_result]) + expect(execute.success?).to eq(true) end end end end + + describe '#success?' do + subject(:success) { run_checks.success? } + + let_it_be(:merge_request) { create(:merge_request) } + + context 'when the execute method has been executed' do + before do + run_checks.execute + end + + context 'when all the checks succeed' do + it 'returns true' do + expect(success).to eq(true) + end + end + + context 'when one check fails' do + before do + allow(merge_request).to receive(:open?).and_return(false) + run_checks.execute + end + + it 'returns false' do + expect(success).to eq(false) + end + end + end + + context 'when execute has not been exectued' do + it 'raises an error' do + expect { subject } + .to raise_error(/Execute needs to be called before/) + end + end + end + + describe '#failure_reason' do + subject(:failure_reason) { run_checks.failure_reason } + + let_it_be(:merge_request) { create(:merge_request) } + + context 'when the execute method has been executed' do + before do + run_checks.execute + end + + context 'when all the checks succeed' do + it 'returns nil' do + expect(failure_reason).to eq(nil) + end + end + + context 'when one check fails' do + before do + allow(merge_request).to receive(:open?).and_return(false) + run_checks.execute + end + + it 'returns the open reason' do + expect(failure_reason).to eq(:not_open) + end + end + end + + context 'when execute has not been exectued' do + it 'raises an error' do + expect { subject } + .to raise_error(/Execute needs to be called before/) + end + end + end end diff --git a/spec/services/merge_requests/push_options_handler_service_spec.rb b/spec/services/merge_requests/push_options_handler_service_spec.rb index 338057f23d5..391377ad801 100644 --- a/spec/services/merge_requests/push_options_handler_service_spec.rb +++ b/spec/services/merge_requests/push_options_handler_service_spec.rb @@ -179,7 +179,7 @@ RSpec.describe MergeRequests::PushOptionsHandlerService do context 'with an existing branch that has a merge request open' do let(:changes) { existing_branch_changes } - let!(:merge_request) { create(:merge_request, source_project: project, source_branch: source_branch)} + let!(:merge_request) { create(:merge_request, source_project: project, source_branch: source_branch) } it_behaves_like 'a service that does not create a merge request' end @@ -231,7 +231,7 @@ RSpec.describe MergeRequests::PushOptionsHandlerService do context 'with an existing branch that has a merge request open' do let(:changes) { existing_branch_changes } - let!(:merge_request) { create(:merge_request, source_project: project, source_branch: source_branch)} + let!(:merge_request) { create(:merge_request, source_project: project, source_branch: source_branch) } it_behaves_like 'a service that does not create a merge request' it_behaves_like 'a service that can set the merge request to merge when pipeline succeeds' @@ -284,7 +284,7 @@ RSpec.describe MergeRequests::PushOptionsHandlerService do context 'with an existing branch that has a merge request open' do let(:changes) { existing_branch_changes } - let!(:merge_request) { create(:merge_request, source_project: project, source_branch: source_branch)} + let!(:merge_request) { create(:merge_request, source_project: project, source_branch: source_branch) } it_behaves_like 'a service that does not create a merge request' it_behaves_like 'a service that can remove the source branch when it is merged' @@ -337,7 +337,7 @@ RSpec.describe MergeRequests::PushOptionsHandlerService do context 'with an existing branch that has a merge request open' do let(:changes) { existing_branch_changes } - let!(:merge_request) { create(:merge_request, source_project: project, source_branch: source_branch)} + let!(:merge_request) { create(:merge_request, source_project: project, source_branch: source_branch) } it_behaves_like 'a service that does not create a merge request' it_behaves_like 'a service that can set the target of a merge request' @@ -390,7 +390,7 @@ RSpec.describe MergeRequests::PushOptionsHandlerService do context 'with an existing branch that has a merge request open' do let(:changes) { existing_branch_changes } - let!(:merge_request) { create(:merge_request, source_project: project, source_branch: source_branch)} + let!(:merge_request) { create(:merge_request, source_project: project, source_branch: source_branch) } it_behaves_like 'a service that does not create a merge request' it_behaves_like 'a service that can set the title of a merge request' @@ -443,7 +443,7 @@ RSpec.describe MergeRequests::PushOptionsHandlerService do context 'with an existing branch that has a merge request open' do let(:changes) { existing_branch_changes } - let!(:merge_request) { create(:merge_request, source_project: project, source_branch: source_branch)} + let!(:merge_request) { create(:merge_request, source_project: project, source_branch: source_branch) } it_behaves_like 'a service that does not create a merge request' it_behaves_like 'a service that can set the description of a merge request' @@ -503,7 +503,7 @@ RSpec.describe MergeRequests::PushOptionsHandlerService do context 'with an existing branch that has a merge request open' do let(:changes) { existing_branch_changes } - let!(:merge_request) { create(:merge_request, source_project: project, source_branch: source_branch)} + let!(:merge_request) { create(:merge_request, source_project: project, source_branch: source_branch) } it_behaves_like 'a service that does not create a merge request' it_behaves_like 'a service that can set the draft of a merge request' @@ -564,7 +564,7 @@ RSpec.describe MergeRequests::PushOptionsHandlerService do context 'with an existing branch that has a merge request open' do let(:changes) { existing_branch_changes } - let!(:merge_request) { create(:merge_request, source_project: project, source_branch: source_branch)} + let!(:merge_request) { create(:merge_request, source_project: project, source_branch: source_branch) } it_behaves_like 'a service that does not create a merge request' it_behaves_like 'a service that can change labels of a merge request', 2 @@ -617,7 +617,7 @@ RSpec.describe MergeRequests::PushOptionsHandlerService do context 'with an existing branch that has a merge request open' do let(:changes) { existing_branch_changes } - let!(:merge_request) { create(:merge_request, source_project: project, source_branch: source_branch)} + let!(:merge_request) { create(:merge_request, source_project: project, source_branch: source_branch) } it_behaves_like 'a service that does not create a merge request' it_behaves_like 'a service that can change labels of a merge request', 1 @@ -672,7 +672,7 @@ RSpec.describe MergeRequests::PushOptionsHandlerService do context 'with an existing branch that has a merge request open' do let(:changes) { existing_branch_changes } - let!(:merge_request) { create(:merge_request, source_project: project, source_branch: source_branch)} + let!(:merge_request) { create(:merge_request, source_project: project, source_branch: source_branch) } it_behaves_like 'a service that does not create a merge request' it_behaves_like 'a service that can set the milestone of a merge request' @@ -713,7 +713,7 @@ RSpec.describe MergeRequests::PushOptionsHandlerService do shared_examples 'with an existing branch that has a merge request open in foss' do let(:changes) { existing_branch_changes } - let!(:merge_request) { create(:merge_request, source_project: project, source_branch: source_branch)} + let!(:merge_request) { create(:merge_request, source_project: project, source_branch: source_branch) } it_behaves_like 'a service that does not create a merge request' it_behaves_like 'a service that can change assignees of a merge request', 1 diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb index 4b7dd84474a..09d06b8b2ab 100644 --- a/spec/services/merge_requests/refresh_service_spec.rb +++ b/spec/services/merge_requests/refresh_service_spec.rb @@ -185,7 +185,7 @@ RSpec.describe MergeRequests::RefreshService do end context 'when pipeline exists for the source branch' do - let!(:pipeline) { create(:ci_empty_pipeline, ref: @merge_request.source_branch, project: @project, sha: @commits.first.sha)} + let!(:pipeline) { create(:ci_empty_pipeline, ref: @merge_request.source_branch, project: @project, sha: @commits.first.sha) } subject { service.new(project: @project, current_user: @user).execute(@oldrev, @newrev, 'refs/heads/master') } diff --git a/spec/services/merge_requests/remove_attention_requested_service_spec.rb b/spec/services/merge_requests/remove_attention_requested_service_spec.rb deleted file mode 100644 index 576049b9f1b..00000000000 --- a/spec/services/merge_requests/remove_attention_requested_service_spec.rb +++ /dev/null @@ -1,183 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe MergeRequests::RemoveAttentionRequestedService do - let_it_be(:current_user) { create(:user) } - let_it_be(:user) { create(:user) } - let_it_be(:assignee_user) { create(:user) } - let_it_be(:merge_request) { create(:merge_request, reviewers: [user], assignees: [assignee_user]) } - - let(:reviewer) { merge_request.find_reviewer(user) } - let(:assignee) { merge_request.find_assignee(assignee_user) } - let(:project) { merge_request.project } - - let(:service) do - described_class.new( - project: project, - current_user: current_user, - merge_request: merge_request, - user: user - ) - end - - let(:result) { service.execute } - - before do - allow(SystemNoteService).to receive(:remove_attention_request) - - project.add_developer(current_user) - project.add_developer(user) - end - - describe '#execute' do - context 'when current user cannot update merge request' do - let(:service) do - described_class.new( - project: project, - current_user: create(:user), - merge_request: merge_request, - user: user - ) - end - - it 'returns an error' do - expect(result[:status]).to eq :error - end - end - - context 'when user is not a reviewer nor assignee' do - let(:service) do - described_class.new( - project: project, - current_user: current_user, - merge_request: merge_request, - user: create(:user) - ) - end - - it 'returns an error' do - expect(result[:status]).to eq :error - end - end - - context 'when user is a reviewer' do - before do - reviewer.update!(state: :attention_requested) - end - - it 'returns success' do - expect(result[:status]).to eq :success - end - - it 'updates reviewer state' do - service.execute - reviewer.reload - - expect(reviewer.state).to eq 'reviewed' - end - - it 'creates a remove attention request system note' do - expect(SystemNoteService) - .to receive(:remove_attention_request) - .with(merge_request, merge_request.project, current_user, user) - - service.execute - end - - it_behaves_like 'invalidates attention request cache' do - let(:users) { [user] } - end - end - - context 'when user is an assignee' do - let(:service) do - described_class.new( - project: project, - current_user: current_user, - merge_request: merge_request, - user: assignee_user - ) - end - - before do - assignee.update!(state: :attention_requested) - end - - it 'returns success' do - expect(result[:status]).to eq :success - end - - it 'updates assignee state' do - service.execute - assignee.reload - - expect(assignee.state).to eq 'reviewed' - end - - it_behaves_like 'invalidates attention request cache' do - let(:users) { [assignee_user] } - end - - it 'creates a remove attention request system note' do - expect(SystemNoteService) - .to receive(:remove_attention_request) - .with(merge_request, merge_request.project, current_user, assignee_user) - - service.execute - end - end - - context 'when user is an assignee and reviewer at the same time' do - let_it_be(:merge_request) { create(:merge_request, reviewers: [user], assignees: [user]) } - - let(:assignee) { merge_request.find_assignee(user) } - - let(:service) do - described_class.new( - project: project, - current_user: current_user, - merge_request: merge_request, - user: user - ) - end - - before do - reviewer.update!(state: :attention_requested) - assignee.update!(state: :attention_requested) - end - - it 'returns success' do - expect(result[:status]).to eq :success - end - - it 'updates reviewers and assignees state' do - service.execute - reviewer.reload - assignee.reload - - expect(reviewer.state).to eq 'reviewed' - expect(assignee.state).to eq 'reviewed' - end - end - - context 'when state is already not attention_requested' do - before do - reviewer.update!(state: :reviewed) - end - - it 'does not change state' do - service.execute - reviewer.reload - - expect(reviewer.state).to eq 'reviewed' - end - - it 'does not create a remove attention request system note' do - expect(SystemNoteService).not_to receive(:remove_attention_request) - - service.execute - end - end - end -end diff --git a/spec/services/merge_requests/request_attention_service_spec.rb b/spec/services/merge_requests/request_attention_service_spec.rb deleted file mode 100644 index 813a8150625..00000000000 --- a/spec/services/merge_requests/request_attention_service_spec.rb +++ /dev/null @@ -1,220 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe MergeRequests::RequestAttentionService do - let_it_be(:current_user) { create(:user) } - let_it_be(:user) { create(:user) } - let_it_be(:assignee_user) { create(:user) } - let_it_be(:merge_request) { create(:merge_request, reviewers: [user], assignees: [assignee_user]) } - - let(:reviewer) { merge_request.find_reviewer(user) } - let(:assignee) { merge_request.find_assignee(assignee_user) } - let(:project) { merge_request.project } - - let(:service) do - described_class.new( - project: project, - current_user: current_user, - merge_request: merge_request, - user: user - ) - end - - let(:result) { service.execute } - let(:todo_svc) { instance_double('TodoService') } - let(:notification_svc) { instance_double('NotificationService') } - - before do - allow(service).to receive(:todo_service).and_return(todo_svc) - allow(service).to receive(:notification_service).and_return(notification_svc) - allow(todo_svc).to receive(:create_attention_requested_todo) - allow(notification_svc).to receive_message_chain(:async, :attention_requested_of_merge_request) - allow(SystemNoteService).to receive(:request_attention) - - project.add_developer(current_user) - project.add_developer(user) - end - - describe '#execute' do - context 'when current user cannot update merge request' do - let(:service) do - described_class.new( - project: project, - current_user: create(:user), - merge_request: merge_request, - user: user - ) - end - - it 'returns an error' do - expect(result[:status]).to eq :error - end - end - - context 'when user is not a reviewer nor assignee' do - let(:service) do - described_class.new( - project: project, - current_user: current_user, - merge_request: merge_request, - user: create(:user) - ) - end - - it 'returns an error' do - expect(result[:status]).to eq :error - end - end - - context 'when user is a reviewer' do - before do - reviewer.update!(state: :reviewed) - end - - it 'returns success' do - expect(result[:status]).to eq :success - end - - it 'updates reviewers state' do - service.execute - reviewer.reload - - expect(reviewer.state).to eq 'attention_requested' - end - - it 'adds who toggled attention' do - service.execute - reviewer.reload - - expect(reviewer.updated_state_by).to eq current_user - end - - it 'creates a new todo for the reviewer' do - expect(todo_svc).to receive(:create_attention_requested_todo).with(merge_request, current_user, user) - - service.execute - end - - it 'sends email to reviewer' do - expect(notification_svc) - .to receive_message_chain(:async, :attention_requested_of_merge_request) - .with(merge_request, current_user, user) - - service.execute - end - - it 'removes attention requested state' do - expect(MergeRequests::RemoveAttentionRequestedService).to receive(:new) - .with(project: project, current_user: current_user, merge_request: merge_request, user: current_user) - .and_call_original - - service.execute - end - - it_behaves_like 'invalidates attention request cache' do - let(:users) { [user] } - end - end - - context 'when user is an assignee' do - let(:service) do - described_class.new( - project: project, - current_user: current_user, - merge_request: merge_request, - user: assignee_user - ) - end - - before do - assignee.update!(state: :reviewed) - end - - it 'returns success' do - expect(result[:status]).to eq :success - end - - it 'updates assignees state' do - service.execute - assignee.reload - - expect(assignee.state).to eq 'attention_requested' - end - - it 'creates a new todo for the reviewer' do - expect(todo_svc).to receive(:create_attention_requested_todo).with(merge_request, current_user, assignee_user) - - service.execute - end - - it 'creates a request attention system note' do - expect(SystemNoteService) - .to receive(:request_attention) - .with(merge_request, merge_request.project, current_user, assignee_user) - - service.execute - end - - it 'removes attention requested state' do - expect(MergeRequests::RemoveAttentionRequestedService).to receive(:new) - .with(project: project, current_user: current_user, merge_request: merge_request, user: current_user) - .and_call_original - - service.execute - end - - it_behaves_like 'invalidates attention request cache' do - let(:users) { [assignee_user] } - end - end - - context 'when user is an assignee and reviewer at the same time' do - let_it_be(:merge_request) { create(:merge_request, reviewers: [user], assignees: [user]) } - - let(:assignee) { merge_request.find_assignee(user) } - - let(:service) do - described_class.new( - project: project, - current_user: current_user, - merge_request: merge_request, - user: user - ) - end - - before do - reviewer.update!(state: :reviewed) - assignee.update!(state: :reviewed) - end - - it 'updates reviewers and assignees state' do - service.execute - reviewer.reload - assignee.reload - - expect(reviewer.state).to eq 'attention_requested' - expect(assignee.state).to eq 'attention_requested' - end - end - - context 'when state is attention_requested' do - before do - reviewer.update!(state: :attention_requested) - end - - it 'does not change state' do - service.execute - reviewer.reload - - expect(reviewer.state).to eq 'attention_requested' - end - - it 'does not create a new todo for the reviewer' do - expect(todo_svc).not_to receive(:create_attention_requested_todo).with(merge_request, current_user, user) - - service.execute - end - end - end -end diff --git a/spec/services/merge_requests/toggle_attention_requested_service_spec.rb b/spec/services/merge_requests/toggle_attention_requested_service_spec.rb deleted file mode 100644 index 20bc536b21e..00000000000 --- a/spec/services/merge_requests/toggle_attention_requested_service_spec.rb +++ /dev/null @@ -1,188 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe MergeRequests::ToggleAttentionRequestedService do - let(:current_user) { create(:user) } - let(:user) { create(:user) } - let(:assignee_user) { create(:user) } - let(:merge_request) { create(:merge_request, reviewers: [user], assignees: [assignee_user]) } - let(:reviewer) { merge_request.find_reviewer(user) } - let(:assignee) { merge_request.find_assignee(assignee_user) } - let(:project) { merge_request.project } - let(:service) { described_class.new(project: project, current_user: current_user, merge_request: merge_request, user: user) } - let(:result) { service.execute } - let(:todo_service) { spy('todo service') } - let(:notification_service) { spy('notification service') } - - before do - allow(NotificationService).to receive(:new) { notification_service } - allow(service).to receive(:todo_service).and_return(todo_service) - allow(service).to receive(:notification_service).and_return(notification_service) - allow(SystemNoteService).to receive(:request_attention) - allow(SystemNoteService).to receive(:remove_attention_request) - - project.add_developer(current_user) - project.add_developer(user) - end - - describe '#execute' do - context 'invalid permissions' do - let(:service) { described_class.new(project: project, current_user: create(:user), merge_request: merge_request, user: user) } - - it 'returns an error' do - expect(result[:status]).to eq :error - end - end - - context 'reviewer does not exist' do - let(:service) { described_class.new(project: project, current_user: current_user, merge_request: merge_request, user: create(:user)) } - - it 'returns an error' do - expect(result[:status]).to eq :error - end - end - - context 'reviewer exists' do - before do - reviewer.update!(state: :reviewed) - end - - it 'returns success' do - expect(result[:status]).to eq :success - end - - it 'updates reviewers state' do - service.execute - reviewer.reload - - expect(reviewer.state).to eq 'attention_requested' - end - - it 'adds who toggled attention' do - service.execute - reviewer.reload - - expect(reviewer.updated_state_by).to eq current_user - end - - it 'creates a new todo for the reviewer' do - expect(todo_service).to receive(:create_attention_requested_todo).with(merge_request, current_user, user) - - service.execute - end - - it 'sends email to reviewer' do - expect(notification_service).to receive_message_chain(:async, :attention_requested_of_merge_request).with(merge_request, current_user, user) - - service.execute - end - - it 'removes attention requested state' do - expect(MergeRequests::RemoveAttentionRequestedService).to receive(:new) - .with(project: project, current_user: current_user, merge_request: merge_request, user: current_user) - .and_call_original - - service.execute - end - - it 'invalidates cache' do - cache_mock = double - - expect(cache_mock).to receive(:delete).with(['users', user.id, 'attention_requested_open_merge_requests_count']) - - allow(Rails).to receive(:cache).and_return(cache_mock) - - service.execute - end - end - - context 'assignee exists' do - let(:service) { described_class.new(project: project, current_user: current_user, merge_request: merge_request, user: assignee_user) } - - before do - assignee.update!(state: :reviewed) - end - - it 'returns success' do - expect(result[:status]).to eq :success - end - - it 'updates assignees state' do - service.execute - assignee.reload - - expect(assignee.state).to eq 'attention_requested' - end - - it 'creates a new todo for the reviewer' do - expect(todo_service).to receive(:create_attention_requested_todo).with(merge_request, current_user, assignee_user) - - service.execute - end - - it 'creates a request attention system note' do - expect(SystemNoteService).to receive(:request_attention).with(merge_request, merge_request.project, current_user, assignee_user) - - service.execute - end - - it 'removes attention requested state' do - expect(MergeRequests::RemoveAttentionRequestedService).to receive(:new) - .with(project: project, current_user: current_user, merge_request: merge_request, user: current_user) - .and_call_original - - service.execute - end - - it_behaves_like 'invalidates attention request cache' do - let(:users) { [assignee_user] } - end - end - - context 'assignee is the same as reviewer' do - let(:merge_request) { create(:merge_request, reviewers: [user], assignees: [user]) } - let(:service) { described_class.new(project: project, current_user: current_user, merge_request: merge_request, user: user) } - let(:assignee) { merge_request.find_assignee(user) } - - before do - reviewer.update!(state: :reviewed) - assignee.update!(state: :reviewed) - end - - it 'updates reviewers and assignees state' do - service.execute - reviewer.reload - assignee.reload - - expect(reviewer.state).to eq 'attention_requested' - expect(assignee.state).to eq 'attention_requested' - end - end - - context 'state is attention_requested' do - before do - reviewer.update!(state: :attention_requested) - end - - it 'toggles state to reviewed' do - service.execute - reviewer.reload - - expect(reviewer.state).to eq "reviewed" - end - - it 'does not create a new todo for the reviewer' do - expect(todo_service).not_to receive(:create_attention_requested_todo).with(merge_request, current_user, assignee_user) - - service.execute - end - - it 'creates a remove attention request system note' do - expect(SystemNoteService).to receive(:remove_attention_request).with(merge_request, merge_request.project, current_user, user) - - service.execute - end - end - end -end diff --git a/spec/services/merge_requests/update_reviewers_service_spec.rb b/spec/services/merge_requests/update_reviewers_service_spec.rb new file mode 100644 index 00000000000..8920141adbb --- /dev/null +++ b/spec/services/merge_requests/update_reviewers_service_spec.rb @@ -0,0 +1,162 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe MergeRequests::UpdateReviewersService do + include AfterNextHelpers + + let_it_be(:group) { create(:group, :public) } + let_it_be(:project) { create(:project, :private, :repository, group: group) } + let_it_be(:user) { create(:user) } + let_it_be(:user2) { create(:user) } + let_it_be(:user3) { create(:user) } + + let_it_be_with_reload(:merge_request) do + create(:merge_request, :simple, :unique_branches, + title: 'Old title', + description: "FYI #{user2.to_reference}", + reviewer_ids: [user3.id], + source_project: project, + target_project: project, + author: create(:user)) + end + + before do + project.add_maintainer(user) + project.add_developer(user2) + project.add_developer(user3) + merge_request.errors.clear + end + + let(:service) { described_class.new(project: project, current_user: user, params: opts) } + let(:opts) { { reviewer_ids: [user2.id] } } + + describe 'execute' do + def set_reviewers + service.execute(merge_request) + end + + def find_note(starting_with) + merge_request.notes.find do |note| + note && note.note.start_with?(starting_with) + end + end + + shared_examples 'removing all reviewers' do + it 'removes all reviewers' do + expect(set_reviewers).to have_attributes(reviewers: be_empty, errors: be_none) + end + end + + context 'when the parameters are valid' do + context 'when using sentinel values' do + let(:opts) { { reviewer_ids: [0] } } + + it_behaves_like 'removing all reviewers' + end + + context 'when the reviewer_ids parameter is the empty list' do + let(:opts) { { reviewer_ids: [] } } + + it_behaves_like 'removing all reviewers' + end + + it 'updates the MR' do + expect { set_reviewers } + .to change { merge_request.reload.reviewers }.from([user3]).to([user2]) + .and change(merge_request, :updated_at) + .and change(merge_request, :updated_by).to(user) + end + + it 'creates system note about merge_request review request' do + set_reviewers + + note = find_note('requested review from') + + expect(note).not_to be_nil + expect(note.note).to include "requested review from #{user2.to_reference}" + end + + it 'creates a pending todo for new review request' do + set_reviewers + + attributes = { + project: project, + author: user, + user: user2, + target_id: merge_request.id, + target_type: merge_request.class.name, + action: Todo::REVIEW_REQUESTED, + state: :pending + } + + expect(Todo.where(attributes).count).to eq 1 + end + + it 'sends email reviewer change notifications to old and new reviewers', :sidekiq_inline, :mailer do + perform_enqueued_jobs do + set_reviewers + end + + should_email(user2) + should_email(user3) + end + + it 'updates open merge request counter for reviewers', :use_clean_rails_memory_store_caching do + # Cache them to ensure the cache gets invalidated on update + expect(user2.review_requested_open_merge_requests_count).to eq(0) + expect(user3.review_requested_open_merge_requests_count).to eq(1) + + set_reviewers + + expect(user2.review_requested_open_merge_requests_count).to eq(1) + expect(user3.review_requested_open_merge_requests_count).to eq(0) + end + + it 'updates the tracking' do + expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter) + .to receive(:track_users_review_requested) + .with(users: [user2]) + + set_reviewers + end + + it 'tracks reviewers changed event' do + expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter) + .to receive(:track_reviewers_changed_action).once.with(user: user) + + set_reviewers + end + + it 'calls MergeRequest::ResolveTodosService#async_execute' do + expect_next_instance_of(MergeRequests::ResolveTodosService, merge_request, user) do |service| + expect(service).to receive(:async_execute) + end + + set_reviewers + end + + it 'executes hooks with update action' do + expect(service).to receive(:execute_hooks) + .with( + merge_request, + 'update', + old_associations: { + reviewers: [user3] + } + ) + + set_reviewers + end + + it 'does not update the reviewers if they do not have access' do + opts[:reviewer_ids] = [create(:user).id] + + expect(set_reviewers).to have_attributes( + reviewers: [user3], + errors: be_any + ) + end + end + end +end diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb index 212f75d853f..b7fb48718d8 100644 --- a/spec/services/merge_requests/update_service_spec.rb +++ b/spec/services/merge_requests/update_service_spec.rb @@ -91,7 +91,7 @@ RSpec.describe MergeRequests::UpdateService, :mailer do context 'usage counters' do let(:merge_request2) { create(:merge_request) } - let(:draft_merge_request) { create(:merge_request, :draft_merge_request)} + let(:draft_merge_request) { create(:merge_request, :draft_merge_request) } it 'update as expected' do expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter) @@ -980,8 +980,8 @@ RSpec.describe MergeRequests::UpdateService, :mailer do end it 'creates system note about task status change' do - note1 = find_note('marked the task **Task 1** as completed') - note2 = find_note('marked the task **Task 2** as completed') + note1 = find_note('marked the checklist item **Task 1** as completed') + note2 = find_note('marked the checklist item **Task 2** as completed') expect(note1).not_to be_nil expect(note2).not_to be_nil @@ -998,8 +998,8 @@ RSpec.describe MergeRequests::UpdateService, :mailer do end it 'creates system note about task status change' do - note1 = find_note('marked the task **Task 1** as incomplete') - note2 = find_note('marked the task **Task 2** as incomplete') + note1 = find_note('marked the checklist item **Task 1** as incomplete') + note2 = find_note('marked the checklist item **Task 2** as incomplete') expect(note1).not_to be_nil expect(note2).not_to be_nil diff --git a/spec/services/milestones/transfer_service_spec.rb b/spec/services/milestones/transfer_service_spec.rb index afbc9c7dca2..b15d90d685c 100644 --- a/spec/services/milestones/transfer_service_spec.rb +++ b/spec/services/milestones/transfer_service_spec.rb @@ -11,9 +11,9 @@ RSpec.describe Milestones::TransferService do let(:new_group) { create(:group) } let(:old_group) { create(:group) } let(:project) { create(:project, namespace: old_group) } - let(:group_milestone) { create(:milestone, group: old_group)} - let(:group_milestone2) { create(:milestone, group: old_group)} - let(:project_milestone) { create(:milestone, project: project)} + let(:group_milestone) { create(:milestone, group: old_group) } + let(:group_milestone2) { create(:milestone, group: old_group) } + let(:project_milestone) { create(:milestone, project: project) } let!(:issue_with_group_milestone) { create(:issue, project: project, milestone: group_milestone) } let!(:issue_with_project_milestone) { create(:issue, project: project, milestone: project_milestone) } let!(:mr_with_group_milestone) { create(:merge_request, source_project: project, source_branch: 'branch-1', milestone: group_milestone) } @@ -43,7 +43,7 @@ RSpec.describe Milestones::TransferService do context 'when milestone is from an ancestor group' do let(:old_group_ancestor) { create(:group) } let(:old_group) { create(:group, parent: old_group_ancestor) } - let(:group_milestone) { create(:milestone, group: old_group_ancestor)} + let(:group_milestone) { create(:milestone, group: old_group_ancestor) } it 'recreates the missing group milestones at project level' do expect { service.execute }.to change(project.milestones, :count).by(1) diff --git a/spec/services/notes/build_service_spec.rb b/spec/services/notes/build_service_spec.rb index 0e2bbcc8c66..c25895d2efa 100644 --- a/spec/services/notes/build_service_spec.rb +++ b/spec/services/notes/build_service_spec.rb @@ -170,7 +170,7 @@ RSpec.describe Notes::BuildService do end context 'when creating a new confidential comment' do - let(:params) { { confidential: true, noteable: issue } } + let(:params) { { internal: true, noteable: issue } } shared_examples 'user allowed to set comment as confidential' do it { expect(new_note.confidential).to be_truthy } @@ -219,6 +219,14 @@ RSpec.describe Notes::BuildService do it_behaves_like 'user not allowed to set comment as confidential' end + + context 'when using the deprecated `confidential` parameter' do + let(:params) { { internal: true, noteable: issue } } + + shared_examples 'user allowed to set comment as confidential' do + it { expect(new_note.confidential).to be_truthy } + end + end end context 'when replying to a confidential comment' do diff --git a/spec/services/notes/copy_service_spec.rb b/spec/services/notes/copy_service_spec.rb index fd8802e6640..f146a49e929 100644 --- a/spec/services/notes/copy_service_spec.rb +++ b/spec/services/notes/copy_service_spec.rb @@ -138,7 +138,7 @@ RSpec.describe Notes::CopyService do context 'notes with upload' do let(:uploader) { build(:file_uploader, project: from_noteable.project) } - let(:text) { "Simple text with image: #{uploader.markdown_link} "} + let(:text) { "Simple text with image: #{uploader.markdown_link} " } let!(:note) { create(:note, noteable: from_noteable, note: text, project: from_noteable.project) } it 'rewrites note content correctly' do @@ -146,8 +146,8 @@ RSpec.describe Notes::CopyService do new_note = to_noteable.notes.first aggregate_failures do - expect(note.note).to match(/Simple text with image: #{FileUploader::MARKDOWN_PATTERN}/) - expect(new_note.note).to match(/Simple text with image: #{FileUploader::MARKDOWN_PATTERN}/) + expect(note.note).to match(/Simple text with image: #{FileUploader::MARKDOWN_PATTERN}/o) + expect(new_note.note).to match(/Simple text with image: #{FileUploader::MARKDOWN_PATTERN}/o) expect(note.note).not_to eq(new_note.note) expect(note.note_html).not_to eq(new_note.note_html) end diff --git a/spec/services/notes/create_service_spec.rb b/spec/services/notes/create_service_spec.rb index 53b75a3c991..37318d76586 100644 --- a/spec/services/notes/create_service_spec.rb +++ b/spec/services/notes/create_service_spec.rb @@ -7,37 +7,74 @@ RSpec.describe Notes::CreateService do let_it_be(:issue) { create(:issue, project: project) } let_it_be(:user) { create(:user) } - let(:opts) do - { note: 'Awesome comment', noteable_type: 'Issue', noteable_id: issue.id, confidential: true } - end + let(:base_opts) { { note: 'Awesome comment', noteable_type: 'Issue', noteable_id: issue.id } } + let(:opts) { base_opts.merge(confidential: true) } describe '#execute' do + subject(:note) { described_class.new(project, user, opts).execute } + before do project.add_maintainer(user) end context "valid params" do it 'returns a valid note' do - note = described_class.new(project, user, opts).execute - expect(note).to be_valid end it 'returns a persisted note' do - note = described_class.new(project, user, opts).execute - expect(note).to be_persisted end - it 'note has valid content' do - note = described_class.new(project, user, opts).execute + context 'with internal parameter' do + context 'when confidential' do + let(:opts) { base_opts.merge(internal: true) } + + it 'returns a confidential note' do + expect(note).to be_confidential + end + end + + context 'when not confidential' do + let(:opts) { base_opts.merge(internal: false) } + + it 'returns a confidential note' do + expect(note).not_to be_confidential + end + end + end + + context 'with confidential parameter' do + context 'when confidential' do + let(:opts) { base_opts.merge(confidential: true) } + + it 'returns a confidential note' do + expect(note).to be_confidential + end + end + + context 'when not confidential' do + let(:opts) { base_opts.merge(confidential: false) } + it 'returns a confidential note' do + expect(note).not_to be_confidential + end + end + end + + context 'with confidential and internal parameter set' do + let(:opts) { base_opts.merge(internal: true, confidential: false) } + + it 'prefers the internal parameter' do + expect(note).to be_confidential + end + end + + it 'note has valid content' do expect(note.note).to eq(opts[:note]) end it 'note belongs to the correct project' do - note = described_class.new(project, user, opts).execute - expect(note.project).to eq(project) end @@ -60,8 +97,6 @@ RSpec.describe Notes::CreateService do end context 'issue is an incident' do - subject { described_class.new(project, user, opts).execute } - let(:issue) { create(:incident, project: project) } it_behaves_like 'an incident management tracked event', :incident_management_incident_comment do @@ -69,20 +104,31 @@ RSpec.describe Notes::CreateService do end end - it 'tracks issue comment usage data', :clean_gitlab_redis_shared_state do - event = Gitlab::UsageDataCounters::IssueActivityUniqueCounter::ISSUE_COMMENT_ADDED - counter = Gitlab::UsageDataCounters::HLLRedisCounter + describe 'event tracking', :snowplow do + let(:event) { Gitlab::UsageDataCounters::IssueActivityUniqueCounter::ISSUE_COMMENT_ADDED } + let(:execute_create_service) { described_class.new(project, user, opts).execute } - expect(Gitlab::UsageDataCounters::IssueActivityUniqueCounter).to receive(:track_issue_comment_added_action).with(author: user).and_call_original - expect do - described_class.new(project, user, opts).execute - end.to change { counter.unique_events(event_names: event, start_date: 1.day.ago, end_date: 1.day.from_now) }.by(1) - end + it 'tracks issue comment usage data', :clean_gitlab_redis_shared_state do + counter = Gitlab::UsageDataCounters::HLLRedisCounter + + expect(Gitlab::UsageDataCounters::IssueActivityUniqueCounter).to receive(:track_issue_comment_added_action) + .with(author: user, project: project) + .and_call_original + expect do + execute_create_service + end.to change { counter.unique_events(event_names: event, start_date: 1.day.ago, end_date: 1.day.from_now) }.by(1) + end - it 'does not track merge request usage data' do - expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter).not_to receive(:track_create_comment_action) + it 'does not track merge request usage data' do + expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter).not_to receive(:track_create_comment_action) - described_class.new(project, user, opts).execute + execute_create_service + end + + it_behaves_like 'issue_edit snowplow tracking' do + let(:property) { Gitlab::UsageDataCounters::IssueActivityUniqueCounter::ISSUE_COMMENT_ADDED } + subject(:service_action) { execute_create_service } + end end context 'in a merge request' do diff --git a/spec/services/notes/destroy_service_spec.rb b/spec/services/notes/destroy_service_spec.rb index 55acdabef82..be95a4bb181 100644 --- a/spec/services/notes/destroy_service_spec.rb +++ b/spec/services/notes/destroy_service_spec.rb @@ -25,15 +25,25 @@ RSpec.describe Notes::DestroyService do .to change { user.todos_pending_count }.from(1).to(0) end - it 'tracks issue comment removal usage data', :clean_gitlab_redis_shared_state do - note = create(:note, project: project, noteable: issue) - event = Gitlab::UsageDataCounters::IssueActivityUniqueCounter::ISSUE_COMMENT_REMOVED - counter = Gitlab::UsageDataCounters::HLLRedisCounter + describe 'comment removed event tracking', :snowplow do + let(:property) { Gitlab::UsageDataCounters::IssueActivityUniqueCounter::ISSUE_COMMENT_REMOVED } + let(:note) { create(:note, project: project, noteable: issue) } + let(:service_action) { described_class.new(project, user).execute(note) } + + it 'tracks issue comment removal usage data', :clean_gitlab_redis_shared_state do + counter = Gitlab::UsageDataCounters::HLLRedisCounter + + expect(Gitlab::UsageDataCounters::IssueActivityUniqueCounter).to receive(:track_issue_comment_removed_action) + .with(author: user, project: project) + .and_call_original + expect do + service_action + end.to change { counter.unique_events(event_names: property, start_date: 1.day.ago, end_date: 1.day.from_now) }.by(1) + end - expect(Gitlab::UsageDataCounters::IssueActivityUniqueCounter).to receive(:track_issue_comment_removed_action).with(author: user).and_call_original - expect do - described_class.new(project, user).execute(note) - end.to change { counter.unique_events(event_names: event, start_date: 1.day.ago, end_date: 1.day.from_now) }.by(1) + it_behaves_like 'issue_edit snowplow tracking' do + subject(:execute_service_action) { service_action } + end end it 'tracks merge request usage data' do diff --git a/spec/services/notes/update_service_spec.rb b/spec/services/notes/update_service_spec.rb index ae7bea30944..989ca7b8df1 100644 --- a/spec/services/notes/update_service_spec.rb +++ b/spec/services/notes/update_service_spec.rb @@ -47,21 +47,31 @@ RSpec.describe Notes::UpdateService do end end - it 'does not track usage data when params is blank' do - expect(Gitlab::UsageDataCounters::IssueActivityUniqueCounter).not_to receive(:track_issue_comment_edited_action) - expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter).not_to receive(:track_edit_comment_action) + describe 'event tracking', :snowplow do + let(:event) { Gitlab::UsageDataCounters::IssueActivityUniqueCounter::ISSUE_COMMENT_EDITED } - update_note({}) - end + it 'does not track usage data when params is blank' do + expect(Gitlab::UsageDataCounters::IssueActivityUniqueCounter).not_to receive(:track_issue_comment_edited_action) + expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter).not_to receive(:track_edit_comment_action) - it 'tracks issue usage data', :clean_gitlab_redis_shared_state do - event = Gitlab::UsageDataCounters::IssueActivityUniqueCounter::ISSUE_COMMENT_EDITED - counter = Gitlab::UsageDataCounters::HLLRedisCounter + update_note({}) + end - expect(Gitlab::UsageDataCounters::IssueActivityUniqueCounter).to receive(:track_issue_comment_edited_action).with(author: user).and_call_original - expect do - update_note(note: 'new text') - end.to change { counter.unique_events(event_names: event, start_date: 1.day.ago, end_date: 1.day.from_now) }.by(1) + it 'tracks issue usage data', :clean_gitlab_redis_shared_state do + counter = Gitlab::UsageDataCounters::HLLRedisCounter + + expect(Gitlab::UsageDataCounters::IssueActivityUniqueCounter).to receive(:track_issue_comment_edited_action) + .with(author: user, project: project) + .and_call_original + expect do + update_note(note: 'new text') + end.to change { counter.unique_events(event_names: event, start_date: 1.day.ago, end_date: 1.day.from_now) }.by(1) + end + + it_behaves_like 'issue_edit snowplow tracking' do + let(:property) { Gitlab::UsageDataCounters::IssueActivityUniqueCounter::ISSUE_COMMENT_EDITED } + subject(:service_action) { update_note(note: 'new text') } + end end context 'when note text was changed' do diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index 98fe8a40c61..935dcef1011 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -2006,19 +2006,19 @@ RSpec.describe NotificationService, :mailer do context 'participating' do it_behaves_like 'participating by assignee notification' do - let(:participant) { create(:user, username: 'user-participant')} + let(:participant) { create(:user, username: 'user-participant') } let(:issuable) { merge_request } let(:notification_trigger) { notification.new_merge_request(merge_request, @u_disabled) } end it_behaves_like 'participating by note notification' do - let(:participant) { create(:user, username: 'user-participant')} + let(:participant) { create(:user, username: 'user-participant') } let(:issuable) { merge_request } let(:notification_trigger) { notification.new_merge_request(merge_request, @u_disabled) } end context 'by author' do - let(:participant) { create(:user, username: 'user-participant')} + let(:participant) { create(:user, username: 'user-participant') } before do merge_request.author = participant @@ -2657,45 +2657,6 @@ RSpec.describe NotificationService, :mailer do let(:notification_trigger) { notification.review_requested_of_merge_request(merge_request, current_user, reviewer) } end end - - describe '#attention_requested_of_merge_request' do - let_it_be(:current_user) { create(:user) } - let_it_be(:reviewer) { create(:user) } - let_it_be(:merge_request) { create(:merge_request, source_project: project, reviewers: [reviewer]) } - - it 'sends email to reviewer', :aggregate_failures do - notification.attention_requested_of_merge_request(merge_request, current_user, reviewer) - - merge_request.reviewers.each { |reviewer| should_email(reviewer) } - should_not_email(merge_request.author) - should_not_email(@u_watcher) - should_not_email(@u_participant_mentioned) - should_not_email(@subscriber) - should_not_email(@watcher_and_subscriber) - should_not_email(@u_guest_watcher) - should_not_email(@u_guest_custom) - should_not_email(@u_custom_global) - should_not_email(@unsubscriber) - should_not_email(@u_participating) - should_not_email(@u_disabled) - should_not_email(@u_lazy_participant) - end - - it 'adds "attention requested" reason' do - notification.attention_requested_of_merge_request(merge_request, current_user, [reviewer]) - - merge_request.reviewers.each do |reviewer| - email = find_email_for(reviewer) - - expect(email).to have_header('X-GitLab-NotificationReason', NotificationReason::ATTENTION_REQUESTED) - end - end - - it_behaves_like 'project emails are disabled' do - let(:notification_target) { merge_request } - let(:notification_trigger) { notification.attention_requested_of_merge_request(merge_request, current_user, reviewer) } - end - end end describe 'Projects', :deliver_mails_inline do diff --git a/spec/services/packages/composer/create_package_service_spec.rb b/spec/services/packages/composer/create_package_service_spec.rb index b04a6c8382f..26429a7b5d9 100644 --- a/spec/services/packages/composer/create_package_service_spec.rb +++ b/spec/services/packages/composer/create_package_service_spec.rb @@ -88,7 +88,7 @@ RSpec.describe Packages::Composer::CreatePackageService do end context 'belonging to another project' do - let(:other_project) { create(:project)} + let(:other_project) { create(:project) } let!(:other_package) { create(:composer_package, name: package_name, version: 'dev-master', project: other_project) } it 'fails with an error' do diff --git a/spec/services/packages/create_dependency_service_spec.rb b/spec/services/packages/create_dependency_service_spec.rb index 55414ea68fe..f95e21cd045 100644 --- a/spec/services/packages/create_dependency_service_spec.rb +++ b/spec/services/packages/create_dependency_service_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' RSpec.describe Packages::CreateDependencyService do describe '#execute' do - let_it_be(:namespace) {create(:namespace)} + let_it_be(:namespace) { create(:namespace) } let_it_be(:version) { '1.0.1' } let_it_be(:package_name) { "@#{namespace.path}/my-app" } diff --git a/spec/services/packages/debian/extract_deb_metadata_service_spec.rb b/spec/services/packages/debian/extract_deb_metadata_service_spec.rb index ee3f3d179dc..66a9ca5f9e0 100644 --- a/spec/services/packages/debian/extract_deb_metadata_service_spec.rb +++ b/spec/services/packages/debian/extract_deb_metadata_service_spec.rb @@ -31,7 +31,7 @@ RSpec.describe Packages::Debian::ExtractDebMetadataService do let(:file_name) { 'README.md' } it 'raise error' do - expect {subject.execute}.to raise_error(described_class::CommandFailedError, /is not a Debian format archive/i) + expect { subject.execute }.to raise_error(described_class::CommandFailedError, /is not a Debian format archive/i) end end end diff --git a/spec/services/packages/debian/extract_metadata_service_spec.rb b/spec/services/packages/debian/extract_metadata_service_spec.rb index e3911dbbfe0..02c81ad1644 100644 --- a/spec/services/packages/debian/extract_metadata_service_spec.rb +++ b/spec/services/packages/debian/extract_metadata_service_spec.rb @@ -11,15 +11,10 @@ RSpec.describe Packages::Debian::ExtractMetadataService do end RSpec.shared_examples 'Test Debian ExtractMetadata Service' do |expected_file_type, expected_architecture, expected_fields| - it "returns file_type #{expected_file_type.inspect}" do + it "returns file_type #{expected_file_type.inspect}, architecture #{expected_architecture.inspect} and fields #{expected_fields.nil? ? '' : 'including '}#{expected_fields.inspect}", :aggregate_failures do expect(subject[:file_type]).to eq(expected_file_type) - end - - it "returns architecture #{expected_architecture.inspect}" do expect(subject[:architecture]).to eq(expected_architecture) - end - it "returns fields #{expected_fields.nil? ? '' : 'including '}#{expected_fields.inspect}" do if expected_fields.nil? expect(subject[:fields]).to be_nil else diff --git a/spec/services/packages/debian/parse_debian822_service_spec.rb b/spec/services/packages/debian/parse_debian822_service_spec.rb index cad4e81f350..ff146fda250 100644 --- a/spec/services/packages/debian/parse_debian822_service_spec.rb +++ b/spec/services/packages/debian/parse_debian822_service_spec.rb @@ -102,7 +102,7 @@ RSpec.describe Packages::Debian::ParseDebian822Service do let(:input) { ' continuation' } it 'raise error' do - expect {subject.execute}.to raise_error(described_class::InvalidDebian822Error, 'Parse error. Unexpected continuation line') + expect { subject.execute }.to raise_error(described_class::InvalidDebian822Error, 'Parse error. Unexpected continuation line') end end @@ -116,7 +116,7 @@ RSpec.describe Packages::Debian::ParseDebian822Service do end it 'raise error' do - expect {subject.execute}.to raise_error(described_class::InvalidDebian822Error, "Duplicate field 'Source' in section 'Package: libsample0'") + expect { subject.execute }.to raise_error(described_class::InvalidDebian822Error, "Duplicate field 'Source' in section 'Package: libsample0'") end end @@ -128,7 +128,7 @@ RSpec.describe Packages::Debian::ParseDebian822Service do end it 'raise error' do - expect {subject.execute}.to raise_error(described_class::InvalidDebian822Error, 'Parse error on line Hello') + expect { subject.execute }.to raise_error(described_class::InvalidDebian822Error, 'Parse error on line Hello') end end @@ -142,7 +142,7 @@ RSpec.describe Packages::Debian::ParseDebian822Service do end it 'raise error' do - expect {subject.execute}.to raise_error(described_class::InvalidDebian822Error, "Duplicate section 'Package: libsample0'") + expect { subject.execute }.to raise_error(described_class::InvalidDebian822Error, "Duplicate section 'Package: libsample0'") end end end diff --git a/spec/services/packages/debian/sign_distribution_service_spec.rb b/spec/services/packages/debian/sign_distribution_service_spec.rb index 2aec0e50636..fc070b6e45e 100644 --- a/spec/services/packages/debian/sign_distribution_service_spec.rb +++ b/spec/services/packages/debian/sign_distribution_service_spec.rb @@ -30,7 +30,7 @@ RSpec.describe Packages::Debian::SignDistributionService do end context 'with an existing key' do - let!(:key) { create("debian_#{container_type}_distribution_key", distribution: distribution)} + let!(:key) { create("debian_#{container_type}_distribution_key", distribution: distribution) } it 'returns the content signed', :aggregate_failures do expect(Packages::Debian::GenerateDistributionKeyService).not_to receive(:new) diff --git a/spec/services/packages/helm/process_file_service_spec.rb b/spec/services/packages/helm/process_file_service_spec.rb index d22c1de2335..1be0153a4a5 100644 --- a/spec/services/packages/helm/process_file_service_spec.rb +++ b/spec/services/packages/helm/process_file_service_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' RSpec.describe Packages::Helm::ProcessFileService do - let(:package) { create(:helm_package, without_package_files: true, status: 'processing')} + let(:package) { create(:helm_package, without_package_files: true, status: 'processing') } let!(:package_file) { create(:helm_package_file, without_loaded_metadatum: true, package: package) } let(:channel) { 'stable' } let(:service) { described_class.new(channel, package_file) } diff --git a/spec/services/packages/npm/create_package_service_spec.rb b/spec/services/packages/npm/create_package_service_spec.rb index 5b41055397b..a3e59913918 100644 --- a/spec/services/packages/npm/create_package_service_spec.rb +++ b/spec/services/packages/npm/create_package_service_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' RSpec.describe Packages::Npm::CreatePackageService do - let(:namespace) {create(:namespace)} + let(:namespace) { create(:namespace) } let(:project) { create(:project, namespace: namespace) } let(:user) { create(:user) } let(:version) { '1.0.1' } @@ -129,7 +129,7 @@ RSpec.describe Packages::Npm::CreatePackageService do end describe 'max file size validation' do - let(:max_file_size) { 5.bytes} + let(:max_file_size) { 5.bytes } shared_examples_for 'max file size validation failure' do it 'returns a 400 error', :aggregate_failures do @@ -160,7 +160,7 @@ RSpec.describe Packages::Npm::CreatePackageService do end context "when encoded package data is padded with '='" do - let(:max_file_size) { 4.bytes} + let(:max_file_size) { 4.bytes } # 'Hello' (size = 5 bytes) => 'SGVsbG8=' let(:encoded_package_data) { 'SGVsbG8=' } @@ -168,7 +168,7 @@ RSpec.describe Packages::Npm::CreatePackageService do end context "when encoded package data is padded with '=='" do - let(:max_file_size) { 3.bytes} + let(:max_file_size) { 3.bytes } # 'Hell' (size = 4 bytes) => 'SGVsbA==' let(:encoded_package_data) { 'SGVsbA==' } diff --git a/spec/services/packages/npm/create_tag_service_spec.rb b/spec/services/packages/npm/create_tag_service_spec.rb index e7a784068fa..a4b07bf97cc 100644 --- a/spec/services/packages/npm/create_tag_service_spec.rb +++ b/spec/services/packages/npm/create_tag_service_spec.rb @@ -11,6 +11,7 @@ RSpec.describe Packages::Npm::CreateTagService do shared_examples 'it creates the tag' do it { expect { subject }.to change { Packages::Tag.count }.by(1) } it { expect(subject.name).to eq(tag_name) } + it 'adds tag to the package' do tag = subject expect(package.reload.tags).to match_array([tag]) diff --git a/spec/services/packages/rubygems/dependency_resolver_service_spec.rb b/spec/services/packages/rubygems/dependency_resolver_service_spec.rb index f23ed0e5fbc..bb84e0cd361 100644 --- a/spec/services/packages/rubygems/dependency_resolver_service_spec.rb +++ b/spec/services/packages/rubygems/dependency_resolver_service_spec.rb @@ -47,9 +47,9 @@ RSpec.describe Packages::Rubygems::DependencyResolverService do end context 'package with dependencies' do - let(:dependency_link) { create(:packages_dependency_link, :rubygems, package: package)} - let(:dependency_link2) { create(:packages_dependency_link, :rubygems, package: package)} - let(:dependency_link3) { create(:packages_dependency_link, :rubygems, package: package)} + let(:dependency_link) { create(:packages_dependency_link, :rubygems, package: package) } + let(:dependency_link2) { create(:packages_dependency_link, :rubygems, package: package) } + let(:dependency_link3) { create(:packages_dependency_link, :rubygems, package: package) } it 'returns a set of dependencies' do expected_result = [{ @@ -68,11 +68,11 @@ RSpec.describe Packages::Rubygems::DependencyResolverService do end context 'package with multiple versions' do - let(:dependency_link) { create(:packages_dependency_link, :rubygems, package: package)} - let(:dependency_link2) { create(:packages_dependency_link, :rubygems, package: package)} - let(:dependency_link3) { create(:packages_dependency_link, :rubygems, package: package)} + let(:dependency_link) { create(:packages_dependency_link, :rubygems, package: package) } + let(:dependency_link2) { create(:packages_dependency_link, :rubygems, package: package) } + let(:dependency_link3) { create(:packages_dependency_link, :rubygems, package: package) } let(:package2) { create(:package, project: project, name: package.name, version: '9.9.9') } - let(:dependency_link4) { create(:packages_dependency_link, :rubygems, package: package2)} + let(:dependency_link4) { create(:packages_dependency_link, :rubygems, package: package2) } it 'returns a set of dependencies' do expected_result = [{ diff --git a/spec/services/pages/delete_service_spec.rb b/spec/services/pages/delete_service_spec.rb index 29d9a47c72e..8b9e72ac9b1 100644 --- a/spec/services/pages/delete_service_spec.rb +++ b/spec/services/pages/delete_service_spec.rb @@ -5,8 +5,8 @@ require 'spec_helper' RSpec.describe Pages::DeleteService do let_it_be(:admin) { create(:admin) } - let(:project) { create(:project, path: "my.project")} - let(:service) { described_class.new(project, admin)} + let(:project) { create(:project, path: "my.project") } + let(:service) { described_class.new(project, admin) } before do project.mark_pages_as_deployed diff --git a/spec/services/pages_domains/obtain_lets_encrypt_certificate_service_spec.rb b/spec/services/pages_domains/obtain_lets_encrypt_certificate_service_spec.rb index 79654c9b190..ecb445fa441 100644 --- a/spec/services/pages_domains/obtain_lets_encrypt_certificate_service_spec.rb +++ b/spec/services/pages_domains/obtain_lets_encrypt_certificate_service_spec.rb @@ -135,7 +135,7 @@ RSpec.describe PagesDomains::ObtainLetsEncryptCertificateService do cert.add_extension ef.create_extension("authorityKeyIdentifier", "keyid:always,issuer:always") - cert.sign key, OpenSSL::Digest.new('SHA1') + cert.sign key, OpenSSL::Digest.new('SHA256') cert.to_pem end diff --git a/spec/services/personal_access_tokens/revoke_service_spec.rb b/spec/services/personal_access_tokens/revoke_service_spec.rb index a25484e218e..f16b6f00a0a 100644 --- a/spec/services/personal_access_tokens/revoke_service_spec.rb +++ b/spec/services/personal_access_tokens/revoke_service_spec.rb @@ -6,6 +6,7 @@ RSpec.describe PersonalAccessTokens::RevokeService do shared_examples_for 'a successfully revoked token' do it { expect(subject.success?).to be true } it { expect(service.token.revoked?).to be true } + it 'logs the event' do expect(Gitlab::AppLogger).to receive(:info).with(/PAT REVOCATION: revoked_by: '#{current_user.username}', revoked_for: '#{token.user.username}', token_id: '\d+'/) diff --git a/spec/services/projects/after_rename_service_spec.rb b/spec/services/projects/after_rename_service_spec.rb index 9dc15131bc5..edf4bbe0f7f 100644 --- a/spec/services/projects/after_rename_service_spec.rb +++ b/spec/services/projects/after_rename_service_spec.rb @@ -3,7 +3,6 @@ require 'spec_helper' RSpec.describe Projects::AfterRenameService do - let(:rugged_config) { rugged_repo(project.repository).config } let(:legacy_storage) { Storage::LegacyProject.new(project) } let(:hashed_storage) { Storage::Hashed.new(project) } let!(:path_before_rename) { project.path } @@ -71,10 +70,10 @@ RSpec.describe Projects::AfterRenameService do end end - it 'updates project full path in .git/config' do + it 'updates project full path in gitaly' do service_execute - expect(rugged_config['gitlab.fullpath']).to eq(project.full_path) + expect(project.repository.full_path).to eq(project.full_path) end it 'updates storage location' do @@ -173,10 +172,10 @@ RSpec.describe Projects::AfterRenameService do end end - it 'updates project full path in .git/config' do + it 'updates project full path in gitaly' do service_execute - expect(rugged_config['gitlab.fullpath']).to eq(project.full_path) + expect(project.repository.full_path).to eq(project.full_path) end it 'updates storage location' do diff --git a/spec/services/projects/alerting/notify_service_spec.rb b/spec/services/projects/alerting/notify_service_spec.rb index feae8f3967c..aa2ef39bf98 100644 --- a/spec/services/projects/alerting/notify_service_spec.rb +++ b/spec/services/projects/alerting/notify_service_spec.rb @@ -56,6 +56,7 @@ RSpec.describe Projects::Alerting::NotifyService do it_behaves_like 'processes new firing alert' it_behaves_like 'properly assigns the alert properties' + include_examples 'handles race condition in alert creation' it 'passes the integration to alert processing' do expect(Gitlab::AlertManagement::Payload) @@ -118,10 +119,10 @@ RSpec.describe Projects::Alerting::NotifyService do end context 'with overlong payload' do - let(:deep_size_object) { instance_double(Gitlab::Utils::DeepSize, valid?: false) } + let(:payload_raw) { { 'the-payload-is-too-big' => true } } before do - allow(Gitlab::Utils::DeepSize).to receive(:new).and_return(deep_size_object) + stub_const('::Gitlab::Utils::DeepSize::DEFAULT_MAX_DEPTH', 0) end it_behaves_like 'alerts service responds with an error and takes no actions', :bad_request diff --git a/spec/services/projects/container_repository/third_party/delete_tags_service_spec.rb b/spec/services/projects/container_repository/third_party/delete_tags_service_spec.rb index 22cada7816b..4de36452684 100644 --- a/spec/services/projects/container_repository/third_party/delete_tags_service_spec.rb +++ b/spec/services/projects/container_repository/third_party/delete_tags_service_spec.rb @@ -58,7 +58,7 @@ RSpec.describe Projects::ContainerRepository::ThirdParty::DeleteTagsService do stub_put_manifest_request('Ba', 500, {}) end - it { is_expected.to eq(status: :error, message: "could not delete tags: #{tags.join(', ')}")} + it { is_expected.to eq(status: :error, message: "could not delete tags: #{tags.join(', ')}") } context 'when a large list of tag updates fails' do let(:tags) { Array.new(1000) { |i| "tag_#{i}" } } diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb index 59dee209ff9..e112c1e2497 100644 --- a/spec/services/projects/create_service_spec.rb +++ b/spec/services/projects/create_service_spec.rb @@ -4,7 +4,6 @@ require 'spec_helper' RSpec.describe Projects::CreateService, '#execute' do include ExternalAuthorizationServiceHelpers - include GitHelpers let(:user) { create :user } let(:project_name) { 'GitLab' } @@ -254,6 +253,39 @@ RSpec.describe Projects::CreateService, '#execute' do end end + context 'user with project limit' do + let_it_be(:user_with_projects_limit) { create(:user, projects_limit: 0) } + + let(:params) { opts.merge!(namespace_id: target_namespace.id) } + + subject(:project) { create_project(user_with_projects_limit, params) } + + context 'under personal namespace' do + let(:target_namespace) { user_with_projects_limit.namespace } + + it 'cannot create a project' do + expect(project.errors.errors.length).to eq 1 + expect(project.errors.messages[:limit_reached].first).to eq(_('Personal project creation is not allowed. Please contact your administrator with questions')) + end + end + + context 'under group namespace' do + let_it_be(:group) do + create(:group).tap do |group| + group.add_owner(user_with_projects_limit) + end + end + + let(:target_namespace) { group } + + it 'can create a project' do + expect(project).to be_valid + expect(project).to be_saved + expect(project.errors.errors.length).to eq 0 + end + end + end + context 'membership overrides', :sidekiq_inline do let_it_be(:group) { create(:group, :private) } let_it_be(:subgroup_for_projects) { create(:group, :private, parent: group) } @@ -769,11 +801,10 @@ RSpec.describe Projects::CreateService, '#execute' do create_project(user, opts) end - it 'writes project full path to .git/config' do + it 'writes project full path to gitaly' do project = create_project(user, opts) - rugged = rugged_repo(project.repository) - expect(rugged.config['gitlab.fullpath']).to eq project.full_path + expect(project.repository.full_path).to eq project.full_path end it 'triggers PostCreationWorker' do diff --git a/spec/services/projects/enable_deploy_key_service_spec.rb b/spec/services/projects/enable_deploy_key_service_spec.rb index f297ec374cf..c0b3992037e 100644 --- a/spec/services/projects/enable_deploy_key_service_spec.rb +++ b/spec/services/projects/enable_deploy_key_service_spec.rb @@ -5,7 +5,7 @@ require 'spec_helper' RSpec.describe Projects::EnableDeployKeyService do let(:deploy_key) { create(:deploy_key, public: true) } let(:project) { create(:project) } - let(:user) { project.creator} + let(:user) { project.creator } let!(:params) { { key_id: deploy_key.id } } it 'enables the key' do diff --git a/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb b/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb index d0064873972..65da1976dc2 100644 --- a/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb +++ b/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb @@ -68,12 +68,10 @@ RSpec.describe Projects::HashedStorage::MigrateRepositoryService do service.execute end - it 'writes project full path to .git/config' do + it 'writes project full path to gitaly' do service.execute - rugged_config = rugged_repo(project.repository).config['gitlab.fullpath'] - - expect(rugged_config).to eq project.full_path + expect(project.repository.full_path).to eq project.full_path end end diff --git a/spec/services/projects/hashed_storage/rollback_repository_service_spec.rb b/spec/services/projects/hashed_storage/rollback_repository_service_spec.rb index 23e776b72bc..385c03e6308 100644 --- a/spec/services/projects/hashed_storage/rollback_repository_service_spec.rb +++ b/spec/services/projects/hashed_storage/rollback_repository_service_spec.rb @@ -3,8 +3,6 @@ require 'spec_helper' RSpec.describe Projects::HashedStorage::RollbackRepositoryService, :clean_gitlab_redis_shared_state do - include GitHelpers - let(:gitlab_shell) { Gitlab::Shell.new } let(:project) { create(:project, :repository, :wiki_repo, :design_repo, storage_version: ::Project::HASHED_STORAGE_FEATURES[:repository]) } let(:legacy_storage) { Storage::LegacyProject.new(project) } @@ -68,12 +66,10 @@ RSpec.describe Projects::HashedStorage::RollbackRepositoryService, :clean_gitlab service.execute end - it 'writes project full path to .git/config' do + it 'writes project full path to gitaly' do service.execute - rugged_config = rugged_repo(project.repository).config['gitlab.fullpath'] - - expect(rugged_config).to eq project.full_path + expect(project.repository.full_path).to eq project.full_path end end diff --git a/spec/services/projects/import_export/export_service_spec.rb b/spec/services/projects/import_export/export_service_spec.rb index 54abbc04084..285687505e9 100644 --- a/spec/services/projects/import_export/export_service_spec.rb +++ b/spec/services/projects/import_export/export_service_spec.rb @@ -89,7 +89,21 @@ RSpec.describe Projects::ImportExport::ExportService do context 'when all saver services succeed' do before do - allow(service).to receive(:save_services).and_return(true) + allow(service).to receive(:save_exporters).and_return(true) + end + + it 'logs a successful message' do + allow(Gitlab::ImportExport::Saver).to receive(:save).and_return(true) + + expect(service.instance_variable_get(:@logger)).to receive(:info).ordered.with( + hash_including({ message: 'Project export started', project_id: project.id }) + ) + + expect(service.instance_variable_get(:@logger)).to receive(:info).ordered.with( + hash_including({ message: 'Project successfully exported', project_id: project.id }) + ) + + service.execute end it 'saves the project in the file system' do @@ -111,6 +125,7 @@ RSpec.describe Projects::ImportExport::ExportService do end it 'calls the after export strategy' do + allow(Gitlab::ImportExport::Saver).to receive(:save).and_return(true) expect(after_export_strategy).to receive(:execute) service.execute(after_export_strategy) @@ -119,7 +134,7 @@ RSpec.describe Projects::ImportExport::ExportService do context 'when after export strategy fails' do before do allow(after_export_strategy).to receive(:execute).and_return(false) - expect(Gitlab::ImportExport::Saver).to receive(:save).with(exportable: project, shared: shared).and_return(true) + allow(Gitlab::ImportExport::Saver).to receive(:save).and_return(true) end after do @@ -140,7 +155,9 @@ RSpec.describe Projects::ImportExport::ExportService do end it 'notifies logger' do - expect(service.instance_variable_get(:@logger)).to receive(:error) + expect(service.instance_variable_get(:@logger)).to receive(:error).with( + hash_including({ message: 'Project export error', project_id: project.id }) + ) end end end diff --git a/spec/services/projects/import_export/relation_export_service_spec.rb b/spec/services/projects/import_export/relation_export_service_spec.rb new file mode 100644 index 00000000000..94f5653ee7d --- /dev/null +++ b/spec/services/projects/import_export/relation_export_service_spec.rb @@ -0,0 +1,121 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Projects::ImportExport::RelationExportService do + using RSpec::Parameterized::TableSyntax + + subject(:service) { described_class.new(relation_export, 'jid') } + + let_it_be(:project_export_job) { create(:project_export_job) } + let_it_be(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" } + let_it_be(:archive_path) { "#{Dir.tmpdir}/project_archive_spec" } + + let(:relation_export) { create(:project_relation_export, relation: relation, project_export_job: project_export_job) } + + before do + stub_uploads_object_storage(ImportExportUploader, enabled: false) + + allow(project_export_job.project.import_export_shared).to receive(:export_path).and_return(export_path) + allow(project_export_job.project.import_export_shared).to receive(:archive_path).and_return(archive_path) + allow(FileUtils).to receive(:remove_entry).with(any_args).and_call_original + end + + describe '#execute' do + let(:relation) { 'labels' } + + it 'removes temporary paths used to export files' do + expect(FileUtils).to receive(:remove_entry).with(export_path) + expect(FileUtils).to receive(:remove_entry).with(archive_path) + + service.execute + end + + context 'when saver fails to export relation' do + before do + allow_next_instance_of(Gitlab::ImportExport::Project::RelationSaver) do |saver| + allow(saver).to receive(:save).and_return(false) + end + end + + it 'flags export as failed' do + service.execute + + expect(relation_export.failed?).to eq(true) + end + + it 'logs failed message' do + expect_next_instance_of(Gitlab::Export::Logger) do |logger| + expect(logger).to receive(:error).with( + export_error: '', + message: 'Project relation export failed', + project_export_job_id: project_export_job.id, + project_id: project_export_job.project.id, + project_name: project_export_job.project.name + ) + end + + service.execute + end + end + + context 'when an exception is raised' do + before do + allow_next_instance_of(Gitlab::ImportExport::Project::RelationSaver) do |saver| + allow(saver).to receive(:save).and_raise('Error!') + end + end + + it 'flags export as failed' do + service.execute + + expect(relation_export.failed?).to eq(true) + expect(relation_export.export_error).to eq('Error!') + end + + it 'logs exception error message' do + expect_next_instance_of(Gitlab::Export::Logger) do |logger| + expect(logger).to receive(:error).with( + export_error: 'Error!', + message: 'Project relation export failed', + project_export_job_id: project_export_job.id, + project_id: project_export_job.project.id, + project_name: project_export_job.project.name + ) + end + + service.execute + end + end + + describe 'relation name and saver class' do + where(:relation_name, :saver) do + Projects::ImportExport::RelationExport::UPLOADS_RELATION | Gitlab::ImportExport::UploadsSaver + Projects::ImportExport::RelationExport::REPOSITORY_RELATION | Gitlab::ImportExport::RepoSaver + Projects::ImportExport::RelationExport::WIKI_REPOSITORY_RELATION | Gitlab::ImportExport::WikiRepoSaver + Projects::ImportExport::RelationExport::LFS_OBJECTS_RELATION | Gitlab::ImportExport::LfsSaver + Projects::ImportExport::RelationExport::SNIPPETS_REPOSITORY_RELATION | Gitlab::ImportExport::SnippetsRepoSaver + Projects::ImportExport::RelationExport::DESIGN_REPOSITORY_RELATION | Gitlab::ImportExport::DesignRepoSaver + Projects::ImportExport::RelationExport::ROOT_RELATION | Gitlab::ImportExport::Project::RelationSaver + 'labels' | Gitlab::ImportExport::Project::RelationSaver + end + + with_them do + let(:relation) { relation_name } + + it 'exports relation using correct saver' do + expect(saver).to receive(:new).and_call_original + + service.execute + end + + it 'assigns finished status and relation file' do + service.execute + + expect(relation_export.finished?).to eq(true) + expect(relation_export.upload.export_file.filename).to eq("#{relation}.tar.gz") + end + end + end + end +end diff --git a/spec/services/projects/lfs_pointers/lfs_download_link_list_service_spec.rb b/spec/services/projects/lfs_pointers/lfs_download_link_list_service_spec.rb index 047ebe65dff..d472d6493c3 100644 --- a/spec/services/projects/lfs_pointers/lfs_download_link_list_service_spec.rb +++ b/spec/services/projects/lfs_pointers/lfs_download_link_list_service_spec.rb @@ -6,7 +6,7 @@ RSpec.describe Projects::LfsPointers::LfsDownloadLinkListService do let(:lfs_endpoint) { "#{import_url}/info/lfs/objects/batch" } let!(:project) { create(:project, import_url: import_url) } let(:new_oids) { { 'oid1' => 123, 'oid2' => 125 } } - let(:headers) { { 'X-Some-Header' => '456' }} + let(:headers) { { 'X-Some-Header' => '456' } } let(:remote_uri) { URI.parse(lfs_endpoint) } let(:request_object) { HTTParty::Request.new(Net::HTTP::Post, '/') } diff --git a/spec/services/projects/lfs_pointers/lfs_download_service_spec.rb b/spec/services/projects/lfs_pointers/lfs_download_service_spec.rb index 04c6349bf52..b67b4d64c1d 100644 --- a/spec/services/projects/lfs_pointers/lfs_download_service_spec.rb +++ b/spec/services/projects/lfs_pointers/lfs_download_service_spec.rb @@ -250,7 +250,7 @@ RSpec.describe Projects::LfsPointers::LfsDownloadService do end context 'that is not blocked' do - let(:redirect_link) { "http://example.com/"} + let(:redirect_link) { "http://example.com/" } before do stub_full_request(download_link).to_return(status: 301, headers: { 'Location' => redirect_link }) diff --git a/spec/services/projects/lfs_pointers/lfs_object_download_list_service_spec.rb b/spec/services/projects/lfs_pointers/lfs_object_download_list_service_spec.rb index 981d7027a17..adcc2b85706 100644 --- a/spec/services/projects/lfs_pointers/lfs_object_download_list_service_spec.rb +++ b/spec/services/projects/lfs_pointers/lfs_object_download_list_service_spec.rb @@ -3,8 +3,8 @@ require 'spec_helper' RSpec.describe Projects::LfsPointers::LfsObjectDownloadListService do let(:import_url) { 'http://www.gitlab.com/demo/repo.git' } - let(:default_endpoint) { "#{import_url}/info/lfs/objects/batch"} - let(:group) { create(:group, lfs_enabled: true)} + let(:default_endpoint) { "#{import_url}/info/lfs/objects/batch" } + let(:group) { create(:group, lfs_enabled: true) } let!(:project) { create(:project, namespace: group, import_url: import_url, lfs_enabled: true) } let!(:lfs_objects_project) { create_list(:lfs_objects_project, 2, project: project) } let!(:existing_lfs_objects) { LfsObject.pluck(:oid, :size).to_h } @@ -75,7 +75,7 @@ RSpec.describe Projects::LfsPointers::LfsObjectDownloadListService do end context 'when import url has credentials' do - let(:import_url) { 'http://user:password@www.gitlab.com/demo/repo.git'} + let(:import_url) { 'http://user:password@www.gitlab.com/demo/repo.git' } it 'adds the credentials to the new endpoint' do expect(Projects::LfsPointers::LfsDownloadLinkListService) diff --git a/spec/services/projects/participants_service_spec.rb b/spec/services/projects/participants_service_spec.rb index 61edfd23700..fc745cd669f 100644 --- a/spec/services/projects/participants_service_spec.rb +++ b/spec/services/projects/participants_service_spec.rb @@ -107,7 +107,7 @@ RSpec.describe Projects::ParticipantsService do shared_examples 'return project members' do context 'when there is a project in group namespace' do let_it_be(:public_group) { create(:group, :public) } - let_it_be(:public_project) { create(:project, :public, namespace: public_group)} + let_it_be(:public_project) { create(:project, :public, namespace: public_group) } let_it_be(:public_group_owner) { create(:user) } @@ -125,9 +125,9 @@ RSpec.describe Projects::ParticipantsService do context 'when there is a private group and a public project' do let_it_be(:public_group) { create(:group, :public) } let_it_be(:private_group) { create(:group, :private, :nested) } - let_it_be(:public_project) { create(:project, :public, namespace: public_group)} + let_it_be(:public_project) { create(:project, :public, namespace: public_group) } - let_it_be(:project_issue) { create(:issue, project: public_project)} + let_it_be(:project_issue) { create(:issue, project: public_project) } let_it_be(:public_group_owner) { create(:user) } let_it_be(:private_group_member) { create(:user) } diff --git a/spec/services/projects/prometheus/alerts/notify_service_spec.rb b/spec/services/projects/prometheus/alerts/notify_service_spec.rb index 6f760e6dbfa..7bf6dfd0fd8 100644 --- a/spec/services/projects/prometheus/alerts/notify_service_spec.rb +++ b/spec/services/projects/prometheus/alerts/notify_service_spec.rb @@ -177,6 +177,7 @@ RSpec.describe Projects::Prometheus::Alerts::NotifyService do end it { is_expected.to be_success } + include_examples 'does not send alert notification emails' include_examples 'does not process incident issues' end @@ -187,6 +188,7 @@ RSpec.describe Projects::Prometheus::Alerts::NotifyService do end it { is_expected.to be_success } + include_examples 'does not send alert notification emails' end @@ -196,6 +198,7 @@ RSpec.describe Projects::Prometheus::Alerts::NotifyService do end it { is_expected.to be_success } + include_examples 'does not process incident issues' end end @@ -313,11 +316,11 @@ RSpec.describe Projects::Prometheus::Alerts::NotifyService do end context 'when the payload is too big' do - let(:payload) { { 'the-payload-is-too-big' => true } } - let(:deep_size_object) { instance_double(Gitlab::Utils::DeepSize, valid?: false) } + let(:payload_raw) { { 'the-payload-is-too-big' => true } } + let(:payload) { ActionController::Parameters.new(payload_raw).permit! } before do - allow(Gitlab::Utils::DeepSize).to receive(:new).and_return(deep_size_object) + stub_const('::Gitlab::Utils::DeepSize::DEFAULT_MAX_DEPTH', 0) end it_behaves_like 'alerts service responds with an error and takes no actions', :bad_request diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb index ecf9f92d74f..8f505c31c5a 100644 --- a/spec/services/projects/transfer_service_spec.rb +++ b/spec/services/projects/transfer_service_spec.rb @@ -3,8 +3,6 @@ require 'spec_helper' RSpec.describe Projects::TransferService do - include GitHelpers - let_it_be(:group) { create(:group) } let_it_be(:user) { create(:user) } let_it_be(:group_integration) { create(:integrations_slack, :group, group: group, webhook: 'http://group.slack.com') } @@ -64,6 +62,30 @@ RSpec.describe Projects::TransferService do expect(project.namespace).to eq(group) end + context 'EventStore' do + let(:group) do + create(:group, :nested).tap { |g| g.add_owner(user) } + end + + let(:target) do + create(:group, :nested).tap { |g| g.add_owner(user) } + end + + let(:project) { create(:project, namespace: group) } + + it 'publishes a ProjectTransferedEvent' do + expect { execute_transfer } + .to publish_event(Projects::ProjectTransferedEvent) + .with( + project_id: project.id, + old_namespace_id: group.id, + old_root_namespace_id: group.root_ancestor.id, + new_namespace_id: target.id, + new_root_namespace_id: target.root_ancestor.id + ) + end + end + context 'when project has an associated project namespace' do it 'keeps project namespace in sync with project' do transfer_result = execute_transfer @@ -178,10 +200,10 @@ RSpec.describe Projects::TransferService do expect(project.disk_path).to start_with(group.path) end - it 'updates project full path in .git/config' do + it 'updates project full path in gitaly' do execute_transfer - expect(rugged_config['gitlab.fullpath']).to eq "#{group.full_path}/#{project.path}" + expect(project.repository.full_path).to eq "#{group.full_path}/#{project.path}" end it 'updates storage location' do @@ -272,10 +294,10 @@ RSpec.describe Projects::TransferService do expect(original_path).to eq current_path end - it 'rolls back project full path in .git/config' do + it 'rolls back project full path in gitaly' do attempt_project_transfer - expect(rugged_config['gitlab.fullpath']).to eq project.full_path + expect(project.repository.full_path).to eq project.full_path end it "doesn't send move notifications" do @@ -299,6 +321,11 @@ RSpec.describe Projects::TransferService do ) end + it 'does not publish a ProjectTransferedEvent' do + expect { attempt_project_transfer } + .not_to publish_event(Projects::ProjectTransferedEvent) + end + context 'when project has pending builds', :sidekiq_inline do let!(:other_project) { create(:project) } let!(:pending_build) { create(:ci_pending_build, project: project.reload) } @@ -741,10 +768,6 @@ RSpec.describe Projects::TransferService do end end - def rugged_config - rugged_repo(project.repository).config - end - def project_namespace_in_sync(group) project.reload expect(project.namespace).to eq(group) diff --git a/spec/services/projects/update_service_spec.rb b/spec/services/projects/update_service_spec.rb index ca838be0fa8..85d3e99109d 100644 --- a/spec/services/projects/update_service_spec.rb +++ b/spec/services/projects/update_service_spec.rb @@ -348,6 +348,18 @@ RSpec.describe Projects::UpdateService do end end + context 'when archiving a project' do + it 'publishes a ProjectTransferedEvent' do + expect { update_project(project, user, archived: true) } + .to publish_event(Projects::ProjectArchivedEvent) + .with( + project_id: project.id, + namespace_id: project.namespace_id, + root_namespace_id: project.root_namespace.id + ) + end + end + context 'when changing operations feature visibility' do let(:feature_params) { { operations_access_level: ProjectFeature::DISABLED } } diff --git a/spec/services/projects/update_statistics_service_spec.rb b/spec/services/projects/update_statistics_service_spec.rb index 6987185b549..1cc69e7e2fe 100644 --- a/spec/services/projects/update_statistics_service_spec.rb +++ b/spec/services/projects/update_statistics_service_spec.rb @@ -5,7 +5,7 @@ require 'spec_helper' RSpec.describe Projects::UpdateStatisticsService do using RSpec::Parameterized::TableSyntax - let(:service) { described_class.new(project, nil, statistics: statistics)} + let(:service) { described_class.new(project, nil, statistics: statistics) } let(:statistics) { %w(repository_size) } describe '#execute' do diff --git a/spec/services/protected_branches/cache_service_spec.rb b/spec/services/protected_branches/cache_service_spec.rb new file mode 100644 index 00000000000..4fa7553c23d --- /dev/null +++ b/spec/services/protected_branches/cache_service_spec.rb @@ -0,0 +1,113 @@ +# frozen_string_literal: true +# rubocop:disable Style/RedundantFetchBlock +# +require 'spec_helper' + +RSpec.describe ProtectedBranches::CacheService, :clean_gitlab_redis_cache do + subject(:service) { described_class.new(project, user) } + + let_it_be(:project) { create(:project) } + let_it_be(:user) { project.first_owner } + + let(:immediate_expiration) { 0 } + + describe '#fetch' do + it 'caches the value' do + expect(service.fetch('main') { true }).to eq(true) + expect(service.fetch('not-found') { false }).to eq(false) + + # Uses cached values + expect(service.fetch('main') { false }).to eq(true) + expect(service.fetch('not-found') { true }).to eq(false) + end + + it 'sets expiry on the key' do + stub_const("#{described_class.name}::CACHE_EXPIRE_IN", immediate_expiration) + + expect(service.fetch('main') { true }).to eq(true) + expect(service.fetch('not-found') { false }).to eq(false) + + expect(service.fetch('main') { false }).to eq(false) + expect(service.fetch('not-found') { true }).to eq(true) + end + + it 'does not set an expiry on the key after the hash is already created' do + expect(service.fetch('main') { true }).to eq(true) + + stub_const("#{described_class.name}::CACHE_EXPIRE_IN", immediate_expiration) + + expect(service.fetch('not-found') { false }).to eq(false) + + expect(service.fetch('main') { false }).to eq(true) + expect(service.fetch('not-found') { true }).to eq(false) + end + + context 'when CACHE_LIMIT is exceeded' do + before do + stub_const("#{described_class.name}::CACHE_LIMIT", 2) + end + + it 'recreates cache' do + expect(service.fetch('main') { true }).to eq(true) + expect(service.fetch('not-found') { false }).to eq(false) + + # Uses cached values + expect(service.fetch('main') { false }).to eq(true) + expect(service.fetch('not-found') { true }).to eq(false) + + # Overflow + expect(service.fetch('new-branch') { true }).to eq(true) + + # Refreshes values + expect(service.fetch('main') { false }).to eq(false) + expect(service.fetch('not-found') { true }).to eq(true) + end + end + + context 'when dry_run is on' do + it 'does not use cached value' do + expect(service.fetch('main', dry_run: true) { true }).to eq(true) + expect(service.fetch('main', dry_run: true) { false }).to eq(false) + end + + context 'when cache mismatch' do + it 'logs an error' do + expect(service.fetch('main', dry_run: true) { true }).to eq(true) + + expect(Gitlab::AppLogger).to receive(:error).with( + 'class' => described_class.name, + 'message' => /Cache mismatch/, + 'project_id' => project.id, + 'project_path' => project.full_path + ) + + expect(service.fetch('main', dry_run: true) { false }).to eq(false) + end + end + + context 'when cache matches' do + it 'does not log an error' do + expect(service.fetch('main', dry_run: true) { true }).to eq(true) + + expect(Gitlab::AppLogger).not_to receive(:error) + + expect(service.fetch('main', dry_run: true) { true }).to eq(true) + end + end + end + end + + describe '#refresh' do + it 'clears cached values' do + expect(service.fetch('main') { true }).to eq(true) + expect(service.fetch('not-found') { false }).to eq(false) + + service.refresh + + # Recreates cache + expect(service.fetch('main') { false }).to eq(false) + expect(service.fetch('not-found') { true }).to eq(true) + end + end +end +# rubocop:enable Style/RedundantFetchBlock diff --git a/spec/services/protected_branches/create_service_spec.rb b/spec/services/protected_branches/create_service_spec.rb index 3ac42d41377..b42524e761c 100644 --- a/spec/services/protected_branches/create_service_spec.rb +++ b/spec/services/protected_branches/create_service_spec.rb @@ -3,7 +3,8 @@ require 'spec_helper' RSpec.describe ProtectedBranches::CreateService do - let(:project) { create(:project) } + let_it_be_with_reload(:project) { create(:project) } + let(:user) { project.first_owner } let(:params) do { @@ -13,22 +14,28 @@ RSpec.describe ProtectedBranches::CreateService do } end + subject(:service) { described_class.new(project, user, params) } + describe '#execute' do let(:name) { 'master' } - subject(:service) { described_class.new(project, user, params) } - it 'creates a new protected branch' do expect { service.execute }.to change(ProtectedBranch, :count).by(1) expect(project.protected_branches.last.push_access_levels.map(&:access_level)).to eq([Gitlab::Access::MAINTAINER]) expect(project.protected_branches.last.merge_access_levels.map(&:access_level)).to eq([Gitlab::Access::MAINTAINER]) end + it 'refreshes the cache' do + expect_next_instance_of(ProtectedBranches::CacheService) do |cache_service| + expect(cache_service).to receive(:refresh) + end + + service.execute + end + context 'when protecting a branch with a name that contains HTML tags' do let(:name) { 'foo<b>bar<\b>' } - subject(:service) { described_class.new(project, user, params) } - it 'creates a new protected branch' do expect { service.execute }.to change(ProtectedBranch, :count).by(1) expect(project.protected_branches.last.name).to eq(name) @@ -52,16 +59,18 @@ RSpec.describe ProtectedBranches::CreateService do end context 'when a policy restricts rule creation' do - before do - policy = instance_double(ProtectedBranchPolicy, allowed?: false) - expect(ProtectedBranchPolicy).to receive(:new).and_return(policy) - end - it "prevents creation of the protected branch rule" do + disallow(:create_protected_branch, an_instance_of(ProtectedBranch)) + expect do service.execute end.to raise_error(Gitlab::Access::AccessDeniedError) end end end + + def disallow(ability, protected_branch) + allow(Ability).to receive(:allowed?).and_call_original + allow(Ability).to receive(:allowed?).with(user, ability, protected_branch).and_return(false) + end end diff --git a/spec/services/protected_branches/destroy_service_spec.rb b/spec/services/protected_branches/destroy_service_spec.rb index 4e55c72f312..9fa07820148 100644 --- a/spec/services/protected_branches/destroy_service_spec.rb +++ b/spec/services/protected_branches/destroy_service_spec.rb @@ -3,30 +3,41 @@ require 'spec_helper' RSpec.describe ProtectedBranches::DestroyService do - let(:protected_branch) { create(:protected_branch) } - let(:project) { protected_branch.project } + let_it_be_with_reload(:project) { create(:project) } + + let(:protected_branch) { create(:protected_branch, project: project) } let(:user) { project.first_owner } - describe '#execute' do - subject(:service) { described_class.new(project, user) } + subject(:service) { described_class.new(project, user) } + describe '#execute' do it 'destroys a protected branch' do service.execute(protected_branch) expect(protected_branch).to be_destroyed end - context 'when a policy restricts rule deletion' do - before do - policy = instance_double(ProtectedBranchPolicy, allowed?: false) - expect(ProtectedBranchPolicy).to receive(:new).and_return(policy) + it 'refreshes the cache' do + expect_next_instance_of(ProtectedBranches::CacheService) do |cache_service| + expect(cache_service).to receive(:refresh) end + service.execute(protected_branch) + end + + context 'when a policy restricts rule deletion' do it "prevents deletion of the protected branch rule" do + disallow(:destroy_protected_branch, protected_branch) + expect do service.execute(protected_branch) end.to raise_error(Gitlab::Access::AccessDeniedError) end end end + + def disallow(ability, protected_branch) + allow(Ability).to receive(:allowed?).and_call_original + allow(Ability).to receive(:allowed?).with(user, ability, protected_branch).and_return(false) + end end diff --git a/spec/services/protected_branches/update_service_spec.rb b/spec/services/protected_branches/update_service_spec.rb index 4405af35c37..c4fe4d78070 100644 --- a/spec/services/protected_branches/update_service_spec.rb +++ b/spec/services/protected_branches/update_service_spec.rb @@ -3,27 +3,34 @@ require 'spec_helper' RSpec.describe ProtectedBranches::UpdateService do - let(:protected_branch) { create(:protected_branch) } - let(:project) { protected_branch.project } + let_it_be_with_reload(:project) { create(:project) } + + let(:protected_branch) { create(:protected_branch, project: project) } let(:user) { project.first_owner } let(:params) { { name: new_name } } + subject(:service) { described_class.new(project, user, params) } + describe '#execute' do let(:new_name) { 'new protected branch name' } let(:result) { service.execute(protected_branch) } - subject(:service) { described_class.new(project, user, params) } - it 'updates a protected branch' do expect(result.reload.name).to eq(params[:name]) end + it 'refreshes the cache' do + expect_next_instance_of(ProtectedBranches::CacheService) do |cache_service| + expect(cache_service).to receive(:refresh) + end + + result + end + context 'when updating name of a protected branch to one that contains HTML tags' do let(:new_name) { 'foo<b>bar<\b>' } let(:result) { service.execute(protected_branch) } - subject(:service) { described_class.new(project, user, params) } - it 'updates a protected branch' do expect(result.reload.name).to eq(new_name) end @@ -37,15 +44,17 @@ RSpec.describe ProtectedBranches::UpdateService do end end - context 'when a policy restricts rule creation' do - before do - policy = instance_double(ProtectedBranchPolicy, allowed?: false) - expect(ProtectedBranchPolicy).to receive(:new).and_return(policy) - end + context 'when a policy restricts rule update' do + it "prevents update of the protected branch rule" do + disallow(:update_protected_branch, protected_branch) - it "prevents creation of the protected branch rule" do expect { service.execute(protected_branch) }.to raise_error(Gitlab::Access::AccessDeniedError) end end end + + def disallow(ability, protected_branch) + allow(Ability).to receive(:allowed?).and_call_original + allow(Ability).to receive(:allowed?).with(user, ability, protected_branch).and_return(false) + end end diff --git a/spec/services/quick_actions/interpret_service_spec.rb b/spec/services/quick_actions/interpret_service_spec.rb index 3f11eaa7e93..2d38d968ce4 100644 --- a/spec/services/quick_actions/interpret_service_spec.rb +++ b/spec/services/quick_actions/interpret_service_spec.rb @@ -810,38 +810,6 @@ RSpec.describe QuickActions::InterpretService do end end - shared_examples 'attention command' do - it 'updates reviewers attention status' do - _, _, message = service.execute(content, issuable) - - expect(message).to eq("Requested attention from #{developer.to_reference}.") - - reviewer.reload - - expect(reviewer).to be_attention_requested - end - - it 'supports attn alias' do - attn_cmd = content.gsub(/attention/, 'attn') - _, _, message = service.execute(attn_cmd, issuable) - - expect(message).to eq("Requested attention from #{developer.to_reference}.") - - reviewer.reload - - expect(reviewer).to be_attention_requested - end - end - - shared_examples 'remove attention command' do - it 'updates reviewers attention status' do - _, _, message = service.execute(content, issuable) - - expect(message).to eq("Removed attention from #{developer.to_reference}.") - expect(reviewer).not_to be_attention_requested - end - end - it_behaves_like 'reopen command' do let(:content) { '/reopen' } let(:issuable) { issue } @@ -1888,7 +1856,7 @@ RSpec.describe QuickActions::InterpretService do context '/target_branch command' do let(:non_empty_project) { create(:project, :repository) } let(:another_merge_request) { create(:merge_request, author: developer, source_project: non_empty_project) } - let(:service) { described_class.new(non_empty_project, developer)} + let(:service) { described_class.new(non_empty_project, developer) } it 'updates target_branch if /target_branch command is executed' do _, updates, _ = service.execute('/target_branch merge-test', merge_request) @@ -2481,82 +2449,6 @@ RSpec.describe QuickActions::InterpretService do expect(message).to eq('One or more contacts were successfully removed.') end end - - describe 'attention command' do - let(:issuable) { create(:merge_request, reviewers: [developer], source_project: project) } - let(:reviewer) { issuable.merge_request_reviewers.find_by(user_id: developer.id) } - let(:content) { "/attention @#{developer.username}" } - - context 'with one user' do - before do - reviewer.update!(state: :reviewed) - end - - it_behaves_like 'attention command' - end - - context 'with no user' do - let(:content) { "/attention" } - - it_behaves_like 'failed command', 'Failed to request attention because no user was found.' - end - - context 'with incorrect permissions' do - let(:service) { described_class.new(project, create(:user)) } - - it_behaves_like 'failed command', 'Could not apply attention command.' - end - - context 'with feature flag disabled' do - before do - stub_feature_flags(mr_attention_requests: false) - end - - it_behaves_like 'failed command', 'Could not apply attention command.' - end - - context 'with an issue instead of a merge request' do - let(:issuable) { issue } - - it_behaves_like 'failed command', 'Could not apply attention command.' - end - end - - describe 'remove attention command' do - let(:issuable) { create(:merge_request, reviewers: [developer], source_project: project) } - let(:reviewer) { issuable.merge_request_reviewers.find_by(user_id: developer.id) } - let(:content) { "/remove_attention @#{developer.username}" } - - context 'with one user' do - it_behaves_like 'remove attention command' - end - - context 'with no user' do - let(:content) { "/remove_attention" } - - it_behaves_like 'failed command', 'Failed to remove attention because no user was found.' - end - - context 'with incorrect permissions' do - let(:service) { described_class.new(project, create(:user)) } - - it_behaves_like 'failed command', 'Could not apply remove_attention command.' - end - - context 'with feature flag disabled' do - before do - stub_feature_flags(mr_attention_requests: false) - end - - it_behaves_like 'failed command', 'Could not apply remove_attention command.' - end - - context 'with an issue instead of a merge request' do - let(:issuable) { issue } - - it_behaves_like 'failed command', 'Could not apply remove_attention command.' - end - end end describe '#explain' do diff --git a/spec/services/releases/create_service_spec.rb b/spec/services/releases/create_service_spec.rb index 566d73a3b75..2421fab0eec 100644 --- a/spec/services/releases/create_service_spec.rb +++ b/spec/services/releases/create_service_spec.rb @@ -111,14 +111,6 @@ RSpec.describe Releases::CreateService do expect(result[:message]).to eq("Milestone(s) not found: #{inexistent_milestone_tag}") end end - end - - describe '#find_or_build_release' do - it 'does not save the built release' do - service.find_or_build_release - - expect(project.releases.count).to eq(0) - end context 'when existing milestone is passed in' do let(:title) { 'v1.0' } diff --git a/spec/services/releases/destroy_service_spec.rb b/spec/services/releases/destroy_service_spec.rb index bc5bff0b31d..46550ac5bef 100644 --- a/spec/services/releases/destroy_service_spec.rb +++ b/spec/services/releases/destroy_service_spec.rb @@ -39,7 +39,7 @@ RSpec.describe Releases::DestroyService do end context 'when release is not found' do - let!(:release) { } + let!(:release) {} it 'returns an error' do is_expected.to include(status: :error, diff --git a/spec/services/releases/update_service_spec.rb b/spec/services/releases/update_service_spec.rb index 932a7fab5ec..7461470a844 100644 --- a/spec/services/releases/update_service_spec.rb +++ b/spec/services/releases/update_service_spec.rb @@ -45,7 +45,7 @@ RSpec.describe Releases::UpdateService do end context 'when the release does not exist' do - let!(:release) { } + let!(:release) {} it_behaves_like 'a failed update' end diff --git a/spec/services/resource_access_tokens/create_service_spec.rb b/spec/services/resource_access_tokens/create_service_spec.rb index 127948549b0..442232920f9 100644 --- a/spec/services/resource_access_tokens/create_service_spec.rb +++ b/spec/services/resource_access_tokens/create_service_spec.rb @@ -16,7 +16,7 @@ RSpec.describe ResourceAccessTokens::CreateService do describe '#execute' do shared_examples 'token creation fails' do - let(:resource) { create(:project)} + let(:resource) { create(:project) } it 'does not add the project bot as a member' do expect { subject }.not_to change { resource.members.count } diff --git a/spec/services/resource_events/change_labels_service_spec.rb b/spec/services/resource_events/change_labels_service_spec.rb index c2c0a4c2126..8dc7b07e397 100644 --- a/spec/services/resource_events/change_labels_service_spec.rb +++ b/spec/services/resource_events/change_labels_service_spec.rb @@ -5,11 +5,40 @@ require 'spec_helper' RSpec.describe ResourceEvents::ChangeLabelsService do let_it_be(:project) { create(:project) } let_it_be(:author) { create(:user) } + let_it_be(:issue) { create(:issue, project: project) } + let_it_be(:incident) { create(:incident, project: project) } - let(:resource) { create(:issue, project: project) } + let(:resource) { issue } - describe '.change_labels' do - subject { described_class.new(resource, author).execute(added_labels: added, removed_labels: removed) } + describe '#execute' do + shared_examples 'creating timeline events' do + context 'when resource is not an incident' do + let(:resource) { issue } + + it 'does not call create timeline events service' do + expect(IncidentManagement::TimelineEvents::CreateService).not_to receive(:change_labels) + + change_labels + end + end + + context 'when resource is an incident' do + let(:resource) { incident } + + it 'calls create timeline events service with correct attributes' do + expect(IncidentManagement::TimelineEvents::CreateService) + .to receive(:change_labels) + .with(resource, author, added_labels: added, removed_labels: removed) + .and_call_original + + change_labels + end + end + end + + subject(:change_labels) do + described_class.new(resource, author).execute(added_labels: added, removed_labels: removed) + end let_it_be(:labels) { create_list(:label, 2, project: project) } @@ -20,9 +49,9 @@ RSpec.describe ResourceEvents::ChangeLabelsService do end it 'expires resource note etag cache' do - expect_any_instance_of(Gitlab::EtagCaching::Store) - .to receive(:touch) - .with("/#{resource.project.namespace.to_param}/#{resource.project.to_param}/noteable/issue/#{resource.id}/notes") + expect_any_instance_of(Gitlab::EtagCaching::Store).to receive(:touch).with( + "/#{resource.project.namespace.to_param}/#{resource.project.to_param}/noteable/issue/#{resource.id}/notes" + ) described_class.new(resource, author).execute(added_labels: [labels[0]]) end @@ -32,10 +61,12 @@ RSpec.describe ResourceEvents::ChangeLabelsService do let(:removed) { [] } it 'creates new label event' do - expect { subject }.to change { resource.resource_label_events.count }.from(0).to(1) + expect { change_labels }.to change { resource.resource_label_events.count }.from(0).to(1) expect_label_event(resource.resource_label_events.first, labels[0], 'add') end + + it_behaves_like 'creating timeline events' end context 'when removing a label' do @@ -43,10 +74,12 @@ RSpec.describe ResourceEvents::ChangeLabelsService do let(:removed) { [labels[1]] } it 'creates new label event' do - expect { subject }.to change { resource.resource_label_events.count }.from(0).to(1) + expect { change_labels }.to change { resource.resource_label_events.count }.from(0).to(1) expect_label_event(resource.resource_label_events.first, labels[1], 'remove') end + + it_behaves_like 'creating timeline events' end context 'when both adding and removing labels' do @@ -55,8 +88,10 @@ RSpec.describe ResourceEvents::ChangeLabelsService do it 'creates all label events in a single query' do expect(ApplicationRecord).to receive(:legacy_bulk_insert).once.and_call_original - expect { subject }.to change { resource.resource_label_events.count }.from(0).to(2) + expect { change_labels }.to change { resource.resource_label_events.count }.from(0).to(2) end + + it_behaves_like 'creating timeline events' end describe 'usage data' do @@ -67,7 +102,7 @@ RSpec.describe ResourceEvents::ChangeLabelsService do it 'tracks changed labels' do expect(Gitlab::UsageDataCounters::IssueActivityUniqueCounter).to receive(:track_issue_label_changed_action) - subject + change_labels end end @@ -75,9 +110,10 @@ RSpec.describe ResourceEvents::ChangeLabelsService do let(:resource) { create(:merge_request, source_project: project) } it 'does not track changed labels' do - expect(Gitlab::UsageDataCounters::IssueActivityUniqueCounter).not_to receive(:track_issue_label_changed_action) + expect(Gitlab::UsageDataCounters::IssueActivityUniqueCounter) + .not_to receive(:track_issue_label_changed_action) - subject + change_labels end end end diff --git a/spec/services/search/group_service_spec.rb b/spec/services/search/group_service_spec.rb index 7beeec98b23..152d0700cc1 100644 --- a/spec/services/search/group_service_spec.rb +++ b/spec/services/search/group_service_spec.rb @@ -11,7 +11,7 @@ RSpec.describe Search::GroupService do # These projects shouldn't be found let!(:outside_project) { create(:project, :public, name: "Outside #{term}") } - let!(:private_project) { create(:project, :private, namespace: nested_group, name: "Private #{term}" )} + let!(:private_project) { create(:project, :private, namespace: nested_group, name: "Private #{term}" ) } let!(:other_project) { create(:project, :public, namespace: nested_group, name: term.reverse) } # These projects should be found diff --git a/spec/services/security/ci_configuration/sast_parser_service_spec.rb b/spec/services/security/ci_configuration/sast_parser_service_spec.rb index 4346d0a9e07..1fd196cdcee 100644 --- a/spec/services/security/ci_configuration/sast_parser_service_spec.rb +++ b/spec/services/security/ci_configuration/sast_parser_service_spec.rb @@ -16,6 +16,7 @@ RSpec.describe Security::CiConfiguration::SastParserService do let(:bandit) { configuration['analyzers'][0] } let(:brakeman) { configuration['analyzers'][1] } let(:sast_brakeman_level) { brakeman['variables'][0] } + let(:secure_analyzers_prefix) { '$CI_TEMPLATE_REGISTRY_HOST/security-products' } it 'parses the configuration for SAST' do expect(secure_analyzers['default_value']).to eql(secure_analyzers_prefix) diff --git a/spec/services/snippets/update_service_spec.rb b/spec/services/snippets/update_service_spec.rb index f61d33e2436..67cc258b4b6 100644 --- a/spec/services/snippets/update_service_spec.rb +++ b/spec/services/snippets/update_service_spec.rb @@ -140,7 +140,7 @@ RSpec.describe Snippets::UpdateService do context 'when snippet_actions param is used' do let(:file_path) { 'CHANGELOG' } - let(:created_file_path) { 'New file'} + let(:created_file_path) { 'New file' } let(:content) { 'foobar' } let(:snippet_actions) { [{ action: :move, previous_path: snippet.file_name, file_path: file_path }, { action: :create, file_path: created_file_path, content: content }] } let(:base_opts) do diff --git a/spec/services/suggestions/apply_service_spec.rb b/spec/services/suggestions/apply_service_spec.rb index 6052882813e..e34324d5fe2 100644 --- a/spec/services/suggestions/apply_service_spec.rb +++ b/spec/services/suggestions/apply_service_spec.rb @@ -359,7 +359,7 @@ RSpec.describe Suggestions::ApplyService do end context 'multiple suggestions' do - let(:author_emails) { suggestions.map {|s| s.note.author.commit_email_or_default } } + let(:author_emails) { suggestions.map { |s| s.note.author.commit_email_or_default } } let(:first_author) { suggestion.note.author } let(:commit) { project.repository.commit } diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index 741d136b9a0..a192fae27db 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -134,15 +134,15 @@ RSpec.describe SystemNoteService do end end - describe '.change_due_date' do - let(:due_date) { double } + describe '.change_start_date_or_due_date' do + let(:changed_dates) { double } it 'calls TimeTrackingService' do expect_next_instance_of(::SystemNotes::TimeTrackingService) do |service| - expect(service).to receive(:change_due_date).with(due_date) + expect(service).to receive(:change_start_date_or_due_date).with(changed_dates) end - described_class.change_due_date(noteable, project, author, due_date) + described_class.change_start_date_or_due_date(noteable, project, author, changed_dates) end end @@ -159,30 +159,6 @@ RSpec.describe SystemNoteService do end end - describe '.request_attention' do - let(:user) { double } - - it 'calls IssuableService' do - expect_next_instance_of(::SystemNotes::IssuablesService) do |service| - expect(service).to receive(:request_attention).with(user) - end - - described_class.request_attention(noteable, project, author, user) - end - end - - describe '.remove_attention_request' do - let(:user) { double } - - it 'calls IssuableService' do - expect_next_instance_of(::SystemNotes::IssuablesService) do |service| - expect(service).to receive(:remove_attention_request).with(user) - end - - described_class.remove_attention_request(noteable, project, author, user) - end - end - describe '.merge_when_pipeline_succeeds' do it 'calls MergeRequestsService' do sha = double @@ -375,13 +351,14 @@ RSpec.describe SystemNoteService do describe '.noteable_cloned' do let(:noteable_ref) { double } let(:direction) { double } + let(:created_at) { double } it 'calls IssuableService' do expect_next_instance_of(::SystemNotes::IssuablesService) do |service| - expect(service).to receive(:noteable_cloned).with(noteable_ref, direction) + expect(service).to receive(:noteable_cloned).with(noteable_ref, direction, created_at: created_at) end - described_class.noteable_cloned(double, double, noteable_ref, double, direction: direction) + described_class.noteable_cloned(double, double, noteable_ref, double, direction: direction, created_at: created_at) end end @@ -431,9 +408,22 @@ RSpec.describe SystemNoteService do end end + describe '.created_timelog' do + let(:issue) { create(:issue, project: project) } + let(:timelog) { create(:timelog, user: author, issue: issue, time_spent: 1800) } + + it 'calls TimeTrackingService' do + expect_next_instance_of(::SystemNotes::TimeTrackingService) do |service| + expect(service).to receive(:created_timelog) + end + + described_class.created_timelog(noteable, project, author, timelog) + end + end + describe '.remove_timelog' do let(:issue) { create(:issue, project: project) } - let(:timelog) { create(:timelog, user: author, issue: issue, time_spent: 1800)} + let(:timelog) { create(:timelog, user: author, issue: issue, time_spent: 1800) } it 'calls TimeTrackingService' do expect_next_instance_of(::SystemNotes::TimeTrackingService) do |service| @@ -742,4 +732,38 @@ RSpec.describe SystemNoteService do described_class.delete_timeline_event(noteable, author) end end + + describe '.relate_work_item' do + let(:work_item) { double('work_item', issue_type: :task) } + let(:noteable) { double } + + before do + allow(noteable).to receive(:project).and_return(double) + end + + it 'calls IssuableService' do + expect_next_instance_of(::SystemNotes::IssuablesService) do |service| + expect(service).to receive(:hierarchy_changed).with(work_item, 'relate') + end + + described_class.relate_work_item(noteable, work_item, double) + end + end + + describe '.unrelate_wotk_item' do + let(:work_item) { double('work_item', issue_type: :task) } + let(:noteable) { double } + + before do + allow(noteable).to receive(:project).and_return(double) + end + + it 'calls IssuableService' do + expect_next_instance_of(::SystemNotes::IssuablesService) do |service| + expect(service).to receive(:hierarchy_changed).with(work_item, 'unrelate') + end + + described_class.unrelate_work_item(noteable, work_item, double) + end + end end diff --git a/spec/services/system_notes/issuables_service_spec.rb b/spec/services/system_notes/issuables_service_spec.rb index 5bc7ea82976..b2ccd9dba52 100644 --- a/spec/services/system_notes/issuables_service_spec.rb +++ b/spec/services/system_notes/issuables_service_spec.rb @@ -247,42 +247,6 @@ RSpec.describe ::SystemNotes::IssuablesService do end end - describe '#request_attention' do - subject { service.request_attention(user) } - - let(:user) { create(:user) } - - it_behaves_like 'a system note' do - let(:action) { 'attention_requested' } - end - - context 'when attention requested' do - it_behaves_like 'a note with overridable created_at' - - it 'sets the note text' do - expect(subject.note).to eq "requested attention from @#{user.username}" - end - end - end - - describe '#remove_attention_request' do - subject { service.remove_attention_request(user) } - - let(:user) { create(:user) } - - it_behaves_like 'a system note' do - let(:action) { 'attention_request_removed' } - end - - context 'when attention request is removed' do - it_behaves_like 'a note with overridable created_at' - - it 'sets the note text' do - expect(subject.note).to eq "removed attention request from @#{user.username}" - end - end - end - describe '#change_title' do let(:noteable) { create(:issue, project: project, title: 'Lorem ipsum') } @@ -559,8 +523,8 @@ RSpec.describe ::SystemNotes::IssuablesService do let(:action) { 'task' } end - it "posts the 'marked the task as complete' system note" do - expect(subject.note).to eq("marked the task **task** as completed") + it "posts the 'marked the checklist item as complete' system note" do + expect(subject.note).to eq("marked the checklist item **task** as completed") end end @@ -625,8 +589,8 @@ RSpec.describe ::SystemNotes::IssuablesService do end describe '#noteable_cloned' do - let(:new_project) { create(:project) } - let(:new_noteable) { create(:issue, project: new_project) } + let_it_be(:new_project) { create(:project) } + let_it_be(:new_noteable) { create(:issue, project: new_project) } subject do service.noteable_cloned(new_noteable, direction) @@ -684,6 +648,22 @@ RSpec.describe ::SystemNotes::IssuablesService do end end + context 'custom created timestamp' do + let(:direction) { :from } + + it 'allows setting of custom created_at value' do + timestamp = 1.day.ago + + note = service.noteable_cloned(new_noteable, direction, created_at: timestamp) + + expect(note.created_at).to be_like_time(timestamp) + end + + it 'defaults to current time when created_at is not given', :freeze_time do + expect(subject.created_at).to be_like_time(Time.current) + end + end + context 'metrics' do context 'cloned from' do let(:direction) { :from } @@ -696,15 +676,20 @@ RSpec.describe ::SystemNotes::IssuablesService do end end - context 'cloned to' do + context 'cloned to', :snowplow do let(:direction) { :to } it 'tracks usage' do expect(Gitlab::UsageDataCounters::IssueActivityUniqueCounter) - .to receive(:track_issue_cloned_action).with(author: author) + .to receive(:track_issue_cloned_action).with(author: author, project: project ) subject end + + it_behaves_like 'issue_edit snowplow tracking' do + let(:property) { Gitlab::UsageDataCounters::IssueActivityUniqueCounter::ISSUE_CLONED } + let(:user) { author } + end end end end @@ -886,4 +871,43 @@ RSpec.describe ::SystemNotes::IssuablesService do it { expect(subject.note).to eq "changed issue type to incident" } end + + describe '#hierarchy_changed' do + let_it_be_with_reload(:work_item) { create(:work_item, project: project) } + let_it_be_with_reload(:task) { create(:work_item, :task, project: project) } + + let(:service) { described_class.new(noteable: work_item, project: project, author: author) } + + subject { service.hierarchy_changed(task, hierarchy_change_action) } + + context 'when task is added as a child' do + let(:hierarchy_change_action) { 'relate' } + + it_behaves_like 'a system note' do + let(:expected_noteable) { task } + let(:action) { 'relate_to_parent' } + end + + it 'sets the correct note text' do + expect { subject }.to change { Note.system.count }.by(2) + expect(work_item.notes.last.note).to eq("added ##{task.iid} as child task") + expect(task.notes.last.note).to eq("added ##{work_item.iid} as parent issue") + end + end + + context 'when child task is removed' do + let(:hierarchy_change_action) { 'unrelate' } + + it_behaves_like 'a system note' do + let(:expected_noteable) { task } + let(:action) { 'unrelate_from_parent' } + end + + it 'sets the correct note text' do + expect { subject }.to change { Note.system.count }.by(2) + expect(work_item.notes.last.note).to eq("removed child task ##{task.iid}") + expect(task.notes.last.note).to eq("removed parent issue ##{work_item.iid}") + end + end + end end diff --git a/spec/services/system_notes/merge_requests_service_spec.rb b/spec/services/system_notes/merge_requests_service_spec.rb index 58d2489f878..3e66ccef106 100644 --- a/spec/services/system_notes/merge_requests_service_spec.rb +++ b/spec/services/system_notes/merge_requests_service_spec.rb @@ -167,8 +167,8 @@ RSpec.describe ::SystemNotes::MergeRequestsService do end describe '.change_branch' do - let(:old_branch) { 'old_branch'} - let(:new_branch) { 'new_branch'} + let(:old_branch) { 'old_branch' } + let(:new_branch) { 'new_branch' } it_behaves_like 'a system note' do let(:action) { 'branch' } diff --git a/spec/services/system_notes/time_tracking_service_spec.rb b/spec/services/system_notes/time_tracking_service_spec.rb index fdf18f4f29a..33608deaa64 100644 --- a/spec/services/system_notes/time_tracking_service_spec.rb +++ b/spec/services/system_notes/time_tracking_service_spec.rb @@ -3,35 +3,112 @@ require 'spec_helper' RSpec.describe ::SystemNotes::TimeTrackingService do - let_it_be(:author) { create(:user) } - let_it_be(:project) { create(:project, :repository) } + let_it_be(:author) { create(:user) } + let_it_be(:project) { create(:project, :repository) } - describe '#change_due_date' do - subject { described_class.new(noteable: noteable, project: project, author: author).change_due_date(due_date) } + describe '#change_start_date_or_due_date' do + let_it_be(:issue) { create(:issue, project: project) } + let_it_be(:work_item) { create(:work_item, project: project) } - let(:due_date) { Date.today } + subject(:note) { described_class.new(noteable: noteable, project: project, author: author).change_start_date_or_due_date(changed_dates) } - context 'when noteable is an issue' do - let_it_be(:noteable) { create(:issue, project: project) } + let(:start_date) { Date.today } + let(:due_date) { 1.week.from_now.to_date } + let(:changed_dates) { { 'due_date' => [nil, due_date], 'start_date' => [nil, start_date] } } + shared_examples 'issuable getting date change notes' do it_behaves_like 'a note with overridable created_at' it_behaves_like 'a system note' do - let(:action) { 'due_date' } + let(:action) { 'start_date_or_due_date' } end - context 'when due date added' do - it 'sets the note text' do - expect(subject.note).to eq "changed due date to #{due_date.to_s(:long)}" + context 'when both dates are added' do + it 'sets the correct note message' do + expect(note.note).to eq("changed start date to #{start_date.to_s(:long)} and changed due date to #{due_date.to_s(:long)}") end end - context 'when due date removed' do - let(:due_date) { nil } + context 'when both dates are removed' do + let(:changed_dates) { { 'due_date' => [due_date, nil], 'start_date' => [start_date, nil] } } - it 'sets the note text' do - expect(subject.note).to eq 'removed due date' + before do + noteable.update!(start_date: start_date, due_date: due_date) + end + + it 'sets the correct note message' do + expect(note.note).to eq('removed start date and removed due date') + end + end + + context 'when due date is added' do + let(:changed_dates) { { 'due_date' => [nil, due_date] } } + + it 'sets the correct note message' do + expect(note.note).to eq("changed due date to #{due_date.to_s(:long)}") + end + + it 'tracks the issue event in usage ping' do + expect(activity_counter_class).to receive(activity_counter_method).with(author: author) + + subject end + + context 'and start date removed' do + let(:changed_dates) { { 'due_date' => [nil, due_date], 'start_date' => [start_date, nil] } } + + it 'sets the correct note message' do + expect(note.note).to eq("removed start date and changed due date to #{due_date.to_s(:long)}") + end + end + end + + context 'when start_date is added' do + let(:changed_dates) { { 'start_date' => [nil, start_date] } } + + it 'does not track the issue event in usage ping' do + expect(Gitlab::UsageDataCounters::IssueActivityUniqueCounter).not_to receive(:track_issue_due_date_changed_action) + + subject + end + + it 'sets the correct note message' do + expect(note.note).to eq("changed start date to #{start_date.to_s(:long)}") + end + + context 'and due date removed' do + let(:changed_dates) { { 'due_date' => [due_date, nil], 'start_date' => [nil, start_date] } } + + it 'sets the correct note message' do + expect(note.note).to eq("changed start date to #{start_date.to_s(:long)} and removed due date") + end + end + end + + context 'when no dates are changed' do + let(:changed_dates) { {} } + + it 'does not create a note and returns nil' do + expect do + note + end.to not_change(Note, :count) + + expect(note).to be_nil + end + end + end + + context 'when noteable is an issue' do + let(:noteable) { issue } + let(:activity_counter_class) { Gitlab::UsageDataCounters::IssueActivityUniqueCounter } + let(:activity_counter_method) { :track_issue_due_date_changed_action } + + it_behaves_like 'issuable getting date change notes' + + it 'does not track the work item event in usage ping' do + expect(Gitlab::UsageDataCounters::WorkItemActivityUniqueCounter).not_to receive(:track_work_item_date_changed_action) + + subject end it 'tracks the issue event in usage ping' do @@ -39,13 +116,48 @@ RSpec.describe ::SystemNotes::TimeTrackingService do subject end + + context 'when only start_date is added' do + let(:changed_dates) { { 'start_date' => [nil, start_date] } } + + it 'does not track the issue event in usage ping' do + expect(activity_counter_class).not_to receive(activity_counter_method) + + subject + end + end + end + + context 'when noteable is a work item' do + let(:noteable) { work_item } + let(:activity_counter_class) { Gitlab::UsageDataCounters::WorkItemActivityUniqueCounter } + let(:activity_counter_method) { :track_work_item_date_changed_action } + + it_behaves_like 'issuable getting date change notes' + + it 'does not track the issue event in usage ping' do + expect(Gitlab::UsageDataCounters::IssueActivityUniqueCounter).not_to receive(:track_issue_due_date_changed_action) + + subject + end + + context 'when only start_date is added' do + let(:changed_dates) { { 'start_date' => [nil, start_date] } } + + it 'tracks the issue event in usage ping' do + expect(activity_counter_class).to receive(activity_counter_method).with(author: author) + + subject + end + end end context 'when noteable is a merge request' do - let_it_be(:noteable) { create(:merge_request, source_project: project) } + let(:noteable) { create(:merge_request, source_project: project) } it 'does not track the issue event in usage ping' do - expect(Gitlab::UsageDataCounters::IssueActivityUniqueCounter).not_to receive(:track_issue_due_date_changed_action).with(author: author) + expect(Gitlab::UsageDataCounters::IssueActivityUniqueCounter).not_to receive(:track_issue_due_date_changed_action) + expect(Gitlab::UsageDataCounters::WorkItemActivityUniqueCounter).not_to receive(:track_work_item_date_changed_action) subject end @@ -106,13 +218,37 @@ RSpec.describe ::SystemNotes::TimeTrackingService do end end + describe '#create_timelog' do + subject { described_class.new(noteable: noteable, project: project, author: author).created_timelog(timelog) } + + context 'when the timelog has a positive time spent value' do + let_it_be(:noteable, reload: true) { create(:issue, project: project) } + + let(:timelog) { create(:timelog, user: author, issue: noteable, time_spent: 1800, spent_at: '2022-03-30T00:00:00.000Z') } + + it 'sets the note text' do + expect(subject.note).to eq "added 30m of time spent at 2022-03-30" + end + end + + context 'when the timelog has a negative time spent value' do + let_it_be(:noteable, reload: true) { create(:issue, project: project) } + + let(:timelog) { create(:timelog, user: author, issue: noteable, time_spent: -1800, spent_at: '2022-03-30T00:00:00.000Z') } + + it 'sets the note text' do + expect(subject.note).to eq "subtracted 30m of time spent at 2022-03-30" + end + end + end + describe '#remove_timelog' do subject { described_class.new(noteable: noteable, project: project, author: author).remove_timelog(timelog) } context 'when the timelog has a positive time spent value' do let_it_be(:noteable, reload: true) { create(:issue, project: project) } - let(:timelog) { create(:timelog, user: author, issue: noteable, time_spent: 1800, spent_at: '2022-03-30T00:00:00.000Z')} + let(:timelog) { create(:timelog, user: author, issue: noteable, time_spent: 1800, spent_at: '2022-03-30T00:00:00.000Z') } it 'sets the note text' do expect(subject.note).to eq "deleted 30m of spent time from 2022-03-30" @@ -122,7 +258,7 @@ RSpec.describe ::SystemNotes::TimeTrackingService do context 'when the timelog has a negative time spent value' do let_it_be(:noteable, reload: true) { create(:issue, project: project) } - let(:timelog) { create(:timelog, user: author, issue: noteable, time_spent: -1800, spent_at: '2022-03-30T00:00:00.000Z')} + let(:timelog) { create(:timelog, user: author, issue: noteable, time_spent: -1800, spent_at: '2022-03-30T00:00:00.000Z') } it 'sets the note text' do expect(subject.note).to eq "deleted -30m of spent time from 2022-03-30" diff --git a/spec/services/terraform/remote_state_handler_spec.rb b/spec/services/terraform/remote_state_handler_spec.rb index 19c1d4109e9..369309e4d5a 100644 --- a/spec/services/terraform/remote_state_handler_spec.rb +++ b/spec/services/terraform/remote_state_handler_spec.rb @@ -171,7 +171,7 @@ RSpec.describe Terraform::RemoteStateHandler do end context 'with no lock ID (force-unlock)' do - let(:lock_id) { } + let(:lock_id) {} it 'unlocks the state' do state = handler.unlock! diff --git a/spec/services/timelogs/create_service_spec.rb b/spec/services/timelogs/create_service_spec.rb new file mode 100644 index 00000000000..b5ed4a005c7 --- /dev/null +++ b/spec/services/timelogs/create_service_spec.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Timelogs::CreateService do + let_it_be(:author) { create(:user) } + let_it_be(:project) { create(:project, :public) } + let_it_be(:time_spent) { 3600 } + let_it_be(:spent_at) { "2022-07-08" } + let_it_be(:summary) { "Test summary" } + + let(:issuable) { nil } + let(:users_container) { project } + let(:service) { described_class.new(issuable, time_spent, spent_at, summary, user) } + + describe '#execute' do + subject { service.execute } + + context 'when issuable is an Issue' do + let_it_be(:issuable) { create(:issue, project: project) } + let_it_be(:note_noteable) { create(:issue, project: project) } + + it_behaves_like 'issuable supports timelog creation service' + end + + context 'when issuable is a MergeRequest' do + let_it_be(:issuable) { create(:merge_request, source_project: project, source_branch: 'branch-1') } + let_it_be(:note_noteable) { create(:merge_request, source_project: project, source_branch: 'branch-2') } + + it_behaves_like 'issuable supports timelog creation service' + end + + context 'when issuable is a WorkItem' do + let_it_be(:issuable) { create(:work_item, project: project, title: 'WorkItem-1') } + let_it_be(:note_noteable) { create(:work_item, project: project, title: 'WorkItem-2') } + + it_behaves_like 'issuable supports timelog creation service' + end + + context 'when issuable is an Incident' do + let_it_be(:issuable) { create(:incident, project: project) } + let_it_be(:note_noteable) { create(:incident, project: project) } + + it_behaves_like 'issuable supports timelog creation service' + end + end +end diff --git a/spec/services/timelogs/delete_service_spec.rb b/spec/services/timelogs/delete_service_spec.rb index c52cebdc5bf..ee1133af6b3 100644 --- a/spec/services/timelogs/delete_service_spec.rb +++ b/spec/services/timelogs/delete_service_spec.rb @@ -6,7 +6,7 @@ RSpec.describe Timelogs::DeleteService do let_it_be(:author) { create(:user) } let_it_be(:project) { create(:project, :public) } let_it_be(:issue) { create(:issue, project: project) } - let_it_be(:timelog) { create(:timelog, user: author, issue: issue, time_spent: 1800)} + let_it_be(:timelog) { create(:timelog, user: author, issue: issue, time_spent: 1800) } let(:service) { described_class.new(timelog, user) } @@ -21,8 +21,8 @@ RSpec.describe Timelogs::DeleteService do end it 'returns the removed timelog' do - expect(subject).to be_success - expect(subject.payload).to eq(timelog) + is_expected.to be_success + expect(subject.payload[:timelog]).to eq(timelog) end end @@ -31,7 +31,7 @@ RSpec.describe Timelogs::DeleteService do let!(:timelog) { nil } it 'returns an error' do - expect(subject).to be_error + is_expected.to be_error expect(subject.message).to eq('Timelog doesn\'t exist or you don\'t have permission to delete it') expect(subject.http_status).to eq(404) end @@ -41,7 +41,7 @@ RSpec.describe Timelogs::DeleteService do let(:user) { create(:user) } it 'returns an error' do - expect(subject).to be_error + is_expected.to be_error expect(subject.message).to eq('Timelog doesn\'t exist or you don\'t have permission to delete it') expect(subject.http_status).to eq(404) end @@ -49,14 +49,14 @@ RSpec.describe Timelogs::DeleteService do context 'when the timelog deletion fails' do let(:user) { author } - let!(:timelog) { create(:timelog, user: author, issue: issue, time_spent: 1800)} + let!(:timelog) { create(:timelog, user: author, issue: issue, time_spent: 1800) } before do allow(timelog).to receive(:destroy).and_return(false) end it 'returns an error' do - expect(subject).to be_error + is_expected.to be_error expect(subject.message).to eq('Failed to remove timelog') expect(subject.http_status).to eq(400) end diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb index 1cb44366457..45a8268043f 100644 --- a/spec/services/todo_service_spec.rb +++ b/spec/services/todo_service_spec.rb @@ -207,7 +207,7 @@ RSpec.describe TodoService do end it_behaves_like 'an incident management tracked event', :incident_management_incident_todo do - let(:current_user) { john_doe} + let(:current_user) { john_doe } end end end @@ -1139,7 +1139,7 @@ RSpec.describe TodoService do it 'updates related todos for the user with the new_state' do method_call - expect(collection.all? { |todo| todo.reload.state?(new_state)}).to be_truthy + expect(collection.all? { |todo| todo.reload.state?(new_state) }).to be_truthy end if new_resolved_by @@ -1250,17 +1250,6 @@ RSpec.describe TodoService do end end - describe '#create_attention_requested_todo' do - let(:target) { create(:merge_request, author: author, source_project: project) } - let(:user) { create(:user) } - - it 'creates a todo for user' do - service.create_attention_requested_todo(target, author, user) - - should_create_todo(user: user, target: target, action: Todo::ATTENTION_REQUESTED) - end - end - def should_create_todo(attributes = {}) attributes.reverse_merge!( project: project, diff --git a/spec/services/todos/destroy/design_service_spec.rb b/spec/services/todos/destroy/design_service_spec.rb index 61a6718dc9d..92b25d94dc6 100644 --- a/spec/services/todos/destroy/design_service_spec.rb +++ b/spec/services/todos/destroy/design_service_spec.rb @@ -9,8 +9,8 @@ RSpec.describe Todos::Destroy::DesignService do let_it_be(:design_2) { create(:design) } let_it_be(:design_3) { create(:design) } - let_it_be(:create_action) { create(:design_action, design: design)} - let_it_be(:create_action_2) { create(:design_action, design: design_2)} + let_it_be(:create_action) { create(:design_action, design: design) } + let_it_be(:create_action_2) { create(:design_action, design: design_2) } describe '#execute' do before do @@ -23,8 +23,8 @@ RSpec.describe Todos::Destroy::DesignService do subject { described_class.new([design.id, design_2.id, design_3.id]).execute } context 'when the design has been archived' do - let_it_be(:archive_action) { create(:design_action, design: design, event: :deletion)} - let_it_be(:archive_action_2) { create(:design_action, design: design_3, event: :deletion)} + let_it_be(:archive_action) { create(:design_action, design: design, event: :deletion) } + let_it_be(:archive_action_2) { create(:design_action, design: design_3, event: :deletion) } it 'removes todos for that design' do expect { subject }.to change { Todo.count }.from(4).to(1) diff --git a/spec/services/todos/destroy/destroyed_issuable_service_spec.rb b/spec/services/todos/destroy/destroyed_issuable_service_spec.rb index 24f74bae7c8..6d6abe06d1c 100644 --- a/spec/services/todos/destroy/destroyed_issuable_service_spec.rb +++ b/spec/services/todos/destroy/destroyed_issuable_service_spec.rb @@ -4,31 +4,46 @@ require 'spec_helper' RSpec.describe Todos::Destroy::DestroyedIssuableService do describe '#execute' do - let_it_be(:target) { create(:merge_request) } - let_it_be(:pending_todo) { create(:todo, :pending, project: target.project, target: target, user: create(:user)) } - let_it_be(:done_todo) { create(:todo, :done, project: target.project, target: target, user: create(:user)) } + let_it_be(:user) { create(:user) } - def execute - described_class.new(target.id, target.class.name).execute - end + subject { described_class.new(target.id, target.class.name).execute } + + context 'when target is merge request' do + let_it_be(:target) { create(:merge_request) } + let_it_be(:pending_todo) { create(:todo, :pending, project: target.project, target: target, user: user) } + let_it_be(:done_todo) { create(:todo, :done, project: target.project, target: target, user: user) } - it 'deletes todos for specified target ID and type' do - control_count = ActiveRecord::QueryRecorder.new { execute }.count + it 'deletes todos for specified target ID and type' do + control_count = ActiveRecord::QueryRecorder.new { subject }.count - # Create more todos for the target - create(:todo, :pending, project: target.project, target: target, user: create(:user)) - create(:todo, :pending, project: target.project, target: target, user: create(:user)) - create(:todo, :done, project: target.project, target: target, user: create(:user)) - create(:todo, :done, project: target.project, target: target, user: create(:user)) + # Create more todos for the target + create(:todo, :pending, project: target.project, target: target, user: user) + create(:todo, :pending, project: target.project, target: target, user: user) + create(:todo, :done, project: target.project, target: target, user: user) + create(:todo, :done, project: target.project, target: target, user: user) - expect { execute }.not_to exceed_query_limit(control_count) - expect(target.reload.todos.count).to eq(0) + expect { subject }.not_to exceed_query_limit(control_count) + end + + it 'invalidates todos cache counts of todo users', :use_clean_rails_redis_caching do + expect { subject } + .to change { pending_todo.user.todos_pending_count }.from(1).to(0) + .and change { done_todo.user.todos_done_count }.from(1).to(0) + end end - it 'invalidates todos cache counts of todo users', :use_clean_rails_redis_caching do - expect { execute } - .to change { pending_todo.user.todos_pending_count }.from(1).to(0) - .and change { done_todo.user.todos_done_count }.from(1).to(0) + context 'when target is an work item' do + let_it_be(:target) { create(:work_item) } + let_it_be(:todo1) { create(:todo, :pending, project: target.project, target: target, user: user) } + let_it_be(:todo2) { create(:todo, :done, project: target.project, target: target, user: user) } + # rubocop: disable Cop/AvoidBecomes + let_it_be(:todo3) { create(:todo, :pending, project: target.project, target: target.becomes(Issue), user: user) } + let_it_be(:todo4) { create(:todo, :done, project: target.project, target: target.becomes(Issue), user: user) } + # rubocop: enable Cop/AvoidBecomes + + it 'deletes todos' do + expect { subject }.to change(Todo, :count).by(-4) + end end end end diff --git a/spec/services/topics/merge_service_spec.rb b/spec/services/topics/merge_service_spec.rb new file mode 100644 index 00000000000..971917eb8e9 --- /dev/null +++ b/spec/services/topics/merge_service_spec.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Topics::MergeService do + let_it_be(:source_topic) { create(:topic, name: 'source_topic') } + let_it_be(:target_topic) { create(:topic, name: 'target_topic') } + let_it_be(:project_1) { create(:project, :public, topic_list: source_topic.name ) } + let_it_be(:project_2) { create(:project, :private, topic_list: source_topic.name ) } + let_it_be(:project_3) { create(:project, :public, topic_list: target_topic.name ) } + let_it_be(:project_4) { create(:project, :public, topic_list: [source_topic.name, target_topic.name] ) } + + subject { described_class.new(source_topic, target_topic).execute } + + describe '#execute' do + it 'merges source topic into target topic' do + subject + + expect(target_topic.projects).to contain_exactly(project_1, project_2, project_3, project_4) + expect { source_topic.reload }.to raise_error(ActiveRecord::RecordNotFound) + end + + it 'refreshes counters of target topic' do + expect { subject } + .to change { target_topic.reload.total_projects_count }.by(2) + .and change { target_topic.reload.non_private_projects_count }.by(1) + end + + context 'when source topic fails to delete' do + it 'reverts previous changes' do + allow(source_topic.reload).to receive(:destroy!).and_raise(ActiveRecord::RecordNotDestroyed) + + expect { subject }.to raise_error(ActiveRecord::RecordNotDestroyed) + + expect(source_topic.projects).to contain_exactly(project_1, project_2, project_4) + expect(target_topic.projects).to contain_exactly(project_3, project_4) + end + end + + context 'for parameter validation' do + using RSpec::Parameterized::TableSyntax + + subject { described_class.new(source_topic_parameter, target_topic_parameter).execute } + + where(:source_topic_parameter, :target_topic_parameter, :expected_message) do + nil | ref(:target_topic) | 'The source topic is not a topic.' + ref(:source_topic) | nil | 'The target topic is not a topic.' + ref(:target_topic) | ref(:target_topic) | 'The source topic and the target topic are identical.' # rubocop:disable Lint/BinaryOperatorWithIdenticalOperands + end + + with_them do + it 'raises correct error' do + expect { subject }.to raise_error(ArgumentError) do |error| + expect(error.message).to eq(expected_message) + end + end + end + end + end +end diff --git a/spec/services/uploads/destroy_service_spec.rb b/spec/services/uploads/destroy_service_spec.rb new file mode 100644 index 00000000000..bb58da231b6 --- /dev/null +++ b/spec/services/uploads/destroy_service_spec.rb @@ -0,0 +1,103 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Uploads::DestroyService do + let_it_be(:project) { create(:project) } + let_it_be(:user) { create(:user) } + let_it_be_with_reload(:upload) { create(:upload, :issuable_upload, model: project) } + + let(:filename) { File.basename(upload.path) } + let(:secret) { upload.secret } + let(:model) { project } + let(:service) { described_class.new(model, user) } + + describe '#execute' do + subject { service.execute(secret, filename) } + + shared_examples_for 'upload not found' do + it 'does not delete any upload' do + expect { subject }.not_to change { Upload.count } + end + + it 'returns an error' do + expect(subject[:status]).to eq(:error) + expect(subject[:message]).to eq("The resource that you are attempting to access does not "\ + "exist or you don't have permission to perform this action.") + end + end + + context 'when user is nil' do + let(:user) { nil } + + it_behaves_like 'upload not found' + end + + context 'when user cannot destroy upload' do + before do + project.add_developer(user) + end + + it_behaves_like 'upload not found' + end + + context 'when user can destroy upload' do + before do + project.add_maintainer(user) + end + + it 'deletes the upload' do + expect { subject }.to change { Upload.count }.by(-1) + end + + it 'returns success response' do + expect(subject[:status]).to eq(:success) + expect(subject[:upload]).to eq(upload) + end + + context 'when upload is not found' do + let(:filename) { 'not existing filename' } + + it_behaves_like 'upload not found' + end + + context 'when upload secret is not found' do + let(:secret) { 'aaaaaaaaaa' } + + it_behaves_like 'upload not found' + end + + context 'when upload secret has invalid format' do + let(:secret) { 'invalid' } + + it_behaves_like 'upload not found' + end + + context 'when unknown model is used' do + let(:model) { user } + + it 'raises an error' do + expect { subject }.to raise_exception(ArgumentError) + end + end + + context 'when upload belongs to other model' do + let_it_be(:upload) { create(:upload, :namespace_upload) } + + it_behaves_like 'upload not found' + end + + context 'when upload destroy fails' do + before do + allow(service).to receive(:find_upload).and_return(upload) + allow(upload).to receive(:destroy).and_return(false) + end + + it 'returns error' do + expect(subject[:status]).to eq(:error) + expect(subject[:message]).to eq('Upload could not be deleted.') + end + end + end + end +end diff --git a/spec/services/users/create_service_spec.rb b/spec/services/users/create_service_spec.rb index 74340bac055..f3c9701c556 100644 --- a/spec/services/users/create_service_spec.rb +++ b/spec/services/users/create_service_spec.rb @@ -4,6 +4,7 @@ require 'spec_helper' RSpec.describe Users::CreateService do describe '#execute' do + let(:password) { User.random_password } let(:admin_user) { create(:admin) } context 'with an admin user' do @@ -12,7 +13,7 @@ RSpec.describe Users::CreateService do context 'when required parameters are provided' do let(:params) do - { name: 'John Doe', username: 'jduser', email: email, password: 'mydummypass' } + { name: 'John Doe', username: 'jduser', email: email, password: password } end it 'returns a persisted user' do @@ -82,13 +83,13 @@ RSpec.describe Users::CreateService do context 'when force_random_password parameter is true' do let(:params) do - { name: 'John Doe', username: 'jduser', email: 'jd@example.com', password: 'mydummypass', force_random_password: true } + { name: 'John Doe', username: 'jduser', email: 'jd@example.com', password: password, force_random_password: true } end it 'generates random password' do user = service.execute - expect(user.password).not_to eq 'mydummypass' + expect(user.password).not_to eq password expect(user.password).to be_present end end @@ -99,7 +100,7 @@ RSpec.describe Users::CreateService do name: 'John Doe', username: 'jduser', email: 'jd@example.com', - password: 'mydummypass', + password: password, password_automatically_set: true } end @@ -121,7 +122,7 @@ RSpec.describe Users::CreateService do context 'when skip_confirmation parameter is true' do let(:params) do - { name: 'John Doe', username: 'jduser', email: 'jd@example.com', password: 'mydummypass', skip_confirmation: true } + { name: 'John Doe', username: 'jduser', email: 'jd@example.com', password: password, skip_confirmation: true } end it 'confirms the user' do @@ -131,7 +132,7 @@ RSpec.describe Users::CreateService do context 'when reset_password parameter is true' do let(:params) do - { name: 'John Doe', username: 'jduser', email: 'jd@example.com', password: 'mydummypass', reset_password: true } + { name: 'John Doe', username: 'jduser', email: 'jd@example.com', password: password, reset_password: true } end it 'resets password even if a password parameter is given' do @@ -152,7 +153,7 @@ RSpec.describe Users::CreateService do context 'with nil user' do let(:params) do - { name: 'John Doe', username: 'jduser', email: 'jd@example.com', password: 'mydummypass', skip_confirmation: true } + { name: 'John Doe', username: 'jduser', email: 'jd@example.com', password: password, skip_confirmation: true } end let(:service) { described_class.new(nil, params) } diff --git a/spec/services/users/dismiss_namespace_callout_service_spec.rb b/spec/services/users/dismiss_namespace_callout_service_spec.rb new file mode 100644 index 00000000000..fbcdb66c9e8 --- /dev/null +++ b/spec/services/users/dismiss_namespace_callout_service_spec.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Users::DismissNamespaceCalloutService do + describe '#execute' do + let_it_be(:user) { create(:user) } + + let(:params) { { feature_name: feature_name, namespace_id: user.namespace.id } } + let(:feature_name) { Users::NamespaceCallout.feature_names.each_key.first } + + subject(:execute) do + described_class.new( + container: nil, current_user: user, params: params + ).execute + end + + it_behaves_like 'dismissing user callout', Users::NamespaceCallout + + it 'sets the namespace_id' do + expect(execute.namespace_id).to eq(user.namespace.id) + end + end +end diff --git a/spec/services/users/dismiss_project_callout_service_spec.rb b/spec/services/users/dismiss_project_callout_service_spec.rb new file mode 100644 index 00000000000..73e50a4c37d --- /dev/null +++ b/spec/services/users/dismiss_project_callout_service_spec.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Users::DismissProjectCalloutService do + describe '#execute' do + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project) } + + let(:params) { { feature_name: feature_name, project_id: project.id } } + let(:feature_name) { Users::ProjectCallout.feature_names.each_key.first } + + subject(:execute) do + described_class.new( + container: nil, current_user: user, params: params + ).execute + end + + it_behaves_like 'dismissing user callout', Users::ProjectCallout + + it 'sets the project_id' do + expect(execute.project_id).to eq(project.id) + end + end +end diff --git a/spec/services/users/update_service_spec.rb b/spec/services/users/update_service_spec.rb index 52c7b54ed72..411cd7316d8 100644 --- a/spec/services/users/update_service_spec.rb +++ b/spec/services/users/update_service_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' RSpec.describe Users::UpdateService do - let(:password) { 'longsecret987!' } + let(:password) { User.random_password } let(:user) { create(:user, password: password, password_confirmation: password) } describe '#execute' do diff --git a/spec/services/web_hook_service_spec.rb b/spec/services/web_hook_service_spec.rb index 339ffc44e4d..fed3ae7a543 100644 --- a/spec/services/web_hook_service_spec.rb +++ b/spec/services/web_hook_service_spec.rb @@ -190,7 +190,7 @@ RSpec.describe WebHookService, :request_store, :clean_gitlab_redis_shared_state end context 'when auth credentials are present' do - let_it_be(:url) {'https://example.org'} + let_it_be(:url) { 'https://example.org' } let_it_be(:project_hook) { create(:project_hook, url: 'https://demo:demo@example.org/') } it 'uses the credentials' do @@ -205,7 +205,7 @@ RSpec.describe WebHookService, :request_store, :clean_gitlab_redis_shared_state end context 'when auth credentials are partial present' do - let_it_be(:url) {'https://example.org'} + let_it_be(:url) { 'https://example.org' } let_it_be(:project_hook) { create(:project_hook, url: 'https://demo@example.org/') } it 'uses the credentials anyways' do diff --git a/spec/services/web_hooks/destroy_service_spec.rb b/spec/services/web_hooks/destroy_service_spec.rb index 4d9bb18e540..ca8cb8a1b75 100644 --- a/spec/services/web_hooks/destroy_service_spec.rb +++ b/spec/services/web_hooks/destroy_service_spec.rb @@ -8,43 +8,54 @@ RSpec.describe WebHooks::DestroyService do subject { described_class.new(user) } describe '#execute' do - %i[system_hook project_hook].each do |factory| - context "deleting a #{factory}" do - let!(:hook) { create(factory) } # rubocop: disable Rails/SaveBang (false-positive!) - let!(:log) { create_list(:web_hook_log, 3, web_hook: hook) } + # Testing with a project hook only - for permission tests, see policy specs. + let!(:hook) { create(:project_hook) } + let!(:log) { create_list(:web_hook_log, 3, web_hook: hook) } + + context 'when the user does not have permission' do + it 'is an error' do + expect(subject.execute(hook)) + .to be_error + .and have_attributes(message: described_class::DENIED) + end + end - it 'is successful' do - expect(subject.execute(hook)).to be_success - end + context 'when the user does have permission' do + before do + hook.project.add_maintainer(user) + end - it 'destroys the hook' do - expect { subject.execute(hook) }.to change(WebHook, :count).from(1).to(0) - end + it 'is successful' do + expect(subject.execute(hook)).to be_success + end - it 'does not destroy logs' do - expect { subject.execute(hook) }.not_to change(WebHookLog, :count) - end + it 'destroys the hook' do + expect { subject.execute(hook) }.to change(WebHook, :count).from(1).to(0) + end - it 'schedules the destruction of logs' do - expect(WebHooks::LogDestroyWorker).to receive(:perform_async).with({ 'hook_id' => hook.id }) - expect(Gitlab::AppLogger).to receive(:info).with(match(/scheduled a deletion of logs/)) + it 'does not destroy logs' do + expect { subject.execute(hook) }.not_to change(WebHookLog, :count) + end - subject.execute(hook) - end + it 'schedules the destruction of logs' do + expect(WebHooks::LogDestroyWorker).to receive(:perform_async).with({ 'hook_id' => hook.id }) + expect(Gitlab::AppLogger).to receive(:info).with(match(/scheduled a deletion of logs/)) - context 'when the hook fails to destroy' do - before do - allow(hook).to receive(:destroy).and_return(false) - end + subject.execute(hook) + end + + context 'when the hook fails to destroy' do + before do + allow(hook).to receive(:destroy).and_return(false) + end - it 'is not a success' do - expect(WebHooks::LogDestroyWorker).not_to receive(:perform_async) + it 'is not a success' do + expect(WebHooks::LogDestroyWorker).not_to receive(:perform_async) - r = subject.execute(hook) + r = subject.execute(hook) - expect(r).to be_error - expect(r[:message]).to match %r{Unable to destroy} - end + expect(r).to be_error + expect(r[:message]).to match %r{Unable to destroy} end end end diff --git a/spec/services/web_hooks/log_execution_service_spec.rb b/spec/services/web_hooks/log_execution_service_spec.rb index 873f6adc8dc..1967a8368fb 100644 --- a/spec/services/web_hooks/log_execution_service_spec.rb +++ b/spec/services/web_hooks/log_execution_service_spec.rb @@ -101,27 +101,6 @@ RSpec.describe WebHooks::LogExecutionService do it 'resets the failure count' do expect { service.execute }.to change(project_hook, :recent_failures).to(0) end - - it 'sends a message to AuthLogger if the hook as not previously enabled' do - project_hook.update!(recent_failures: ::WebHook::FAILURE_THRESHOLD + 1) - - expect(Gitlab::AuthLogger).to receive(:info).with include( - message: 'WebHook change active_state', - # identification - hook_id: project_hook.id, - hook_type: project_hook.type, - project_id: project_hook.project_id, - group_id: nil, - # relevant data - prev_state: :permanently_disabled, - new_state: :enabled, - duration: 1.2, - response_status: '200', - recent_hook_failures: 0 - ) - - service.execute - end end end @@ -158,27 +137,6 @@ RSpec.describe WebHooks::LogExecutionService do expect { service.execute }.not_to change(project_hook, :recent_failures) end end - - it 'sends a message to AuthLogger if the state would change' do - project_hook.update!(recent_failures: ::WebHook::FAILURE_THRESHOLD) - - expect(Gitlab::AuthLogger).to receive(:info).with include( - message: 'WebHook change active_state', - # identification - hook_id: project_hook.id, - hook_type: project_hook.type, - project_id: project_hook.project_id, - group_id: nil, - # relevant data - prev_state: :enabled, - new_state: :permanently_disabled, - duration: (be > 0), - response_status: data[:response_status], - recent_hook_failures: ::WebHook::FAILURE_THRESHOLD + 1 - ) - - service.execute - end end context 'when response_category is :error' do @@ -200,25 +158,6 @@ RSpec.describe WebHooks::LogExecutionService do expect { service.execute }.to change(project_hook, :backoff_count).by(1) end - it 'sends a message to AuthLogger if the state would change' do - expect(Gitlab::AuthLogger).to receive(:info).with include( - message: 'WebHook change active_state', - # identification - hook_id: project_hook.id, - hook_type: project_hook.type, - project_id: project_hook.project_id, - group_id: nil, - # relevant data - prev_state: :enabled, - new_state: :temporarily_disabled, - duration: (be > 0), - response_status: data[:response_status], - recent_hook_failures: 0 - ) - - service.execute - end - context 'when the previous cool-off was near the maximum' do before do project_hook.update!(disabled_until: 5.minutes.ago, backoff_count: 8) diff --git a/spec/services/webauthn/authenticate_service_spec.rb b/spec/services/webauthn/authenticate_service_spec.rb index 61f64f24f5e..b40f9465b63 100644 --- a/spec/services/webauthn/authenticate_service_spec.rb +++ b/spec/services/webauthn/authenticate_service_spec.rb @@ -30,19 +30,28 @@ RSpec.describe Webauthn::AuthenticateService do get_result['clientExtensionResults'] = {} service = Webauthn::AuthenticateService.new(user, get_result.to_json, challenge) - expect(service.execute).to be_truthy + expect(service.execute).to eq true end - it 'returns false if the response is valid but no matching stored credential is present' do - other_client = WebAuthn::FakeClient.new(origin) - other_client.create(challenge: challenge) # rubocop:disable Rails/SaveBang + context 'when response is valid but no matching stored credential is present' do + it 'returns false' do + other_client = WebAuthn::FakeClient.new(origin) + other_client.create(challenge: challenge) # rubocop:disable Rails/SaveBang - get_result = other_client.get(challenge: challenge) + get_result = other_client.get(challenge: challenge) - get_result['clientExtensionResults'] = {} - service = Webauthn::AuthenticateService.new(user, get_result.to_json, challenge) + get_result['clientExtensionResults'] = {} + service = Webauthn::AuthenticateService.new(user, get_result.to_json, challenge) + + expect(service.execute).to eq false + end + end - expect(service.execute).to be_falsey + context 'when device response includes invalid json' do + it 'returns false' do + service = Webauthn::AuthenticateService.new(user, 'invalid JSON', '') + expect(service.execute).to eq false + end end end end diff --git a/spec/services/work_items/create_and_link_service_spec.rb b/spec/services/work_items/create_and_link_service_spec.rb index 81be15f9e2f..e259a22d388 100644 --- a/spec/services/work_items/create_and_link_service_spec.rb +++ b/spec/services/work_items/create_and_link_service_spec.rb @@ -6,7 +6,7 @@ RSpec.describe WorkItems::CreateAndLinkService do let_it_be(:group) { create(:group) } let_it_be(:project) { create(:project, group: group) } let_it_be(:user) { create(:user) } - let_it_be(:related_work_item) { create(:work_item, project: project) } + let_it_be(:related_work_item, refind: true) { create(:work_item, project: project) } let_it_be(:invalid_parent) { create(:work_item, :task, project: project) } let(:spam_params) { double } @@ -24,6 +24,26 @@ RSpec.describe WorkItems::CreateAndLinkService do project.add_developer(user) end + shared_examples 'successful work item and link creator' do + it 'creates a work item successfully with links' do + expect do + service_result + end.to change(WorkItem, :count).by(1).and( + change(WorkItems::ParentLink, :count).by(1) + ) + end + + it 'copies confidential status from the parent' do + expect do + service_result + end.to change(WorkItem, :count).by(1) + + created_task = WorkItem.last + + expect(created_task.confidential).to eq(related_work_item.confidential) + end + end + describe '#execute' do subject(:service_result) { described_class.new(project: project, current_user: user, params: params, spam_params: spam_params, link_params: link_params).execute } @@ -42,15 +62,21 @@ RSpec.describe WorkItems::CreateAndLinkService do ) end + it_behaves_like 'title with extra spaces' + context 'when link params are valid' do let(:link_params) { { parent_work_item: related_work_item } } - it 'creates a work item successfully with links' do - expect do - service_result - end.to change(WorkItem, :count).by(1).and( - change(WorkItems::ParentLink, :count).by(1) - ) + context 'when parent is not confidential' do + it_behaves_like 'successful work item and link creator' + end + + context 'when parent is confidential' do + before do + related_work_item.update!(confidential: true) + end + + it_behaves_like 'successful work item and link creator' end end diff --git a/spec/services/work_items/create_from_task_service_spec.rb b/spec/services/work_items/create_from_task_service_spec.rb index 7d2dab228b1..7c5430f038c 100644 --- a/spec/services/work_items/create_from_task_service_spec.rb +++ b/spec/services/work_items/create_from_task_service_spec.rb @@ -64,6 +64,8 @@ RSpec.describe WorkItems::CreateFromTaskService do expect(list_work_item.description).to eq("- [ ] #{created_work_item.to_reference}+") end + + it_behaves_like 'title with extra spaces' end context 'when last operation fails' do diff --git a/spec/services/work_items/create_service_spec.rb b/spec/services/work_items/create_service_spec.rb index 4009c85bacd..c0bcf9b606d 100644 --- a/spec/services/work_items/create_service_spec.rb +++ b/spec/services/work_items/create_service_spec.rb @@ -65,6 +65,12 @@ RSpec.describe WorkItems::CreateService do expect(work_item.description).to eq('please fix') expect(work_item.work_item_type.base_type).to eq('issue') end + + it 'calls NewIssueWorker with correct arguments' do + expect(NewIssueWorker).to receive(:perform_async).with(Integer, current_user.id, 'WorkItem') + + service_result + end end context 'when params are invalid' do @@ -170,7 +176,7 @@ RSpec.describe WorkItems::CreateService do let_it_be(:parent) { create(:work_item, :task, project: project) } it_behaves_like 'fails creating work item and returns errors' do - let(:error_message) { 'only Issue and Incident can be parent of Task.'} + let(:error_message) { 'only Issue and Incident can be parent of Task.' } end end @@ -197,7 +203,7 @@ RSpec.describe WorkItems::CreateService do end it_behaves_like 'fails creating work item and returns errors' do - let(:error_message) { 'No matching task found. Make sure that you are adding a valid task ID.'} + let(:error_message) { 'No matching task found. Make sure that you are adding a valid task ID.' } end end end diff --git a/spec/services/work_items/parent_links/create_service_spec.rb b/spec/services/work_items/parent_links/create_service_spec.rb index 85b0ee040cd..0ba41373544 100644 --- a/spec/services/work_items/parent_links/create_service_spec.rb +++ b/spec/services/work_items/parent_links/create_service_spec.rb @@ -12,10 +12,10 @@ RSpec.describe WorkItems::ParentLinks::CreateService do let_it_be(:task1) { create(:work_item, :task, project: project) } let_it_be(:task2) { create(:work_item, :task, project: project) } let_it_be(:guest_task) { create(:work_item, :task) } - let_it_be(:invalid_task) { build_stubbed(:work_item, :task, id: non_existing_record_id)} + let_it_be(:invalid_task) { build_stubbed(:work_item, :task, id: non_existing_record_id) } let_it_be(:another_project) { (create :project) } let_it_be(:other_project_task) { create(:work_item, :task, iid: 100, project: another_project) } - let_it_be(:existing_parent_link) { create(:parent_link, work_item: task, work_item_parent: work_item)} + let_it_be(:existing_parent_link) { create(:parent_link, work_item: task, work_item_parent: work_item) } let(:parent_link_class) { WorkItems::ParentLink } let(:issuable_type) { :task } @@ -84,13 +84,26 @@ RSpec.describe WorkItems::ParentLinks::CreateService do expect(subject[:created_references].map(&:work_item_id)).to match_array([task1.id, task2.id]) end + it 'creates notes', :aggregate_failures do + subject + + work_item_notes = work_item.notes.last(2) + expect(work_item_notes.first.note).to eq("added #{task1.to_reference} as child task") + expect(work_item_notes.last.note).to eq("added #{task2.to_reference} as child task") + expect(task1.notes.last.note).to eq("added #{work_item.to_reference} as parent issue") + expect(task2.notes.last.note).to eq("added #{work_item.to_reference} as parent issue") + end + context 'when task is already assigned' do let(:params) { { issuable_references: [task, task2] } } - it 'creates links only for non related tasks' do + it 'creates links only for non related tasks', :aggregate_failures do expect { subject }.to change(parent_link_class, :count).by(1) expect(subject[:created_references].map(&:work_item_id)).to match_array([task2.id]) + expect(work_item.notes.last.note).to eq("added #{task2.to_reference} as child task") + expect(task2.notes.last.note).to eq("added #{work_item.to_reference} as parent issue") + expect(task.notes).to be_empty end end @@ -109,6 +122,15 @@ RSpec.describe WorkItems::ParentLinks::CreateService do is_expected.to eq(service_error(error, http_status: 422)) end + + it 'creates notes for valid links' do + subject + + expect(work_item.notes.last.note).to eq("added #{task1.to_reference} as child task") + expect(task1.notes.last.note).to eq("added #{work_item.to_reference} as parent issue") + expect(issue.notes).to be_empty + expect(other_project_task.notes).to be_empty + end end context 'when parent type is invalid' do diff --git a/spec/services/work_items/parent_links/destroy_service_spec.rb b/spec/services/work_items/parent_links/destroy_service_spec.rb index 574b70af397..654a03ef6f7 100644 --- a/spec/services/work_items/parent_links/destroy_service_spec.rb +++ b/spec/services/work_items/parent_links/destroy_service_spec.rb @@ -9,7 +9,7 @@ RSpec.describe WorkItems::ParentLinks::DestroyService do let_it_be(:project) { create(:project) } let_it_be(:work_item) { create(:work_item, project: project) } let_it_be(:task) { create(:work_item, :task, project: project) } - let_it_be(:parent_link) { create(:parent_link, work_item: task, work_item_parent: work_item)} + let_it_be(:parent_link) { create(:parent_link, work_item: task, work_item_parent: work_item) } let(:parent_link_class) { WorkItems::ParentLink } @@ -23,8 +23,11 @@ RSpec.describe WorkItems::ParentLinks::DestroyService do context 'when user has permissions to update work items' do let(:user) { reporter } - it 'removes relation' do + it 'removes relation and creates notes', :aggregate_failures do expect { subject }.to change(parent_link_class, :count).by(-1) + + expect(work_item.notes.last.note).to eq("removed child task #{task.to_reference}") + expect(task.notes.last.note).to eq("removed parent issue #{work_item.to_reference}") end it 'returns success message' do @@ -35,8 +38,10 @@ RSpec.describe WorkItems::ParentLinks::DestroyService do context 'when user has insufficient permissions' do let(:user) { guest } - it 'does not remove relation' do + it 'does not remove relation', :aggregate_failures do expect { subject }.not_to change(parent_link_class, :count).from(1) + + expect(SystemNoteService).not_to receive(:unrelate_work_item) end it 'returns error message' do diff --git a/spec/services/work_items/update_service_spec.rb b/spec/services/work_items/update_service_spec.rb index b17c9ffb4fb..2e0b0051495 100644 --- a/spec/services/work_items/update_service_spec.rb +++ b/spec/services/work_items/update_service_spec.rb @@ -4,7 +4,8 @@ require 'spec_helper' RSpec.describe WorkItems::UpdateService do let_it_be(:developer) { create(:user) } - let_it_be(:project) { create(:project).tap { |proj| proj.add_developer(developer) } } + let_it_be(:guest) { create(:user) } + let_it_be(:project) { create(:project) } let_it_be(:parent) { create(:work_item, project: project) } let_it_be_with_reload(:work_item) { create(:work_item, project: project, assignees: [developer]) } @@ -13,21 +14,36 @@ RSpec.describe WorkItems::UpdateService do let(:opts) { {} } let(:current_user) { developer } + before do + project.add_developer(developer) + project.add_guest(guest) + end + describe '#execute' do - subject(:update_work_item) do + let(:service) do described_class.new( project: project, current_user: current_user, params: opts, spam_params: spam_params, widget_params: widget_params - ).execute(work_item) + ) end + subject(:update_work_item) { service.execute(work_item) } + before do stub_spam_services end + shared_examples 'update service that triggers graphql dates updated subscription' do + it 'triggers graphql subscription issueableDatesUpdated' do + expect(GraphqlTriggers).to receive(:issuable_dates_updated).with(work_item).and_call_original + + update_work_item + end + end + context 'when title is changed' do let(:opts) { { title: 'changed' } } @@ -50,6 +66,16 @@ RSpec.describe WorkItems::UpdateService do end end + context 'when dates are changed' do + let(:opts) { { start_date: Date.today } } + + it 'tracks users updating work item dates' do + expect(Gitlab::UsageDataCounters::WorkItemActivityUniqueCounter).to receive(:track_work_item_date_changed_action).with(author: current_user) + + update_work_item + end + end + context 'when updating state_event' do context 'when state_event is close' do let(:opts) { { state_event: 'close' } } @@ -82,8 +108,7 @@ RSpec.describe WorkItems::UpdateService do let(:widget_params) do { hierarchy_widget: { parent: parent }, - description_widget: { description: 'foo' }, - weight_widget: { weight: 1 } + description_widget: { description: 'foo' } } end @@ -101,8 +126,7 @@ RSpec.describe WorkItems::UpdateService do let(:supported_widgets) do [ - { klass: WorkItems::Widgets::DescriptionService::UpdateService, callback: :update, params: { description: 'foo' } }, - { klass: WorkItems::Widgets::WeightService::UpdateService, callback: :update, params: { weight: 1 } }, + { klass: WorkItems::Widgets::DescriptionService::UpdateService, callback: :before_update_callback, params: { description: 'foo' } }, { klass: WorkItems::Widgets::HierarchyService::UpdateService, callback: :before_update_in_transaction, params: { parent: parent } } ] end @@ -126,7 +150,7 @@ RSpec.describe WorkItems::UpdateService do before do allow_next_instance_of(widget_service_class) do |instance| allow(instance) - .to receive(:update) + .to receive(:before_update_callback) .with(params: { description: 'changed' }).and_return(nil) end end @@ -142,6 +166,69 @@ RSpec.describe WorkItems::UpdateService do expect(work_item.description).to eq('changed') end + + context 'with mentions', :mailer, :sidekiq_might_not_need_inline do + shared_examples 'creates the todo and sends email' do |attribute| + it 'creates a todo and sends email' do + expect { perform_enqueued_jobs { update_work_item } }.to change(Todo, :count).by(1) + expect(work_item.reload.attributes[attribute.to_s]).to eq("mention #{guest.to_reference}") + should_email(guest) + end + end + + context 'when description contains a user mention' do + let(:widget_params) { { description_widget: { description: "mention #{guest.to_reference}" } } } + + it_behaves_like 'creates the todo and sends email', :description + end + + context 'when title contains a user mention' do + let(:opts) { { title: "mention #{guest.to_reference}" } } + + it_behaves_like 'creates the todo and sends email', :title + end + end + + context 'when work item validation fails' do + let(:opts) { { title: '' } } + + it 'returns validation errors' do + expect(update_work_item[:message]).to contain_exactly("Title can't be blank") + end + + it 'does not execute after-update widgets', :aggregate_failures do + expect(service).to receive(:update).and_call_original + expect(service).not_to receive(:execute_widgets).with(callback: :update, widget_params: widget_params) + + expect { update_work_item }.not_to change(work_item, :description) + end + end + end + + context 'for start and due date widget' do + let(:updated_date) { 1.week.from_now.to_date } + + context 'when due_date is updated' do + let(:widget_params) { { start_and_due_date_widget: { due_date: updated_date } } } + + it_behaves_like 'update service that triggers graphql dates updated subscription' + end + + context 'when start_date is updated' do + let(:widget_params) { { start_and_due_date_widget: { start_date: updated_date } } } + + it_behaves_like 'update service that triggers graphql dates updated subscription' + end + + context 'when no date param is updated' do + let(:opts) { { title: 'should not trigger' } } + + it 'does not trigger date updated subscription' do + expect(GraphqlTriggers).not_to receive(:issuable_dates_updated) + + update_work_item + end + end end context 'for the hierarchy widget' do @@ -175,6 +262,22 @@ RSpec.describe WorkItems::UpdateService do end.to not_change(WorkItems::ParentLink, :count).and(not_change(work_item, :title)) end end + + context 'when work item validation fails' do + let(:opts) { { title: '' } } + + it 'returns validation errors' do + expect(update_work_item[:message]).to contain_exactly("Title can't be blank") + end + + it 'does not execute after-update widgets', :aggregate_failures do + expect(service).to receive(:update).and_call_original + expect(service).not_to receive(:execute_widgets).with(callback: :before_update_in_transaction, widget_params: widget_params) + expect(work_item.work_item_children).not_to include(child_work_item) + + update_work_item + end + end end end end diff --git a/spec/services/work_items/widgets/assignees_service/update_service_spec.rb b/spec/services/work_items/widgets/assignees_service/update_service_spec.rb new file mode 100644 index 00000000000..0ab2c85f078 --- /dev/null +++ b/spec/services/work_items/widgets/assignees_service/update_service_spec.rb @@ -0,0 +1,116 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe WorkItems::Widgets::AssigneesService::UpdateService, :freeze_time do + let_it_be(:reporter) { create(:user) } + let_it_be(:project) { create(:project, :private) } + let_it_be(:new_assignee) { create(:user) } + + let(:work_item) do + create(:work_item, project: project, updated_at: 1.day.ago) + end + + let(:widget) { work_item.widgets.find { |widget| widget.is_a?(WorkItems::Widgets::Assignees) } } + let(:current_user) { reporter } + let(:params) { { assignee_ids: [new_assignee.id] } } + + before_all do + project.add_reporter(reporter) + project.add_guest(new_assignee) + end + + describe '#before_update_in_transaction' do + subject do + described_class.new(widget: widget, current_user: current_user) + .before_update_in_transaction(params: params) + end + + it 'updates the assignees and sets updated_at to the current time' do + subject + + expect(work_item.assignee_ids).to contain_exactly(new_assignee.id) + expect(work_item.updated_at).to be_like_time(Time.current) + end + + context 'when passing an empty array' do + let(:params) { { assignee_ids: [] } } + + before do + work_item.assignee_ids = [reporter.id] + end + + it 'removes existing assignees' do + subject + + expect(work_item.assignee_ids).to be_empty + expect(work_item.updated_at).to be_like_time(Time.current) + end + end + + context 'when user does not have access' do + let(:current_user) { create(:user) } + + it 'does not update the assignees' do + subject + + expect(work_item.assignee_ids).to be_empty + expect(work_item.updated_at).to be_like_time(1.day.ago) + end + end + + context 'when multiple assignees are given' do + let(:params) { { assignee_ids: [new_assignee.id, reporter.id] } } + + context 'when work item allows multiple assignees' do + before do + allow(work_item).to receive(:allows_multiple_assignees?).and_return(true) + end + + it 'sets all the given assignees' do + subject + + expect(work_item.assignee_ids).to contain_exactly(new_assignee.id, reporter.id) + expect(work_item.updated_at).to be_like_time(Time.current) + end + end + + context 'when work item does not allow multiple assignees' do + before do + allow(work_item).to receive(:allows_multiple_assignees?).and_return(false) + end + + it 'only sets the first assignee' do + subject + + expect(work_item.assignee_ids).to contain_exactly(new_assignee.id) + expect(work_item.updated_at).to be_like_time(Time.current) + end + end + end + + context 'when assignee does not have access to the work item' do + let(:params) { { assignee_ids: [create(:user).id] } } + + it 'does not set the assignee' do + subject + + expect(work_item.assignee_ids).to be_empty + expect(work_item.updated_at).to be_like_time(1.day.ago) + end + end + + context 'when assignee ids are the same as the existing ones' do + before do + work_item.assignee_ids = [new_assignee.id] + end + + it 'does not touch updated_at' do + subject + + expect(work_item.assignee_ids).to contain_exactly(new_assignee.id) + expect(work_item.updated_at).to be_like_time(1.day.ago) + end + end + end +end diff --git a/spec/services/work_items/widgets/description_service/update_service_spec.rb b/spec/services/work_items/widgets/description_service/update_service_spec.rb index a2eceb97f09..582d9dc85f7 100644 --- a/spec/services/work_items/widgets/description_service/update_service_spec.rb +++ b/spec/services/work_items/widgets/description_service/update_service_spec.rb @@ -3,32 +3,102 @@ require 'spec_helper' RSpec.describe WorkItems::Widgets::DescriptionService::UpdateService do - let_it_be(:user) { create(:user) } - let_it_be(:project) { create(:project) } - let_it_be_with_reload(:work_item) { create(:work_item, project: project, description: 'old description') } + let_it_be(:random_user) { create(:user) } + let_it_be(:author) { create(:user) } + let_it_be(:guest) { create(:user) } + let_it_be(:reporter) { create(:user) } + let_it_be(:project) { create(:project, :public) } - let(:widget) { work_item.widgets.find {|widget| widget.is_a?(WorkItems::Widgets::Description) } } + let(:params) { { description: 'updated description' } } + let(:current_user) { author } + let(:work_item) do + create(:work_item, author: author, project: project, description: 'old description', + last_edited_at: Date.yesterday, last_edited_by: random_user + ) + end - describe '#update' do - subject { described_class.new(widget: widget, current_user: user).update(params: params) } # rubocop:disable Rails/SaveBang + let(:widget) { work_item.widgets.find { |widget| widget.is_a?(WorkItems::Widgets::Description) } } - context 'when description param is present' do - let(:params) { { description: 'updated description' } } + describe '#update' do + subject { described_class.new(widget: widget, current_user: current_user).before_update_callback(params: params) } + shared_examples 'sets work item description' do it 'correctly sets work item description value' do subject - expect(work_item.description).to eq('updated description') + expect(work_item.description).to eq(params[:description]) + expect(work_item.last_edited_by).to eq(current_user) + expect(work_item.last_edited_at).to be_within(2.seconds).of(Time.current) end end - context 'when description param is not present' do - let(:params) { {} } - + shared_examples 'does not set work item description' do it 'does not change work item description value' do subject expect(work_item.description).to eq('old description') + expect(work_item.last_edited_by).to eq(random_user) + expect(work_item.last_edited_at).to eq(Date.yesterday) + end + end + + context 'when user has permission to update description' do + context 'when user is work item author' do + let(:current_user) { author } + + it_behaves_like 'sets work item description' + end + + context 'when user is a project reporter' do + let(:current_user) { reporter } + + before do + project.add_reporter(reporter) + end + + it_behaves_like 'sets work item description' + end + + context 'when description is nil' do + let(:current_user) { author } + let(:params) { { description: nil } } + + it_behaves_like 'sets work item description' + end + + context 'when description is empty' do + let(:current_user) { author } + let(:params) { { description: '' } } + + it_behaves_like 'sets work item description' + end + + context 'when description param is not present' do + let(:params) { {} } + + it_behaves_like 'does not set work item description' + end + end + + context 'when user does not have permission to update description' do + context 'when user is a project guest' do + let(:current_user) { guest } + + before do + project.add_guest(guest) + end + + it_behaves_like 'does not set work item description' + end + + context 'with private project' do + let_it_be(:project) { create(:project) } + + context 'when user is work item author' do + let(:current_user) { author } + + it_behaves_like 'does not set work item description' + end end end end diff --git a/spec/services/work_items/widgets/hierarchy_service/update_service_spec.rb b/spec/services/work_items/widgets/hierarchy_service/update_service_spec.rb index 4f6ff1b8676..9a425d5308c 100644 --- a/spec/services/work_items/widgets/hierarchy_service/update_service_spec.rb +++ b/spec/services/work_items/widgets/hierarchy_service/update_service_spec.rb @@ -11,7 +11,7 @@ RSpec.describe WorkItems::Widgets::HierarchyService::UpdateService do let_it_be(:child_work_item) { create(:work_item, :task, project: project) } let_it_be(:existing_link) { create(:parent_link, work_item: child_work_item, work_item_parent: work_item) } - let(:widget) { work_item.widgets.find {|widget| widget.is_a?(WorkItems::Widgets::Hierarchy) } } + let(:widget) { work_item.widgets.find { |widget| widget.is_a?(WorkItems::Widgets::Hierarchy) } } let(:not_found_error) { 'No matching task found. Make sure that you are adding a valid task ID.' } shared_examples 'raises a WidgetError' do @@ -29,13 +29,21 @@ RSpec.describe WorkItems::Widgets::HierarchyService::UpdateService do end end + context 'when invalid params are present' do + let(:params) { { other_parent: parent_work_item } } + + it_behaves_like 'raises a WidgetError' do + let(:message) { 'One or more arguments are invalid: other_parent.' } + end + end + context 'when updating children' do let_it_be(:child_work_item2) { create(:work_item, :task, project: project) } let_it_be(:child_work_item3) { create(:work_item, :task, project: project) } let_it_be(:child_work_item4) { create(:work_item, :task, project: project) } context 'when work_items_hierarchy feature flag is disabled' do - let(:params) { { children: [child_work_item4] }} + let(:params) { { children: [child_work_item4] } } before do stub_feature_flags(work_items_hierarchy: false) @@ -47,7 +55,7 @@ RSpec.describe WorkItems::Widgets::HierarchyService::UpdateService do end context 'when user has insufficient permissions to link work items' do - let(:params) { { children: [child_work_item4] }} + let(:params) { { children: [child_work_item4] } } it_behaves_like 'raises a WidgetError' do let(:message) { not_found_error } @@ -60,7 +68,7 @@ RSpec.describe WorkItems::Widgets::HierarchyService::UpdateService do end context 'with valid params' do - let(:params) { { children: [child_work_item2, child_work_item3] }} + let(:params) { { children: [child_work_item2, child_work_item3] } } it 'correctly sets work item parent' do subject @@ -71,7 +79,7 @@ RSpec.describe WorkItems::Widgets::HierarchyService::UpdateService do end context 'when child is already assigned' do - let(:params) { { children: [child_work_item] }} + let(:params) { { children: [child_work_item] } } it_behaves_like 'raises a WidgetError' do let(:message) { 'Task(s) already assigned' } @@ -81,7 +89,7 @@ RSpec.describe WorkItems::Widgets::HierarchyService::UpdateService do context 'when child type is invalid' do let_it_be(:child_issue) { create(:work_item, project: project) } - let(:params) { { children: [child_issue] }} + let(:params) { { children: [child_issue] } } it_behaves_like 'raises a WidgetError' do let(:message) do @@ -95,7 +103,7 @@ RSpec.describe WorkItems::Widgets::HierarchyService::UpdateService do context 'when updating parent' do let_it_be(:work_item) { create(:work_item, :task, project: project) } - let(:params) {{ parent: parent_work_item } } + let(:params) { { parent: parent_work_item } } context 'when work_items_hierarchy feature flag is disabled' do before do @@ -144,9 +152,9 @@ RSpec.describe WorkItems::Widgets::HierarchyService::UpdateService do end context 'when type is invalid' do - let_it_be(:parent_task) { create(:work_item, :task, project: project)} + let_it_be(:parent_task) { create(:work_item, :task, project: project) } - let(:params) {{ parent: parent_task } } + let(:params) { { parent: parent_task } } it_behaves_like 'raises a WidgetError' do let(:message) do diff --git a/spec/services/work_items/widgets/start_and_due_date_service/update_service_spec.rb b/spec/services/work_items/widgets/start_and_due_date_service/update_service_spec.rb new file mode 100644 index 00000000000..d328c541fc7 --- /dev/null +++ b/spec/services/work_items/widgets/start_and_due_date_service/update_service_spec.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe WorkItems::Widgets::StartAndDueDateService::UpdateService do + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project) } + let_it_be_with_reload(:work_item) { create(:work_item, project: project) } + + let(:widget) { work_item.widgets.find { |widget| widget.is_a?(WorkItems::Widgets::StartAndDueDate) } } + + describe '#before_update_callback' do + let(:start_date) { Date.today } + let(:due_date) { 1.week.from_now.to_date } + + subject(:update_params) do + described_class.new(widget: widget, current_user: user).before_update_callback(params: params) + end + + context 'when start and due date params are present' do + let(:params) { { start_date: Date.today, due_date: 1.week.from_now.to_date } } + + it 'correctly sets date values' do + expect do + update_params + end.to change(work_item, :start_date).from(nil).to(start_date).and( + change(work_item, :due_date).from(nil).to(due_date) + ) + end + end + + context 'when date params are not present' do + let(:params) { {} } + + it 'does not change work item date values' do + expect do + update_params + end.to not_change(work_item, :start_date).from(nil).and( + not_change(work_item, :due_date).from(nil) + ) + end + end + + context 'when work item had both date values already set' do + before do + work_item.update!(start_date: start_date, due_date: due_date) + end + + context 'when one of the two params is null' do + let(:params) { { start_date: nil } } + + it 'sets only one date to null' do + expect do + update_params + end.to change(work_item, :start_date).from(start_date).to(nil).and( + not_change(work_item, :due_date).from(due_date) + ) + end + end + end + end +end diff --git a/spec/services/work_items/widgets/weight_service/update_service_spec.rb b/spec/services/work_items/widgets/weight_service/update_service_spec.rb deleted file mode 100644 index 97e17f1c526..00000000000 --- a/spec/services/work_items/widgets/weight_service/update_service_spec.rb +++ /dev/null @@ -1,36 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe WorkItems::Widgets::WeightService::UpdateService do - let_it_be(:user) { create(:user) } - let_it_be(:project) { create(:project) } - let_it_be_with_reload(:work_item) { create(:work_item, project: project, weight: 1) } - - let(:widget) { work_item.widgets.find {|widget| widget.is_a?(WorkItems::Widgets::Weight) } } - - describe '#update' do - subject { described_class.new(widget: widget, current_user: user).update(params: params) } # rubocop:disable Rails/SaveBang - - context 'when weight param is present' do - let(:params) { { weight: 2 } } - - it 'correctly sets work item weight value' do - subject - - expect(work_item.weight).to eq(2) - end - end - - context 'when weight param is not present' do - let(:params) { {} } - - it 'does not change work item weight value', :aggregate_failures do - expect { subject } - .to not_change { work_item.weight } - - expect(work_item.weight).to eq(1) - end - end - end -end |