diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-02-20 16:49:51 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-02-20 16:49:51 +0300 |
commit | 71786ddc8e28fbd3cb3fcc4b3ff15e5962a1c82e (patch) | |
tree | 6a2d93ef3fb2d353bb7739e4b57e6541f51cdd71 /spec/services | |
parent | a7253423e3403b8c08f8a161e5937e1488f5f407 (diff) |
Add latest changes from gitlab-org/gitlab@15-9-stable-eev15.9.0-rc42
Diffstat (limited to 'spec/services')
133 files changed, 3595 insertions, 1646 deletions
diff --git a/spec/services/achievements/create_service_spec.rb b/spec/services/achievements/create_service_spec.rb index f62a45deb50..ac28a88572b 100644 --- a/spec/services/achievements/create_service_spec.rb +++ b/spec/services/achievements/create_service_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Achievements::CreateService, feature_category: :users do +RSpec.describe Achievements::CreateService, feature_category: :user_profile do describe '#execute' do let_it_be(:user) { create(:user) } diff --git a/spec/services/analytics/cycle_analytics/stages/list_service_spec.rb b/spec/services/analytics/cycle_analytics/stages/list_service_spec.rb index 24f0123ed3b..7bfae0cd9fc 100644 --- a/spec/services/analytics/cycle_analytics/stages/list_service_spec.rb +++ b/spec/services/analytics/cycle_analytics/stages/list_service_spec.rb @@ -5,11 +5,14 @@ require 'spec_helper' RSpec.describe Analytics::CycleAnalytics::Stages::ListService do let_it_be(:project) { create(:project) } let_it_be(:user) { create(:user) } + let_it_be(:project_namespace) { project.project_namespace.reload } - let(:value_stream) { Analytics::CycleAnalytics::ProjectValueStream.build_default_value_stream(project) } + let(:value_stream) { Analytics::CycleAnalytics::ValueStream.build_default_value_stream(project_namespace) } let(:stages) { subject.payload[:stages] } - subject { described_class.new(parent: project, current_user: user).execute } + subject do + described_class.new(parent: project_namespace, current_user: user, params: { value_stream: value_stream }).execute + end before_all do project.add_reporter(user) diff --git a/spec/services/audit_event_service_spec.rb b/spec/services/audit_event_service_spec.rb index 1d079adc0be..4f8b90fcb4a 100644 --- a/spec/services/audit_event_service_spec.rb +++ b/spec/services/audit_event_service_spec.rb @@ -13,12 +13,12 @@ RSpec.describe AuditEventService, :with_license do describe '#security_event' do it 'creates an event and logs to a file' do expect(service).to receive(:file_logger).and_return(logger) - expect(logger).to receive(:info).with(author_id: user.id, - author_name: user.name, - entity_id: project.id, - entity_type: "Project", - action: :destroy, - created_at: anything) + expect(logger).to receive(:info).with({ author_id: user.id, + author_name: user.name, + entity_id: project.id, + entity_type: "Project", + action: :destroy, + created_at: anything }) expect { service.security_event }.to change(AuditEvent, :count).by(1) end @@ -33,15 +33,15 @@ RSpec.describe AuditEventService, :with_license do target_id: 1 }) expect(service).to receive(:file_logger).and_return(logger) - expect(logger).to receive(:info).with(author_id: user.id, - author_name: user.name, - entity_type: 'Project', - entity_id: project.id, - from: 'true', - to: 'false', - action: :create, - target_id: 1, - created_at: anything) + expect(logger).to receive(:info).with({ author_id: user.id, + author_name: user.name, + entity_type: 'Project', + entity_id: project.id, + from: 'true', + to: 'false', + action: :create, + target_id: 1, + created_at: anything }) expect { service.security_event }.to change(AuditEvent, :count).by(1) @@ -58,12 +58,12 @@ RSpec.describe AuditEventService, :with_license do it 'is overridden successfully' do freeze_time do expect(service).to receive(:file_logger).and_return(logger) - expect(logger).to receive(:info).with(author_id: user.id, - author_name: user.name, - entity_id: project.id, - entity_type: "Project", - action: :destroy, - created_at: 3.weeks.ago) + expect(logger).to receive(:info).with({ author_id: user.id, + author_name: user.name, + entity_id: project.id, + entity_type: "Project", + action: :destroy, + created_at: 3.weeks.ago }) expect { service.security_event }.to change(AuditEvent, :count).by(1) expect(AuditEvent.last.created_at).to eq(3.weeks.ago) @@ -129,12 +129,12 @@ RSpec.describe AuditEventService, :with_license do describe '#log_security_event_to_file' do it 'logs security event to file' do expect(service).to receive(:file_logger).and_return(logger) - expect(logger).to receive(:info).with(author_id: user.id, - author_name: user.name, - entity_type: 'Project', - entity_id: project.id, - action: :destroy, - created_at: anything) + expect(logger).to receive(:info).with({ author_id: user.id, + author_name: user.name, + entity_type: 'Project', + entity_id: project.id, + action: :destroy, + created_at: anything }) service.log_security_event_to_file end diff --git a/spec/services/authorized_project_update/project_access_changed_service_spec.rb b/spec/services/authorized_project_update/project_access_changed_service_spec.rb index 11621055a47..da428bece20 100644 --- a/spec/services/authorized_project_update/project_access_changed_service_spec.rb +++ b/spec/services/authorized_project_update/project_access_changed_service_spec.rb @@ -4,18 +4,11 @@ require 'spec_helper' RSpec.describe AuthorizedProjectUpdate::ProjectAccessChangedService do describe '#execute' do - it 'schedules the project IDs' do - expect(AuthorizedProjectUpdate::ProjectRecalculateWorker).to receive(:bulk_perform_and_wait) - .with([[1], [2]]) - - described_class.new([1, 2]).execute - end - - it 'permits non-blocking operation' do + it 'executes projects_authorizations refresh' do expect(AuthorizedProjectUpdate::ProjectRecalculateWorker).to receive(:bulk_perform_async) .with([[1], [2]]) - described_class.new([1, 2]).execute(blocking: false) + described_class.new([1, 2]).execute end end end diff --git a/spec/services/auto_merge_service_spec.rb b/spec/services/auto_merge_service_spec.rb index 043b413acff..7584e44152e 100644 --- a/spec/services/auto_merge_service_spec.rb +++ b/spec/services/auto_merge_service_spec.rb @@ -131,7 +131,7 @@ RSpec.describe AutoMergeService do subject end - context 'when the head piipeline succeeded' do + context 'when the head pipeline succeeded' do let(:pipeline_status) { :success } it 'returns failed' do diff --git a/spec/services/bulk_imports/create_service_spec.rb b/spec/services/bulk_imports/create_service_spec.rb index 75f88e3989c..7f892cfe722 100644 --- a/spec/services/bulk_imports/create_service_spec.rb +++ b/spec/services/bulk_imports/create_service_spec.rb @@ -8,27 +8,27 @@ RSpec.describe BulkImports::CreateService, feature_category: :importers do let(:destination_group) { create(:group, path: 'destination1') } let(:migrate_projects) { true } let_it_be(:parent_group) { create(:group, path: 'parent-group') } + # note: destination_name and destination_slug are currently interchangable so we need to test for both possibilities let(:params) do [ { source_type: 'group_entity', source_full_path: 'full/path/to/group1', - destination_slug: 'destination group 1', + destination_slug: 'destination-group-1', destination_namespace: 'parent-group', migrate_projects: migrate_projects - }, { source_type: 'group_entity', source_full_path: 'full/path/to/group2', - destination_slug: 'destination group 2', + destination_name: 'destination-group-2', destination_namespace: 'parent-group', migrate_projects: migrate_projects }, { source_type: 'project_entity', source_full_path: 'full/path/to/project1', - destination_slug: 'destination project 1', + destination_slug: 'destination-project-1', destination_namespace: 'parent-group', migrate_projects: migrate_projects } @@ -226,7 +226,12 @@ RSpec.describe BulkImports::CreateService, feature_category: :importers do expect(result).to be_a(ServiceResponse) expect(result).to be_error - expect(result.message).to eq("Validation failed: Source full path can't be blank") + expect(result.message).to eq("Validation failed: Source full path can't be blank, " \ + "Source full path cannot start with a non-alphanumeric character except " \ + "for periods or underscores, can contain only alphanumeric characters, " \ + "forward slashes, periods, and underscores, cannot end with " \ + "a period or forward slash, and has a relative path structure " \ + "with no http protocol chars or leading or trailing forward slashes") end describe '#user-role' do @@ -267,56 +272,188 @@ RSpec.describe BulkImports::CreateService, feature_category: :importers do extra: { user_role: 'Not a member', import_type: 'bulk_import_group' } ) end - end - context 'when there is a destination_namespace but no parent_namespace' do - let(:params) do - [ - { - source_type: 'group_entity', - source_full_path: 'full/path/to/group1', - destination_slug: 'destination-group-1', - destination_namespace: 'destination1' - } - ] + context 'when there is a destination_namespace but no parent_namespace' do + let(:params) do + [ + { + source_type: 'group_entity', + source_full_path: 'full/path/to/group1', + destination_slug: 'destination-group-1', + destination_namespace: 'destination1' + } + ] + end + + it 'defines access_level from destination_namespace' do + destination_group.add_developer(user) + subject.execute + + expect_snowplow_event( + category: 'BulkImports::CreateService', + action: 'create', + label: 'import_access_level', + user: user, + extra: { user_role: 'Developer', import_type: 'bulk_import_group' } + ) + end end - it 'defines access_level from destination_namespace' do - destination_group.add_developer(user) - subject.execute + context 'when there is no destination_namespace or parent_namespace' do + let(:params) do + [ + { + source_type: 'group_entity', + source_full_path: 'full/path/to/group1', + destination_slug: 'destinationational-mcdestiny', + destination_namespace: 'destinational-mcdestiny' + } + ] + end - expect_snowplow_event( - category: 'BulkImports::CreateService', - action: 'create', - label: 'import_access_level', - user: user, - extra: { user_role: 'Developer', import_type: 'bulk_import_group' } - ) + it 'defines access_level as owner' do + subject.execute + + expect_snowplow_event( + category: 'BulkImports::CreateService', + action: 'create', + label: 'import_access_level', + user: user, + extra: { user_role: 'Owner', import_type: 'bulk_import_group' } + ) + end end end - context 'when there is no destination_namespace or parent_namespace' do - let(:params) do - [ - { - source_type: 'group_entity', - source_full_path: 'full/path/to/group1', - destination_slug: 'destinationational mcdestiny', - destination_namespace: 'destinational-mcdestiny' - } - ] + describe '.validate_destination_full_path' do + context 'when the source_type is a group' do + context 'when the provided destination_slug already exists in the destination_namespace' do + let_it_be(:existing_subgroup) { create(:group, path: 'existing-subgroup', parent_id: parent_group.id ) } + let_it_be(:existing_subgroup_2) { create(:group, path: 'existing-subgroup_2', parent_id: parent_group.id ) } + let(:params) do + [ + { + source_type: 'group_entity', + source_full_path: 'full/path/to/source', + destination_slug: existing_subgroup.path, + destination_namespace: parent_group.path, + migrate_projects: migrate_projects + } + ] + end + + it 'returns ServiceResponse with an error message' do + result = subject.execute + + expect(result).to be_a(ServiceResponse) + expect(result).to be_error + expect(result.message) + .to eq( + "Import aborted as 'parent-group/existing-subgroup' already exists. " \ + "Change the destination and try again." + ) + end + end + + context 'when the destination_slug conflicts with an existing top-level namespace' do + let_it_be(:existing_top_level_group) { create(:group, path: 'top-level-group') } + let(:params) do + [ + { + source_type: 'group_entity', + source_full_path: 'full/path/to/source', + destination_slug: existing_top_level_group.path, + destination_namespace: '', + migrate_projects: migrate_projects + } + ] + end + + it 'returns ServiceResponse with an error message' do + result = subject.execute + + expect(result).to be_a(ServiceResponse) + expect(result).to be_error + expect(result.message) + .to eq( + "Import aborted as 'top-level-group' already exists. " \ + "Change the destination and try again." + ) + end + end + + context 'when the destination_slug does not conflict with an existing top-level namespace' do + let(:params) do + [ + { + source_type: 'group_entity', + source_full_path: 'full/path/to/source', + destination_slug: 'new-group', + destination_namespace: parent_group.path, + migrate_projects: migrate_projects + } + ] + end + + it 'returns success ServiceResponse' do + result = subject.execute + + expect(result).to be_a(ServiceResponse) + expect(result).to be_success + end + end end - it 'defines access_level as owner' do - subject.execute + context 'when the source_type is a project' do + context 'when the provided destination_slug already exists in the destination_namespace' do + let_it_be(:existing_group) { create(:group, path: 'existing-group' ) } + let_it_be(:existing_project) { create(:project, path: 'existing-project', parent_id: existing_group.id ) } + let(:params) do + [ + { + source_type: 'project_entity', + source_full_path: 'full/path/to/source', + destination_slug: existing_project.path, + destination_namespace: existing_group.path, + migrate_projects: migrate_projects + } + ] + end - expect_snowplow_event( - category: 'BulkImports::CreateService', - action: 'create', - label: 'import_access_level', - user: user, - extra: { user_role: 'Owner', import_type: 'bulk_import_group' } - ) + it 'returns ServiceResponse with an error message' do + result = subject.execute + + expect(result).to be_a(ServiceResponse) + expect(result).to be_error + expect(result.message) + .to eq( + "Import aborted as 'existing-group/existing-project' already exists. " \ + "Change the destination and try again." + ) + end + end + + context 'when the destination_slug does not conflict with an existing project' do + let_it_be(:existing_group) { create(:group, path: 'existing-group' ) } + let(:params) do + [ + { + source_type: 'project_entity', + source_full_path: 'full/path/to/source', + destination_slug: 'new-project', + destination_namespace: 'existing-group', + migrate_projects: migrate_projects + } + ] + end + + it 'returns success ServiceResponse' do + result = subject.execute + + expect(result).to be_a(ServiceResponse) + expect(result).to be_success + end + end end end end diff --git a/spec/services/chat_names/authorize_user_service_spec.rb b/spec/services/chat_names/authorize_user_service_spec.rb index 4c261ece504..c9b4439202a 100644 --- a/spec/services/chat_names/authorize_user_service_spec.rb +++ b/spec/services/chat_names/authorize_user_service_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe ChatNames::AuthorizeUserService, feature_category: :users do +RSpec.describe ChatNames::AuthorizeUserService, feature_category: :user_profile do describe '#execute' do let(:result) { subject.execute } diff --git a/spec/services/ci/archive_trace_service_spec.rb b/spec/services/ci/archive_trace_service_spec.rb index 359ea0699e4..3fb9d092ae7 100644 --- a/spec/services/ci/archive_trace_service_spec.rb +++ b/spec/services/ci/archive_trace_service_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Ci::ArchiveTraceService, '#execute' do +RSpec.describe Ci::ArchiveTraceService, '#execute', feature_category: :continuous_integration do subject { described_class.new.execute(job, worker_name: Ci::ArchiveTraceWorker.name) } context 'when job is finished' do @@ -192,4 +192,69 @@ RSpec.describe Ci::ArchiveTraceService, '#execute' do expect(job.trace_metadata.archival_attempts).to eq(1) end end + + describe '#batch_execute' do + subject { described_class.new.batch_execute(worker_name: Ci::ArchiveTraceWorker.name) } + + let_it_be_with_reload(:job) { create(:ci_build, :success, :trace_live, finished_at: 1.day.ago) } + let_it_be_with_reload(:job2) { create(:ci_build, :success, :trace_live, finished_at: 1.day.ago) } + + it 'archives multiple traces' do + expect { subject }.not_to raise_error + + expect(job.reload.job_artifacts_trace).to be_exist + expect(job2.reload.job_artifacts_trace).to be_exist + end + + it 'processes traces independently' do + allow_next_instance_of(Gitlab::Ci::Trace) do |instance| + orig_method = instance.method(:archive!) + allow(instance).to receive(:archive!) do + raise('Unexpected error') if instance.job.id == job.id + + orig_method.call + end + end + + expect { subject }.not_to raise_error + + expect(job.reload.job_artifacts_trace).to be_nil + expect(job2.reload.job_artifacts_trace).to be_exist + end + + context 'when timeout is reached' do + before do + stub_const("#{described_class}::LOOP_TIMEOUT", 0.seconds) + end + + it 'stops executing traces' do + expect { subject }.not_to raise_error + + expect(job.reload.job_artifacts_trace).to be_nil + end + end + + context 'when loop limit is reached' do + before do + stub_const("#{described_class}::LOOP_LIMIT", -1) + end + + it 'skips archiving' do + expect(job.trace).not_to receive(:archive!) + + subject + end + + it 'stops executing traces' do + expect(Sidekiq.logger).to receive(:warn).with( + class: Ci::ArchiveTraceWorker.name, + message: "Loop limit reached.", + job_id: job.id) + + expect { subject }.not_to raise_error + + expect(job.reload.job_artifacts_trace).to be_nil + end + end + end end diff --git a/spec/services/ci/components/fetch_service_spec.rb b/spec/services/ci/components/fetch_service_spec.rb new file mode 100644 index 00000000000..f2eaa8d31b4 --- /dev/null +++ b/spec/services/ci/components/fetch_service_spec.rb @@ -0,0 +1,141 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Ci::Components::FetchService, feature_category: :pipeline_authoring do + let_it_be(:project) { create(:project, :repository, create_tag: 'v1.0') } + let_it_be(:user) { create(:user) } + let_it_be(:current_user) { user } + let_it_be(:current_host) { Gitlab.config.gitlab.host } + + let(:service) do + described_class.new(address: address, current_user: current_user) + end + + before do + project.add_developer(user) + end + + describe '#execute', :aggregate_failures do + subject(:result) { service.execute } + + shared_examples 'an external component' do + shared_examples 'component address' do + context 'when content exists' do + let(:sha) { project.commit(version).id } + + let(:content) do + <<~COMPONENT + job: + script: echo + COMPONENT + end + + before do + stub_project_blob(sha, component_yaml_path, content) + end + + it 'returns the content' do + expect(result).to be_success + expect(result.payload[:content]).to eq(content) + end + end + + context 'when content does not exist' do + it 'returns an error' do + expect(result).to be_error + expect(result.reason).to eq(:content_not_found) + end + end + end + + context 'when user does not have permissions to read the code' do + let(:version) { 'master' } + let(:current_user) { create(:user) } + + it 'returns an error' do + expect(result).to be_error + expect(result.reason).to eq(:not_allowed) + end + end + + context 'when version is a branch name' do + it_behaves_like 'component address' do + let(:version) { project.default_branch } + end + end + + context 'when version is a tag name' do + it_behaves_like 'component address' do + let(:version) { project.repository.tags.first.name } + end + end + + context 'when version is a commit sha' do + it_behaves_like 'component address' do + let(:version) { project.repository.tags.first.id } + end + end + + context 'when version is not provided' do + let(:version) { nil } + + it 'returns an error' do + expect(result).to be_error + expect(result.reason).to eq(:content_not_found) + end + end + + context 'when project does not exist' do + let(:component_path) { 'unknown/component' } + let(:version) { '1.0' } + + it 'returns an error' do + expect(result).to be_error + expect(result.reason).to eq(:content_not_found) + end + end + + context 'when host is different than the current instance host' do + let(:current_host) { 'another-host.com' } + let(:version) { '1.0' } + + it 'returns an error' do + expect(result).to be_error + expect(result.reason).to eq(:unsupported_path) + end + end + end + + context 'when address points to an external component' do + let(:address) { "#{current_host}/#{component_path}@#{version}" } + + context 'when component path is the full path to a project' do + let(:component_path) { project.full_path } + let(:component_yaml_path) { 'template.yml' } + + it_behaves_like 'an external component' + end + + context 'when component path points to a directory in a project' do + let(:component_path) { "#{project.full_path}/my-component" } + let(:component_yaml_path) { 'my-component/template.yml' } + + it_behaves_like 'an external component' + end + + context 'when component path points to a nested directory in a project' do + let(:component_path) { "#{project.full_path}/my-dir/my-component" } + let(:component_yaml_path) { 'my-dir/my-component/template.yml' } + + it_behaves_like 'an external component' + end + end + end + + def stub_project_blob(ref, path, content) + allow_next_instance_of(Repository) do |instance| + allow(instance).to receive(:blob_data_at).with(ref, path).and_return(content) + end + end +end diff --git a/spec/services/ci/create_downstream_pipeline_service_spec.rb b/spec/services/ci/create_downstream_pipeline_service_spec.rb index fd978bffacb..7b576339c61 100644 --- a/spec/services/ci/create_downstream_pipeline_service_spec.rb +++ b/spec/services/ci/create_downstream_pipeline_service_spec.rb @@ -890,23 +890,6 @@ RSpec.describe Ci::CreateDownstreamPipelineService, '#execute', feature_category end end end - - context 'with :ci_limit_complete_hierarchy_size disabled' do - before do - stub_feature_flags(ci_limit_complete_hierarchy_size: false) - end - - it 'creates a new pipeline' do - expect { subject }.to change { Ci::Pipeline.count }.by(1) - expect(subject).to be_success - end - - it 'marks the bridge job as successful' do - subject - - expect(bridge.reload).to be_success - end - end 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 711002e28af..47e9e5994ef 100644 --- a/spec/services/ci/job_artifacts/create_service_spec.rb +++ b/spec/services/ci/job_artifacts/create_service_spec.rb @@ -132,6 +132,14 @@ RSpec.describe Ci::JobArtifacts::CreateService do expect(new_artifact).to be_public_accessibility end + it 'logs the created artifact and metadata' do + expect(Gitlab::Ci::Artifacts::Logger) + .to receive(:log_created) + .with(an_instance_of(Ci::JobArtifact)).twice + + subject + end + context 'when accessibility level passed as private' do before do params.merge!('accessibility' => 'private') diff --git a/spec/services/ci/job_artifacts/destroy_all_expired_service_spec.rb b/spec/services/ci/job_artifacts/destroy_all_expired_service_spec.rb index dd10c0df374..457be67c1ea 100644 --- a/spec/services/ci/job_artifacts/destroy_all_expired_service_spec.rb +++ b/spec/services/ci/job_artifacts/destroy_all_expired_service_spec.rb @@ -2,7 +2,8 @@ require 'spec_helper' -RSpec.describe Ci::JobArtifacts::DestroyAllExpiredService, :clean_gitlab_redis_shared_state do +RSpec.describe Ci::JobArtifacts::DestroyAllExpiredService, :clean_gitlab_redis_shared_state, +feature_category: :build_artifacts do include ExclusiveLeaseHelpers let(:service) { described_class.new } diff --git a/spec/services/ci/job_token_scope/add_project_service_spec.rb b/spec/services/ci/job_token_scope/add_project_service_spec.rb index bf7df3a5595..e6674ee384f 100644 --- a/spec/services/ci/job_token_scope/add_project_service_spec.rb +++ b/spec/services/ci/job_token_scope/add_project_service_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true require 'spec_helper' -RSpec.describe Ci::JobTokenScope::AddProjectService do +RSpec.describe Ci::JobTokenScope::AddProjectService, feature_category: :continuous_integration do let(:service) { described_class.new(project, current_user) } let_it_be(:project) { create(:project, ci_outbound_job_token_scope_enabled: true).tap(&:save!) } @@ -21,6 +21,8 @@ RSpec.describe Ci::JobTokenScope::AddProjectService do it_behaves_like 'editable job token scope' do context 'when user has permissions on source and target projects' do + let(:resulting_direction) { result.payload.fetch(:project_link)&.direction } + before do project.add_maintainer(current_user) target_project.add_developer(current_user) @@ -34,6 +36,26 @@ RSpec.describe Ci::JobTokenScope::AddProjectService do end it_behaves_like 'adds project' + + it 'creates an outbound link by default' do + expect(resulting_direction).to eq('outbound') + end + + context 'when direction is specified' do + subject(:result) { service.execute(target_project, direction: direction) } + + context 'when the direction is outbound' do + let(:direction) { :outbound } + + specify { expect(resulting_direction).to eq('outbound') } + end + + context 'when the direction is inbound' do + let(:direction) { :inbound } + + specify { expect(resulting_direction).to eq('inbound') } + end + end end end diff --git a/spec/services/ci/job_token_scope/remove_project_service_spec.rb b/spec/services/ci/job_token_scope/remove_project_service_spec.rb index c3f9081cbd8..5b39f8908f2 100644 --- a/spec/services/ci/job_token_scope/remove_project_service_spec.rb +++ b/spec/services/ci/job_token_scope/remove_project_service_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true require 'spec_helper' -RSpec.describe Ci::JobTokenScope::RemoveProjectService do +RSpec.describe Ci::JobTokenScope::RemoveProjectService, feature_category: :continuous_integration do let(:service) { described_class.new(project, current_user) } let_it_be(:project) { create(:project, ci_outbound_job_token_scope_enabled: true).tap(&:save!) } @@ -23,7 +23,7 @@ RSpec.describe Ci::JobTokenScope::RemoveProjectService do end describe '#execute' do - subject(:result) { service.execute(target_project) } + subject(:result) { service.execute(target_project, :outbound) } it_behaves_like 'editable job token scope' do context 'when user has permissions on source and target project' do diff --git a/spec/services/ci/list_config_variables_service_spec.rb b/spec/services/ci/list_config_variables_service_spec.rb index 5b865914d1b..e2bbdefef7f 100644 --- a/spec/services/ci/list_config_variables_service_spec.rb +++ b/spec/services/ci/list_config_variables_service_spec.rb @@ -2,19 +2,21 @@ require 'spec_helper' -RSpec.describe Ci::ListConfigVariablesService, :use_clean_rails_memory_store_caching do +RSpec.describe Ci::ListConfigVariablesService, +:use_clean_rails_memory_store_caching, feature_category: :pipeline_authoring do include ReactiveCachingHelpers let(:ci_config) { {} } let(:files) { { '.gitlab-ci.yml' => YAML.dump(ci_config) } } let(:project) { create(:project, :custom_repo, :auto_devops_disabled, files: files) } let(:user) { project.creator } - let(:sha) { project.default_branch } + let(:ref) { project.default_branch } + let(:sha) { project.commit(ref).sha } let(:service) { described_class.new(project, user) } - subject(:result) { service.execute(sha) } + subject(:result) { service.execute(ref) } - context 'when sending a valid sha' do + context 'when sending a valid ref' do let(:ci_config) do { variables: { @@ -109,8 +111,8 @@ RSpec.describe Ci::ListConfigVariablesService, :use_clean_rails_memory_store_cac end end - context 'when sending an invalid sha' do - let(:sha) { 'invalid-sha' } + context 'when sending an invalid ref' do + let(:ref) { 'invalid-ref' } let(:ci_config) { nil } before do diff --git a/spec/services/ci/parse_dotenv_artifact_service_spec.rb b/spec/services/ci/parse_dotenv_artifact_service_spec.rb index 7b3af33ac72..f720375f05c 100644 --- a/spec/services/ci/parse_dotenv_artifact_service_spec.rb +++ b/spec/services/ci/parse_dotenv_artifact_service_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Ci::ParseDotenvArtifactService do +RSpec.describe Ci::ParseDotenvArtifactService, feature_category: :build_artifacts do let_it_be(:project) { create(:project) } let_it_be(:pipeline) { create(:ci_pipeline, project: project) } @@ -223,6 +223,18 @@ RSpec.describe Ci::ParseDotenvArtifactService do end end + context 'when blob is encoded in UTF-16 LE' do + let(:blob) { File.read(Rails.root.join('spec/fixtures/build_artifacts/dotenv_utf16_le.txt')) } + + it 'parses the dotenv data' do + subject + + expect(build.job_variables.as_json(only: [:key, :value])).to contain_exactly( + hash_including('key' => 'MY_ENV_VAR', 'value' => 'true'), + hash_including('key' => 'TEST2', 'value' => 'false')) + end + end + context 'when more than limitated variables are specified in dotenv' do let(:blob) do StringIO.new.tap do |s| diff --git a/spec/services/ci/pipeline_creation/cancel_redundant_pipelines_service_spec.rb b/spec/services/ci/pipeline_creation/cancel_redundant_pipelines_service_spec.rb new file mode 100644 index 00000000000..402bc2faa81 --- /dev/null +++ b/spec/services/ci/pipeline_creation/cancel_redundant_pipelines_service_spec.rb @@ -0,0 +1,250 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Ci::PipelineCreation::CancelRedundantPipelinesService, feature_category: :continuous_integration do + let_it_be(:project) { create(:project) } + let_it_be(:user) { create(:user) } + + let(:prev_pipeline) { create(:ci_pipeline, project: project) } + let!(:new_commit) { create(:commit, project: project) } + let(:pipeline) { create(:ci_pipeline, project: project, sha: new_commit.sha) } + + let(:service) { described_class.new(pipeline) } + + before do + create(:ci_build, :interruptible, :running, pipeline: prev_pipeline) + create(:ci_build, :interruptible, :success, pipeline: prev_pipeline) + create(:ci_build, :created, pipeline: prev_pipeline) + + create(:ci_build, :interruptible, pipeline: pipeline) + end + + describe '#execute!' do + subject(:execute) { service.execute } + + context 'when build statuses are set up correctly' do + it 'has builds of all statuses' do + expect(build_statuses(prev_pipeline)).to contain_exactly('running', 'success', 'created') + expect(build_statuses(pipeline)).to contain_exactly('pending') + end + end + + context 'when auto-cancel is enabled' do + before do + project.update!(auto_cancel_pending_pipelines: 'enabled') + end + + it 'cancels only previous interruptible builds' do + execute + + expect(build_statuses(prev_pipeline)).to contain_exactly('canceled', 'success', 'canceled') + expect(build_statuses(pipeline)).to contain_exactly('pending') + end + + it 'logs canceled pipelines' do + allow(Gitlab::AppLogger).to receive(:info) + + execute + + expect(Gitlab::AppLogger).to have_received(:info).with( + class: described_class.name, + message: "Pipeline #{pipeline.id} auto-canceling pipeline #{prev_pipeline.id}", + canceled_pipeline_id: prev_pipeline.id, + canceled_by_pipeline_id: pipeline.id, + canceled_by_pipeline_source: pipeline.source + ) + end + + it 'cancels the builds with 2 queries to avoid query timeout' do + second_query_regex = /WHERE "ci_pipelines"\."id" = \d+ AND \(NOT EXISTS/ + recorder = ActiveRecord::QueryRecorder.new { execute } + second_query = recorder.occurrences.keys.filter { |occ| occ =~ second_query_regex } + + expect(second_query).to be_one + end + + context 'when the previous pipeline has a child pipeline' do + let(:child_pipeline) { create(:ci_pipeline, child_of: prev_pipeline) } + + context 'with another nested child pipeline' do + let(:another_child_pipeline) { create(:ci_pipeline, child_of: child_pipeline) } + + before do + create(:ci_build, :interruptible, :running, pipeline: another_child_pipeline) + create(:ci_build, :interruptible, :running, pipeline: another_child_pipeline) + end + + it 'cancels all nested child pipeline builds' do + expect(build_statuses(another_child_pipeline)).to contain_exactly('running', 'running') + + execute + + expect(build_statuses(another_child_pipeline)).to contain_exactly('canceled', 'canceled') + end + end + + context 'when started after pipeline was finished' do + before do + create(:ci_build, :interruptible, :running, pipeline: child_pipeline) + prev_pipeline.update!(status: "success") + end + + it 'cancels child pipeline builds' do + expect(build_statuses(child_pipeline)).to contain_exactly('running') + + execute + + expect(build_statuses(child_pipeline)).to contain_exactly('canceled') + end + end + + context 'when the child pipeline has interruptible running jobs' do + before do + create(:ci_build, :interruptible, :running, pipeline: child_pipeline) + create(:ci_build, :interruptible, :running, pipeline: child_pipeline) + end + + it 'cancels all child pipeline builds' do + expect(build_statuses(child_pipeline)).to contain_exactly('running', 'running') + + execute + + expect(build_statuses(child_pipeline)).to contain_exactly('canceled', 'canceled') + end + + context 'when the child pipeline includes completed interruptible jobs' do + before do + create(:ci_build, :interruptible, :failed, pipeline: child_pipeline) + create(:ci_build, :interruptible, :success, pipeline: child_pipeline) + end + + it 'cancels all child pipeline builds with a cancelable_status' do + expect(build_statuses(child_pipeline)).to contain_exactly('running', 'running', 'failed', 'success') + + execute + + expect(build_statuses(child_pipeline)).to contain_exactly('canceled', 'canceled', 'failed', 'success') + end + end + end + + context 'when the child pipeline has started non-interruptible job' do + before do + create(:ci_build, :interruptible, :running, pipeline: child_pipeline) + # non-interruptible started + create(:ci_build, :success, pipeline: child_pipeline) + end + + it 'does not cancel any child pipeline builds' do + expect(build_statuses(child_pipeline)).to contain_exactly('running', 'success') + + execute + + expect(build_statuses(child_pipeline)).to contain_exactly('running', 'success') + end + end + + context 'when the child pipeline has non-interruptible non-started job' do + before do + create(:ci_build, :interruptible, :running, pipeline: child_pipeline) + end + + not_started_statuses = Ci::HasStatus::AVAILABLE_STATUSES - Ci::HasStatus::STARTED_STATUSES + context 'when the jobs are cancelable' do + cancelable_not_started_statuses = + Set.new(not_started_statuses).intersection(Ci::HasStatus::CANCELABLE_STATUSES) + cancelable_not_started_statuses.each do |status| + it "cancels all child pipeline builds when build status #{status} included" do + # non-interruptible but non-started + create(:ci_build, status.to_sym, pipeline: child_pipeline) + + expect(build_statuses(child_pipeline)).to contain_exactly('running', status) + + execute + + expect(build_statuses(child_pipeline)).to contain_exactly('canceled', 'canceled') + end + end + end + + context 'when the jobs are not cancelable' do + not_cancelable_not_started_statuses = not_started_statuses - Ci::HasStatus::CANCELABLE_STATUSES + not_cancelable_not_started_statuses.each do |status| + it "does not cancel child pipeline builds when build status #{status} included" do + # non-interruptible but non-started + create(:ci_build, status.to_sym, pipeline: child_pipeline) + + expect(build_statuses(child_pipeline)).to contain_exactly('running', status) + + execute + + expect(build_statuses(child_pipeline)).to contain_exactly('canceled', status) + end + end + end + end + end + + context 'when the pipeline is a child pipeline' do + let!(:parent_pipeline) { create(:ci_pipeline, project: project, sha: new_commit.sha) } + let(:pipeline) { create(:ci_pipeline, child_of: parent_pipeline) } + + before do + create(:ci_build, :interruptible, :running, pipeline: parent_pipeline) + create(:ci_build, :interruptible, :running, pipeline: parent_pipeline) + end + + it 'does not cancel any builds' do + expect(build_statuses(prev_pipeline)).to contain_exactly('running', 'success', 'created') + expect(build_statuses(parent_pipeline)).to contain_exactly('running', 'running') + + execute + + expect(build_statuses(prev_pipeline)).to contain_exactly('running', 'success', 'created') + expect(build_statuses(parent_pipeline)).to contain_exactly('running', 'running') + end + end + + context 'when the previous pipeline source is webide' do + let(:prev_pipeline) { create(:ci_pipeline, :webide, project: project) } + + it 'does not cancel builds of the previous pipeline' do + execute + + expect(build_statuses(prev_pipeline)).to contain_exactly('created', 'running', 'success') + expect(build_statuses(pipeline)).to contain_exactly('pending') + end + end + + it 'does not cancel future pipelines' do + expect(prev_pipeline.id).to be < pipeline.id + expect(build_statuses(pipeline)).to contain_exactly('pending') + expect(build_statuses(prev_pipeline)).to contain_exactly('running', 'success', 'created') + + described_class.new(prev_pipeline).execute + + expect(build_statuses(pipeline.reload)).to contain_exactly('pending') + end + end + + context 'when auto-cancel is disabled' do + before do + project.update!(auto_cancel_pending_pipelines: 'disabled') + end + + it 'does not cancel any build' do + subject + + expect(build_statuses(prev_pipeline)).to contain_exactly('running', 'success', 'created') + expect(build_statuses(pipeline)).to contain_exactly('pending') + end + end + end + + private + + def build_statuses(pipeline) + pipeline.builds.pluck(:status) + end +end diff --git a/spec/services/ci/pipeline_schedule_service_spec.rb b/spec/services/ci/pipeline_schedule_service_spec.rb index 2f094583f1a..8896d8ace30 100644 --- a/spec/services/ci/pipeline_schedule_service_spec.rb +++ b/spec/services/ci/pipeline_schedule_service_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Ci::PipelineScheduleService do +RSpec.describe Ci::PipelineScheduleService, feature_category: :continuous_integration do let_it_be(:user) { create(:user) } let_it_be(:project) { create(:project) } diff --git a/spec/services/ci/pipeline_schedules/update_service_spec.rb b/spec/services/ci/pipeline_schedules/update_service_spec.rb new file mode 100644 index 00000000000..838f49f6dea --- /dev/null +++ b/spec/services/ci/pipeline_schedules/update_service_spec.rb @@ -0,0 +1,78 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Ci::PipelineSchedules::UpdateService, feature_category: :continuous_integration do + let_it_be(:user) { create(:user) } + let_it_be(:reporter) { create(:user) } + let_it_be(:project) { create(:project, :public, :repository) } + let_it_be(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project, owner: user) } + + before_all do + project.add_maintainer(user) + project.add_reporter(reporter) + end + + describe "execute" do + context 'when user does not have permission' do + subject(:service) { described_class.new(pipeline_schedule, reporter, {}) } + + it 'returns ServiceResponse.error' do + result = service.execute + + expect(result).to be_a(ServiceResponse) + expect(result.error?).to be(true) + expect(result.message).to eq(_('The current user is not authorized to update the pipeline schedule')) + end + end + + context 'when user has permission' do + let(:params) do + { + description: 'updated_desc', + ref: 'patch-x', + active: false, + cron: '*/1 * * * *' + } + end + + subject(:service) { described_class.new(pipeline_schedule, user, params) } + + it 'updates database values with passed params' do + expect { service.execute } + .to change { pipeline_schedule.description }.from('pipeline schedule').to('updated_desc') + .and change { pipeline_schedule.ref }.from('master').to('patch-x') + .and change { pipeline_schedule.active }.from(true).to(false) + .and change { pipeline_schedule.cron }.from('0 1 * * *').to('*/1 * * * *') + end + + it 'returns ServiceResponse.success' do + result = service.execute + + expect(result).to be_a(ServiceResponse) + expect(result.success?).to be(true) + expect(result.payload.description).to eq('updated_desc') + end + + context 'when schedule update fails' do + subject(:service) { described_class.new(pipeline_schedule, user, {}) } + + before do + allow(pipeline_schedule).to receive(:update).and_return(false) + + errors = ActiveModel::Errors.new(pipeline_schedule) + errors.add(:base, 'An error occurred') + allow(pipeline_schedule).to receive(:errors).and_return(errors) + end + + it 'returns ServiceResponse.error' do + result = service.execute + + expect(result).to be_a(ServiceResponse) + expect(result.error?).to be(true) + expect(result.message).to match_array(['An error occurred']) + end + end + end + end +end diff --git a/spec/services/ci/register_job_service_spec.rb b/spec/services/ci/register_job_service_spec.rb index f40f5cc5a62..9183df359b4 100644 --- a/spec/services/ci/register_job_service_spec.rb +++ b/spec/services/ci/register_job_service_spec.rb @@ -3,795 +3,830 @@ require 'spec_helper' module Ci - RSpec.describe RegisterJobService do + RSpec.describe RegisterJobService, feature_category: :continuous_integration do let_it_be(:group) { create(:group) } let_it_be_with_reload(:project) { create(:project, group: group, shared_runners_enabled: false, group_runners_enabled: false) } let_it_be_with_reload(:pipeline) { create(:ci_pipeline, project: project) } - let!(:shared_runner) { create(:ci_runner, :instance) } - let!(:specific_runner) { create(:ci_runner, :project, projects: [project]) } + let_it_be(:shared_runner) { create(:ci_runner, :instance) } + let!(:project_runner) { create(:ci_runner, :project, projects: [project]) } let!(:group_runner) { create(:ci_runner, :group, groups: [group]) } let!(:pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline) } describe '#execute' do - subject { described_class.new(shared_runner).execute } + subject(:execute) { described_class.new(runner, runner_machine).execute } + + context 'with runner_machine specified' do + let(:runner) { project_runner } + let!(:runner_machine) { create(:ci_runner_machine, runner: project_runner) } - context 'checks database loadbalancing stickiness' do before do - project.update!(shared_runners_enabled: false) + pending_job.update!(tag_list: ["linux"]) + pending_job.reload + pending_job.create_queuing_entry! + project_runner.update!(tag_list: ["linux"]) end - it 'result is valid if replica did caught-up', :aggregate_failures do - expect(ApplicationRecord.sticking).to receive(:all_caught_up?) - .with(:runner, shared_runner.id) { true } + it 'sets runner_machine on job' do + expect { execute }.to change { pending_job.reload.runner_machine }.from(nil).to(runner_machine) - expect(subject).to be_valid - expect(subject.build).to be_nil - expect(subject.build_json).to be_nil + expect(execute.build).to eq(pending_job) end + end - it 'result is invalid if replica did not caught-up', :aggregate_failures do - expect(ApplicationRecord.sticking).to receive(:all_caught_up?) - .with(:runner, shared_runner.id) { false } + context 'with no runner machine' do + let(:runner_machine) { nil } - expect(subject).not_to be_valid - expect(subject.build).to be_nil - expect(subject.build_json).to be_nil - end - end + context 'checks database loadbalancing stickiness' do + let(:runner) { shared_runner } - shared_examples 'handles runner assignment' do - context 'runner follow tag list' do - it "picks build with the same tag" do - pending_job.update!(tag_list: ["linux"]) - pending_job.reload - pending_job.create_queuing_entry! - specific_runner.update!(tag_list: ["linux"]) - expect(execute(specific_runner)).to eq(pending_job) + before do + project.update!(shared_runners_enabled: false) end - it "does not pick build with different tag" do - pending_job.update!(tag_list: ["linux"]) - pending_job.reload - pending_job.create_queuing_entry! - specific_runner.update!(tag_list: ["win32"]) - expect(execute(specific_runner)).to be_falsey - end + it 'result is valid if replica did caught-up', :aggregate_failures do + expect(ApplicationRecord.sticking).to receive(:all_caught_up?).with(:runner, runner.id) { true } - it "picks build without tag" do - expect(execute(specific_runner)).to eq(pending_job) + expect(execute).to be_valid + expect(execute.build).to be_nil + expect(execute.build_json).to be_nil end - it "does not pick build with tag" do - pending_job.update!(tag_list: ["linux"]) - pending_job.reload - pending_job.create_queuing_entry! - expect(execute(specific_runner)).to be_falsey - end + it 'result is invalid if replica did not caught-up', :aggregate_failures do + expect(ApplicationRecord.sticking).to receive(:all_caught_up?) + .with(:runner, shared_runner.id) { false } - it "pick build without tag" do - specific_runner.update!(tag_list: ["win32"]) - expect(execute(specific_runner)).to eq(pending_job) + expect(subject).not_to be_valid + expect(subject.build).to be_nil + expect(subject.build_json).to be_nil end end - context 'deleted projects' do - before do - project.update!(pending_delete: true) - end + shared_examples 'handles runner assignment' do + context 'runner follow tag list' do + it "picks build with the same tag" do + pending_job.update!(tag_list: ["linux"]) + pending_job.reload + pending_job.create_queuing_entry! + project_runner.update!(tag_list: ["linux"]) + expect(build_on(project_runner)).to eq(pending_job) + end - context 'for shared runners' do - before do - project.update!(shared_runners_enabled: true) + it "does not pick build with different tag" do + pending_job.update!(tag_list: ["linux"]) + pending_job.reload + pending_job.create_queuing_entry! + project_runner.update!(tag_list: ["win32"]) + expect(build_on(project_runner)).to be_falsey end - it 'does not pick a build' do - expect(execute(shared_runner)).to be_nil + it "picks build without tag" do + expect(build_on(project_runner)).to eq(pending_job) end - end - context 'for specific runner' do - it 'does not pick a build' do - expect(execute(specific_runner)).to be_nil - expect(pending_job.reload).to be_failed - expect(pending_job.queuing_entry).to be_nil + it "does not pick build with tag" do + pending_job.update!(tag_list: ["linux"]) + pending_job.reload + pending_job.create_queuing_entry! + expect(build_on(project_runner)).to be_falsey end - end - end - context 'allow shared runners' do - before do - project.update!(shared_runners_enabled: true) - pipeline.reload - pending_job.reload - pending_job.create_queuing_entry! + it "pick build without tag" do + project_runner.update!(tag_list: ["win32"]) + expect(build_on(project_runner)).to eq(pending_job) + end end - context 'when build owner has been blocked' do - let(:user) { create(:user, :blocked) } - + context 'deleted projects' do before do - pending_job.update!(user: user) + project.update!(pending_delete: true) end - it 'does not pick the build and drops the build' do - expect(execute(shared_runner)).to be_falsey + context 'for shared runners' do + before do + project.update!(shared_runners_enabled: true) + end - expect(pending_job.reload).to be_user_blocked + it 'does not pick a build' do + expect(build_on(shared_runner)).to be_nil + end + end + + context 'for project runner' do + it 'does not pick a build' do + expect(build_on(project_runner)).to be_nil + expect(pending_job.reload).to be_failed + expect(pending_job.queuing_entry).to be_nil + end end end - context 'for multiple builds' do - let!(:project2) { create :project, shared_runners_enabled: true } - let!(:pipeline2) { create :ci_pipeline, project: project2 } - let!(:project3) { create :project, shared_runners_enabled: true } - let!(:pipeline3) { create :ci_pipeline, project: project3 } - let!(:build1_project1) { pending_job } - let!(:build2_project1) { create(:ci_build, :pending, :queued, pipeline: pipeline) } - let!(:build3_project1) { create(:ci_build, :pending, :queued, pipeline: pipeline) } - let!(:build1_project2) { create(:ci_build, :pending, :queued, pipeline: pipeline2) } - 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 - # it gets for one build from each of the projects - expect(execute(shared_runner)).to eq(build1_project1) - expect(execute(shared_runner)).to eq(build1_project2) - expect(execute(shared_runner)).to eq(build1_project3) - - # then it gets a second build from each of the projects - expect(execute(shared_runner)).to eq(build2_project1) - expect(execute(shared_runner)).to eq(build2_project2) - - # in the end the third build - expect(execute(shared_runner)).to eq(build3_project1) - end + context 'allow shared runners' do + before do + project.update!(shared_runners_enabled: true) + pipeline.reload + pending_job.reload + pending_job.create_queuing_entry! + end + + context 'when build owner has been blocked' do + let(:user) { create(:user, :blocked) } + + before do + pending_job.update!(user: user) end - context 'when some builds transition to success' do - it 'equalises number of running builds' do - # after finishing the first build for project 1, get a second build from the same project - expect(execute(shared_runner)).to eq(build1_project1) - build1_project1.reload.success - expect(execute(shared_runner)).to eq(build2_project1) + it 'does not pick the build and drops the build' do + expect(build_on(shared_runner)).to be_falsey - expect(execute(shared_runner)).to eq(build1_project2) - build1_project2.reload.success - expect(execute(shared_runner)).to eq(build2_project2) - expect(execute(shared_runner)).to eq(build1_project3) - expect(execute(shared_runner)).to eq(build3_project1) - end + expect(pending_job.reload).to be_user_blocked end end - context 'when using DEFCON mode that disables fair scheduling' do - before do - stub_feature_flags(ci_queueing_disaster_recovery_disable_fair_scheduling: true) - end - - context 'when all builds are pending' do - it 'returns builds in order of creation (FIFO)' do - # it gets for one build from each of the projects - expect(execute(shared_runner)).to eq(build1_project1) - expect(execute(shared_runner)).to eq(build2_project1) - expect(execute(shared_runner)).to eq(build3_project1) - expect(execute(shared_runner)).to eq(build1_project2) - expect(execute(shared_runner)).to eq(build2_project2) - expect(execute(shared_runner)).to eq(build1_project3) + context 'for multiple builds' do + let!(:project2) { create :project, shared_runners_enabled: true } + let!(:pipeline2) { create :ci_pipeline, project: project2 } + let!(:project3) { create :project, shared_runners_enabled: true } + let!(:pipeline3) { create :ci_pipeline, project: project3 } + let!(:build1_project1) { pending_job } + let!(:build2_project1) { create(:ci_build, :pending, :queued, pipeline: pipeline) } + let!(:build3_project1) { create(:ci_build, :pending, :queued, pipeline: pipeline) } + let!(:build1_project2) { create(:ci_build, :pending, :queued, pipeline: pipeline2) } + 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(build_on(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 + # it gets for one build from each of the projects + expect(build_on(shared_runner)).to eq(build1_project1) + expect(build_on(shared_runner)).to eq(build1_project2) + expect(build_on(shared_runner)).to eq(build1_project3) + + # then it gets a second build from each of the projects + expect(build_on(shared_runner)).to eq(build2_project1) + expect(build_on(shared_runner)).to eq(build2_project2) + + # in the end the third build + expect(build_on(shared_runner)).to eq(build3_project1) + end + end + + context 'when some builds transition to success' do + it 'equalises number of running builds' do + # after finishing the first build for project 1, get a second build from the same project + expect(build_on(shared_runner)).to eq(build1_project1) + build1_project1.reload.success + expect(build_on(shared_runner)).to eq(build2_project1) + + expect(build_on(shared_runner)).to eq(build1_project2) + build1_project2.reload.success + expect(build_on(shared_runner)).to eq(build2_project2) + expect(build_on(shared_runner)).to eq(build1_project3) + expect(build_on(shared_runner)).to eq(build3_project1) + end end end - context 'when some builds transition to success' do - it 'returns builds in order of creation (FIFO)' do - expect(execute(shared_runner)).to eq(build1_project1) - build1_project1.reload.success - expect(execute(shared_runner)).to eq(build2_project1) + context 'when using DEFCON mode that disables fair scheduling' do + before do + stub_feature_flags(ci_queueing_disaster_recovery_disable_fair_scheduling: true) + end + + context 'when all builds are pending' do + it 'returns builds in order of creation (FIFO)' do + # it gets for one build from each of the projects + expect(build_on(shared_runner)).to eq(build1_project1) + expect(build_on(shared_runner)).to eq(build2_project1) + expect(build_on(shared_runner)).to eq(build3_project1) + expect(build_on(shared_runner)).to eq(build1_project2) + expect(build_on(shared_runner)).to eq(build2_project2) + expect(build_on(shared_runner)).to eq(build1_project3) + end + end - expect(execute(shared_runner)).to eq(build3_project1) - build2_project1.reload.success - expect(execute(shared_runner)).to eq(build1_project2) - expect(execute(shared_runner)).to eq(build2_project2) - expect(execute(shared_runner)).to eq(build1_project3) + context 'when some builds transition to success' do + it 'returns builds in order of creation (FIFO)' do + expect(build_on(shared_runner)).to eq(build1_project1) + build1_project1.reload.success + expect(build_on(shared_runner)).to eq(build2_project1) + + expect(build_on(shared_runner)).to eq(build3_project1) + build2_project1.reload.success + expect(build_on(shared_runner)).to eq(build1_project2) + expect(build_on(shared_runner)).to eq(build2_project2) + expect(build_on(shared_runner)).to eq(build1_project3) + end end end end - end - context 'shared runner' do - let(:response) { described_class.new(shared_runner).execute } - let(:build) { response.build } + context 'shared runner' do + let(:response) { described_class.new(shared_runner, nil).execute } + let(:build) { response.build } - it { expect(build).to be_kind_of(Build) } - it { expect(build).to be_valid } - it { expect(build).to be_running } - it { expect(build.runner).to eq(shared_runner) } - it { expect(Gitlab::Json.parse(response.build_json)['id']).to eq(build.id) } - end + it { expect(build).to be_kind_of(Build) } + it { expect(build).to be_valid } + it { expect(build).to be_running } + it { expect(build.runner).to eq(shared_runner) } + it { expect(Gitlab::Json.parse(response.build_json)['id']).to eq(build.id) } + end - context 'specific runner' do - let(:build) { execute(specific_runner) } + context 'project runner' do + let(:build) { build_on(project_runner) } - it { expect(build).to be_kind_of(Build) } - it { expect(build).to be_valid } - it { expect(build).to be_running } - it { expect(build.runner).to eq(specific_runner) } + it { expect(build).to be_kind_of(Build) } + it { expect(build).to be_valid } + it { expect(build).to be_running } + it { expect(build.runner).to eq(project_runner) } + end end - end - context 'disallow shared runners' do - before do - project.update!(shared_runners_enabled: false) - end + context 'disallow shared runners' do + before do + project.update!(shared_runners_enabled: false) + end - context 'shared runner' do - let(:build) { execute(shared_runner) } + context 'shared runner' do + let(:build) { build_on(shared_runner) } - it { expect(build).to be_nil } - end + it { expect(build).to be_nil } + end - context 'specific runner' do - let(:build) { execute(specific_runner) } + context 'project runner' do + let(:build) { build_on(project_runner) } - it { expect(build).to be_kind_of(Build) } - it { expect(build).to be_valid } - it { expect(build).to be_running } - it { expect(build.runner).to eq(specific_runner) } + it { expect(build).to be_kind_of(Build) } + it { expect(build).to be_valid } + it { expect(build).to be_running } + it { expect(build.runner).to eq(project_runner) } + end end - end - context 'disallow when builds are disabled' do - before do - project.update!(shared_runners_enabled: true, group_runners_enabled: true) - project.project_feature.update_attribute(:builds_access_level, ProjectFeature::DISABLED) + context 'disallow when builds are disabled' do + before do + project.update!(shared_runners_enabled: true, group_runners_enabled: true) + project.project_feature.update_attribute(:builds_access_level, ProjectFeature::DISABLED) - pending_job.reload.create_queuing_entry! - end + pending_job.reload.create_queuing_entry! + end - context 'and uses shared runner' do - let(:build) { execute(shared_runner) } + context 'and uses shared runner' do + let(:build) { build_on(shared_runner) } - it { expect(build).to be_nil } - end + it { expect(build).to be_nil } + end - context 'and uses group runner' do - let(:build) { execute(group_runner) } + context 'and uses group runner' do + let(:build) { build_on(group_runner) } - it { expect(build).to be_nil } - end + it { expect(build).to be_nil } + end - context 'and uses project runner' do - let(:build) { execute(specific_runner) } + context 'and uses project runner' do + let(:build) { build_on(project_runner) } - it 'does not pick a build' do - expect(build).to be_nil - expect(pending_job.reload).to be_failed - expect(pending_job.queuing_entry).to be_nil + it 'does not pick a build' do + expect(build).to be_nil + expect(pending_job.reload).to be_failed + expect(pending_job.queuing_entry).to be_nil + end end end - end - context 'allow group runners' do - before do - project.update!(group_runners_enabled: true) - end + context 'allow group runners' do + before do + project.update!(group_runners_enabled: true) + end - context 'for multiple builds' do - let!(:project2) { create(:project, group_runners_enabled: true, group: group) } - let!(:pipeline2) { create(:ci_pipeline, project: project2) } - let!(:project3) { create(:project, group_runners_enabled: true, group: group) } - let!(:pipeline3) { create(:ci_pipeline, project: project3) } + context 'for multiple builds' do + let!(:project2) { create(:project, group_runners_enabled: true, group: group) } + let!(:pipeline2) { create(:ci_pipeline, project: project2) } + let!(:project3) { create(:project, group_runners_enabled: true, group: group) } + let!(:pipeline3) { create(:ci_pipeline, project: project3) } - let!(:build1_project1) { pending_job } - let!(:build2_project1) { create(:ci_build, :queued, pipeline: pipeline) } - let!(:build3_project1) { create(:ci_build, :queued, pipeline: pipeline) } - let!(:build1_project2) { create(:ci_build, :queued, pipeline: pipeline2) } - let!(:build2_project2) { create(:ci_build, :queued, pipeline: pipeline2) } - let!(:build1_project3) { create(:ci_build, :queued, pipeline: pipeline3) } + let!(:build1_project1) { pending_job } + let!(:build2_project1) { create(:ci_build, :queued, pipeline: pipeline) } + let!(:build3_project1) { create(:ci_build, :queued, pipeline: pipeline) } + let!(:build1_project2) { create(:ci_build, :queued, pipeline: pipeline2) } + let!(:build2_project2) { create(:ci_build, :queued, pipeline: pipeline2) } + let!(:build1_project3) { create(:ci_build, :queued, pipeline: pipeline3) } - # these shouldn't influence the scheduling - let!(:unrelated_group) { create(:group) } - let!(:unrelated_project) { create(:project, group_runners_enabled: true, group: unrelated_group) } - let!(:unrelated_pipeline) { create(:ci_pipeline, project: unrelated_project) } - let!(:build1_unrelated_project) { create(:ci_build, :pending, :queued, pipeline: unrelated_pipeline) } - let!(:unrelated_group_runner) { create(:ci_runner, :group, groups: [unrelated_group]) } + # these shouldn't influence the scheduling + let!(:unrelated_group) { create(:group) } + let!(:unrelated_project) { create(:project, group_runners_enabled: true, group: unrelated_group) } + let!(:unrelated_pipeline) { create(:ci_pipeline, project: unrelated_project) } + let!(:build1_unrelated_project) { create(:ci_build, :pending, :queued, pipeline: unrelated_pipeline) } + let!(:unrelated_group_runner) { create(:ci_runner, :group, groups: [unrelated_group]) } - it 'does not consider builds from other group runners' do - queue = ::Ci::Queue::BuildQueueService.new(group_runner) + it 'does not consider builds from other group runners' do + queue = ::Ci::Queue::BuildQueueService.new(group_runner) - expect(queue.builds_for_group_runner.size).to eq 6 - execute(group_runner) + expect(queue.builds_for_group_runner.size).to eq 6 + build_on(group_runner) - expect(queue.builds_for_group_runner.size).to eq 5 - execute(group_runner) + expect(queue.builds_for_group_runner.size).to eq 5 + build_on(group_runner) - expect(queue.builds_for_group_runner.size).to eq 4 - execute(group_runner) + expect(queue.builds_for_group_runner.size).to eq 4 + build_on(group_runner) - expect(queue.builds_for_group_runner.size).to eq 3 - execute(group_runner) + expect(queue.builds_for_group_runner.size).to eq 3 + build_on(group_runner) - expect(queue.builds_for_group_runner.size).to eq 2 - execute(group_runner) + expect(queue.builds_for_group_runner.size).to eq 2 + build_on(group_runner) - expect(queue.builds_for_group_runner.size).to eq 1 - execute(group_runner) + expect(queue.builds_for_group_runner.size).to eq 1 + build_on(group_runner) - expect(queue.builds_for_group_runner.size).to eq 0 - expect(execute(group_runner)).to be_nil + expect(queue.builds_for_group_runner.size).to eq 0 + expect(build_on(group_runner)).to be_nil + end end - end - context 'group runner' do - let(:build) { execute(group_runner) } + context 'group runner' do + let(:build) { build_on(group_runner) } - it { expect(build).to be_kind_of(Build) } - it { expect(build).to be_valid } - it { expect(build).to be_running } - it { expect(build.runner).to eq(group_runner) } + it { expect(build).to be_kind_of(Build) } + it { expect(build).to be_valid } + it { expect(build).to be_running } + it { expect(build.runner).to eq(group_runner) } + end end - end - context 'disallow group runners' do - before do - project.update!(group_runners_enabled: false) + context 'disallow group runners' do + before do + project.update!(group_runners_enabled: false) - pending_job.reload.create_queuing_entry! - end + pending_job.reload.create_queuing_entry! + end - context 'group runner' do - let(:build) { execute(group_runner) } + context 'group runner' do + let(:build) { build_on(group_runner) } - it { expect(build).to be_nil } + it { expect(build).to be_nil } + end end - end - context 'when first build is stalled' do - before do - allow_any_instance_of(Ci::RegisterJobService).to receive(:assign_runner!).and_call_original - allow_any_instance_of(Ci::RegisterJobService).to receive(:assign_runner!) - .with(pending_job, anything).and_raise(ActiveRecord::StaleObjectError) - end + context 'when first build is stalled' do + before do + allow_any_instance_of(Ci::RegisterJobService).to receive(:assign_runner!).and_call_original + allow_any_instance_of(Ci::RegisterJobService).to receive(:assign_runner!) + .with(pending_job, anything).and_raise(ActiveRecord::StaleObjectError) + end - subject { described_class.new(specific_runner).execute } + subject { described_class.new(project_runner, nil).execute } - context 'with multiple builds are in queue' do - let!(:other_build) { create(:ci_build, :pending, :queued, pipeline: pipeline) } + context 'with multiple builds are in queue' do + let!(:other_build) { create(:ci_build, :pending, :queued, pipeline: pipeline) } - before do - allow_any_instance_of(::Ci::Queue::BuildQueueService) - .to receive(:execute) - .and_return(Ci::Build.where(id: [pending_job, other_build]).pluck(:id)) - end + before do + allow_any_instance_of(::Ci::Queue::BuildQueueService) + .to receive(:execute) + .and_return(Ci::Build.where(id: [pending_job, other_build]).pluck(:id)) + end - it "receives second build from the queue" do - expect(subject).to be_valid - expect(subject.build).to eq(other_build) + it "receives second build from the queue" do + expect(subject).to be_valid + expect(subject.build).to eq(other_build) + end end - end - context 'when single build is in queue' do - before do - allow_any_instance_of(::Ci::Queue::BuildQueueService) - .to receive(:execute) - .and_return(Ci::Build.where(id: pending_job).pluck(:id)) - end + context 'when single build is in queue' do + before do + allow_any_instance_of(::Ci::Queue::BuildQueueService) + .to receive(:execute) + .and_return(Ci::Build.where(id: pending_job).pluck(:id)) + end - it "does not receive any valid result" do - expect(subject).not_to be_valid + it "does not receive any valid result" do + expect(subject).not_to be_valid + end end - end - context 'when there is no build in queue' do - before do - allow_any_instance_of(::Ci::Queue::BuildQueueService) - .to receive(:execute) - .and_return([]) - end + context 'when there is no build in queue' do + before do + allow_any_instance_of(::Ci::Queue::BuildQueueService) + .to receive(:execute) + .and_return([]) + end - it "does not receive builds but result is valid" do - expect(subject).to be_valid - expect(subject.build).to be_nil + it "does not receive builds but result is valid" do + expect(subject).to be_valid + expect(subject.build).to be_nil + end end end - end - context 'when access_level of runner is not_protected' do - let!(:specific_runner) { create(:ci_runner, :project, projects: [project]) } + context 'when access_level of runner is not_protected' do + let!(:project_runner) { create(:ci_runner, :project, projects: [project]) } - context 'when a job is protected' do - let!(:pending_job) { create(:ci_build, :pending, :queued, :protected, pipeline: pipeline) } + context 'when a job is protected' do + let!(:pending_job) { create(:ci_build, :pending, :queued, :protected, pipeline: pipeline) } - it 'picks the job' do - expect(execute(specific_runner)).to eq(pending_job) + it 'picks the job' do + expect(build_on(project_runner)).to eq(pending_job) + end end - end - context 'when a job is unprotected' do - let!(:pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline) } + context 'when a job is unprotected' do + let!(:pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline) } - it 'picks the job' do - expect(execute(specific_runner)).to eq(pending_job) + it 'picks the job' do + expect(build_on(project_runner)).to eq(pending_job) + end end - end - context 'when protected attribute of a job is nil' do - let!(:pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline) } + context 'when protected attribute of a job is nil' do + let!(:pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline) } - before do - pending_job.update_attribute(:protected, nil) - end + before do + pending_job.update_attribute(:protected, nil) + end - it 'picks the job' do - expect(execute(specific_runner)).to eq(pending_job) + it 'picks the job' do + expect(build_on(project_runner)).to eq(pending_job) + end end end - end - context 'when access_level of runner is ref_protected' do - let!(:specific_runner) { create(:ci_runner, :project, :ref_protected, projects: [project]) } + context 'when access_level of runner is ref_protected' do + let!(:project_runner) { create(:ci_runner, :project, :ref_protected, projects: [project]) } - context 'when a job is protected' do - let!(:pending_job) { create(:ci_build, :pending, :queued, :protected, pipeline: pipeline) } + context 'when a job is protected' do + let!(:pending_job) { create(:ci_build, :pending, :queued, :protected, pipeline: pipeline) } - it 'picks the job' do - expect(execute(specific_runner)).to eq(pending_job) + it 'picks the job' do + expect(build_on(project_runner)).to eq(pending_job) + end end - end - context 'when a job is unprotected' do - let!(:pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline) } + context 'when a job is unprotected' do + let!(:pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline) } - it 'does not pick the job' do - expect(execute(specific_runner)).to be_nil + it 'does not pick the job' do + expect(build_on(project_runner)).to be_nil + end end - end - context 'when protected attribute of a job is nil' do - let!(:pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline) } + context 'when protected attribute of a job is nil' do + let!(:pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline) } - before do - pending_job.update_attribute(:protected, nil) - end + before do + pending_job.update_attribute(:protected, nil) + end - it 'does not pick the job' do - expect(execute(specific_runner)).to be_nil + it 'does not pick the job' do + expect(build_on(project_runner)).to be_nil + end end end - end - context 'runner feature set is verified' do - let(:options) { { artifacts: { reports: { junit: "junit.xml" } } } } - let!(:pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline, options: options) } + context 'runner feature set is verified' do + let(:options) { { artifacts: { reports: { junit: "junit.xml" } } } } + let!(:pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline, options: options) } - subject { execute(specific_runner, params) } + subject { build_on(project_runner, params: params) } - context 'when feature is missing by runner' do - let(:params) { {} } + context 'when feature is missing by runner' do + let(:params) { {} } - it 'does not pick the build and drops the build' do - expect(subject).to be_nil - expect(pending_job.reload).to be_failed - expect(pending_job).to be_runner_unsupported + it 'does not pick the build and drops the build' do + expect(subject).to be_nil + expect(pending_job.reload).to be_failed + expect(pending_job).to be_runner_unsupported + end end - end - context 'when feature is supported by runner' do - let(:params) do - { info: { features: { upload_multiple_artifacts: true } } } - end + context 'when feature is supported by runner' do + let(:params) do + { info: { features: { upload_multiple_artifacts: true } } } + end - it 'does pick job' do - expect(subject).not_to be_nil + it 'does pick job' do + expect(subject).not_to be_nil + end end end - 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 + 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(:dependencies) { %w[test] } + let!(:pending_job) do + create(:ci_build, :pending, :queued, + pipeline: pipeline, stage_idx: 1, + options: { script: ["bash"], dependencies: dependencies }) + end - subject { execute(specific_runner) } + let(:dependencies) { %w[test] } - it 'picks a build with a dependency' do - picked_build = execute(specific_runner) + subject { build_on(project_runner) } - expect(picked_build).to be_present - end + it 'picks a build with a dependency' do + picked_build = build_on(project_runner) - 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) + expect(picked_build).to be_present 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 + 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 - 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 + let(:dependencies) { %w[test deploy] } - shared_examples 'not pick' do - it 'does not pick the build and drops the build' do - expect(subject).to be_nil - expect(pending_job.reload).to be_failed - expect(pending_job).to be_missing_dependency_failure - end - end + it 'logs build artifacts size' do + build_on(project_runner) - shared_examples 'validation is active' do - context 'when depended job has not been completed yet' do - let!(:pre_stage_job) { create(:ci_build, :pending, :queued, :manual, pipeline: pipeline, name: 'test', stage_idx: 0) } + artifacts_size = [pre_stage_job, pre_stage_job_second].sum do |job| + job.job_artifacts_archive.size + end - it { is_expected.to eq(pending_job) } + 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 - context 'when artifacts of depended job has been expired' do - let!(:pre_stage_job) { create(:ci_build, :success, :expired, pipeline: pipeline, name: 'test', stage_idx: 0) } + shared_examples 'not pick' do + it 'does not pick the build and drops the build' do + expect(subject).to be_nil + expect(pending_job.reload).to be_failed + expect(pending_job).to be_missing_dependency_failure + end + end - context 'when the pipeline is locked' do - before do - pipeline.artifacts_locked! + shared_examples 'validation is active' do + context 'when depended job has not been completed yet' do + let!(:pre_stage_job) do + create(:ci_build, :pending, :queued, :manual, pipeline: pipeline, name: 'test', stage_idx: 0) end it { is_expected.to eq(pending_job) } end - context 'when the pipeline is unlocked' do - before do - pipeline.unlocked! + context 'when artifacts of depended job has been expired' do + let!(:pre_stage_job) do + create(:ci_build, :success, :expired, pipeline: pipeline, name: 'test', stage_idx: 0) end - it_behaves_like 'not pick' + context 'when the pipeline is locked' do + before do + pipeline.artifacts_locked! + end + + it { is_expected.to eq(pending_job) } + end + + context 'when the pipeline is unlocked' do + before do + pipeline.unlocked! + end + + it_behaves_like 'not pick' + end end - end - context 'when artifacts of depended job has been erased' do - let!(:pre_stage_job) { create(:ci_build, :success, pipeline: pipeline, name: 'test', stage_idx: 0, erased_at: 1.minute.ago) } + context 'when artifacts of depended job has been erased' do + let!(:pre_stage_job) do + create(:ci_build, :success, pipeline: pipeline, name: 'test', stage_idx: 0, erased_at: 1.minute.ago) + end - it_behaves_like 'not pick' - end + it_behaves_like 'not pick' + end - context 'when job object is staled' do - let!(:pre_stage_job) { create(:ci_build, :success, :expired, pipeline: pipeline, name: 'test', stage_idx: 0) } + context 'when job object is staled' do + let!(:pre_stage_job) do + create(:ci_build, :success, :expired, pipeline: pipeline, name: 'test', stage_idx: 0) + end - before do - pipeline.unlocked! + before do + pipeline.unlocked! - allow_next_instance_of(Ci::Build) do |build| - expect(build).to receive(:drop!) - .and_raise(ActiveRecord::StaleObjectError.new(pending_job, :drop!)) + allow_next_instance_of(Ci::Build) do |build| + expect(build).to receive(:drop!) + .and_raise(ActiveRecord::StaleObjectError.new(pending_job, :drop!)) + end end - end - it 'does not drop nor pick' do - expect(subject).to be_nil + it 'does not drop nor pick' do + expect(subject).to be_nil + end end end - end - shared_examples 'validation is not active' do - context 'when depended job has not been completed yet' do - let!(:pre_stage_job) { create(:ci_build, :pending, :queued, :manual, pipeline: pipeline, name: 'test', stage_idx: 0) } + shared_examples 'validation is not active' do + context 'when depended job has not been completed yet' do + let!(:pre_stage_job) do + create(:ci_build, :pending, :queued, :manual, pipeline: pipeline, name: 'test', stage_idx: 0) + end - it { expect(subject).to eq(pending_job) } - end + it { expect(subject).to eq(pending_job) } + end - context 'when artifacts of depended job has been expired' do - let!(:pre_stage_job) { create(:ci_build, :success, :expired, pipeline: pipeline, name: 'test', stage_idx: 0) } + context 'when artifacts of depended job has been expired' do + let!(:pre_stage_job) do + create(:ci_build, :success, :expired, pipeline: pipeline, name: 'test', stage_idx: 0) + end - it { expect(subject).to eq(pending_job) } - end + it { expect(subject).to eq(pending_job) } + end - context 'when artifacts of depended job has been erased' do - let!(:pre_stage_job) { create(:ci_build, :success, pipeline: pipeline, name: 'test', stage_idx: 0, erased_at: 1.minute.ago) } + context 'when artifacts of depended job has been erased' do + let!(:pre_stage_job) do + create(:ci_build, :success, pipeline: pipeline, name: 'test', stage_idx: 0, erased_at: 1.minute.ago) + end - it { expect(subject).to eq(pending_job) } + it { expect(subject).to eq(pending_job) } + end end - end - it_behaves_like 'validation is active' - end + it_behaves_like 'validation is active' + end - context 'when build is degenerated' do - let!(:pending_job) { create(:ci_build, :pending, :queued, :degenerated, pipeline: pipeline) } + context 'when build is degenerated' do + let!(:pending_job) { create(:ci_build, :pending, :queued, :degenerated, pipeline: pipeline) } - subject { execute(specific_runner, {}) } + subject { build_on(project_runner) } - it 'does not pick the build and drops the build' do - expect(subject).to be_nil + it 'does not pick the build and drops the build' do + expect(subject).to be_nil - pending_job.reload - expect(pending_job).to be_failed - expect(pending_job).to be_archived_failure + pending_job.reload + expect(pending_job).to be_failed + expect(pending_job).to be_archived_failure + end end - end - context 'when build has data integrity problem' do - let!(:pending_job) do - create(:ci_build, :pending, :queued, pipeline: pipeline) - end + context 'when build has data integrity problem' do + let!(:pending_job) do + create(:ci_build, :pending, :queued, pipeline: pipeline) + end - before do - pending_job.update_columns(options: "string") - end + before do + pending_job.update_columns(options: "string") + end - subject { execute(specific_runner, {}) } + subject { build_on(project_runner) } - it 'does drop the build and logs both failures' do - expect(Gitlab::ErrorTracking).to receive(:track_exception) - .with(anything, a_hash_including(build_id: pending_job.id)) - .twice - .and_call_original + it 'does drop the build and logs both failures' do + expect(Gitlab::ErrorTracking).to receive(:track_exception) + .with(anything, a_hash_including(build_id: pending_job.id)) + .twice + .and_call_original - expect(subject).to be_nil + expect(subject).to be_nil - pending_job.reload - expect(pending_job).to be_failed - expect(pending_job).to be_data_integrity_failure + pending_job.reload + expect(pending_job).to be_failed + expect(pending_job).to be_data_integrity_failure + end end - end - context 'when build fails to be run!' do - let!(:pending_job) do - create(:ci_build, :pending, :queued, pipeline: pipeline) - end + context 'when build fails to be run!' do + let!(:pending_job) do + create(:ci_build, :pending, :queued, pipeline: pipeline) + end - before do - expect_any_instance_of(Ci::Build).to receive(:run!) - .and_raise(RuntimeError, 'scheduler error') - end + before do + expect_any_instance_of(Ci::Build).to receive(:run!) + .and_raise(RuntimeError, 'scheduler error') + end - subject { execute(specific_runner, {}) } + subject { build_on(project_runner) } - it 'does drop the build and logs failure' do - expect(Gitlab::ErrorTracking).to receive(:track_exception) - .with(anything, a_hash_including(build_id: pending_job.id)) - .once - .and_call_original + it 'does drop the build and logs failure' do + expect(Gitlab::ErrorTracking).to receive(:track_exception) + .with(anything, a_hash_including(build_id: pending_job.id)) + .once + .and_call_original - expect(subject).to be_nil + expect(subject).to be_nil - pending_job.reload - expect(pending_job).to be_failed - expect(pending_job).to be_scheduler_failure + pending_job.reload + expect(pending_job).to be_failed + expect(pending_job).to be_scheduler_failure + end end - end - context 'when an exception is raised during a persistent ref creation' do - before do - allow_any_instance_of(Ci::PersistentRef).to receive(:exist?) { false } - allow_any_instance_of(Ci::PersistentRef).to receive(:create_ref) { raise ArgumentError } - end + context 'when an exception is raised during a persistent ref creation' do + before do + allow_any_instance_of(Ci::PersistentRef).to receive(:exist?) { false } + allow_any_instance_of(Ci::PersistentRef).to receive(:create_ref) { raise ArgumentError } + end - subject { execute(specific_runner, {}) } + subject { build_on(project_runner) } - it 'picks the build' do - expect(subject).to eq(pending_job) + it 'picks the build' do + expect(subject).to eq(pending_job) - pending_job.reload - expect(pending_job).to be_running - end - end - - context 'when only some builds can be matched by runner' do - let!(:specific_runner) { create(:ci_runner, :project, projects: [project], tag_list: %w[matching]) } - let!(:pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline, tag_list: %w[matching]) } - - before do - # create additional matching and non-matching jobs - create_list(:ci_build, 2, :pending, :queued, pipeline: pipeline, tag_list: %w[matching]) - create(:ci_build, :pending, :queued, pipeline: pipeline, tag_list: %w[non-matching]) + pending_job.reload + expect(pending_job).to be_running + end end - it 'observes queue size of only matching jobs' do - # pending_job + 2 x matching ones - expect(Gitlab::Ci::Queue::Metrics.queue_size_total).to receive(:observe) - .with({ runner_type: specific_runner.runner_type }, 3) + context 'when only some builds can be matched by runner' do + let!(:project_runner) { create(:ci_runner, :project, projects: [project], tag_list: %w[matching]) } + let!(:pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline, tag_list: %w[matching]) } - expect(execute(specific_runner)).to eq(pending_job) - end + before do + # create additional matching and non-matching jobs + create_list(:ci_build, 2, :pending, :queued, pipeline: pipeline, tag_list: %w[matching]) + create(:ci_build, :pending, :queued, pipeline: pipeline, tag_list: %w[non-matching]) + end - it 'observes queue processing time by the runner type' do - expect(Gitlab::Ci::Queue::Metrics.queue_iteration_duration_seconds) - .to receive(:observe) - .with({ runner_type: specific_runner.runner_type }, anything) + it 'observes queue size of only matching jobs' do + # pending_job + 2 x matching ones + expect(Gitlab::Ci::Queue::Metrics.queue_size_total).to receive(:observe) + .with({ runner_type: project_runner.runner_type }, 3) - expect(Gitlab::Ci::Queue::Metrics.queue_retrieval_duration_seconds) - .to receive(:observe) - .with({ runner_type: specific_runner.runner_type }, anything) + expect(build_on(project_runner)).to eq(pending_job) + end - expect(execute(specific_runner)).to eq(pending_job) - end - end + it 'observes queue processing time by the runner type' do + expect(Gitlab::Ci::Queue::Metrics.queue_iteration_duration_seconds) + .to receive(:observe) + .with({ runner_type: project_runner.runner_type }, anything) - context 'when ci_register_job_temporary_lock is enabled' do - before do - stub_feature_flags(ci_register_job_temporary_lock: true) + expect(Gitlab::Ci::Queue::Metrics.queue_retrieval_duration_seconds) + .to receive(:observe) + .with({ runner_type: project_runner.runner_type }, anything) - allow(Gitlab::Ci::Queue::Metrics.queue_operations_total).to receive(:increment) + expect(build_on(project_runner)).to eq(pending_job) + end end - context 'when a build is temporarily locked' do - let(:service) { described_class.new(specific_runner) } - + context 'when ci_register_job_temporary_lock is enabled' do before do - service.send(:acquire_temporary_lock, pending_job.id) - end - - it 'skips this build and marks queue as invalid' do - expect(Gitlab::Ci::Queue::Metrics.queue_operations_total).to receive(:increment) - .with(operation: :queue_iteration) - expect(Gitlab::Ci::Queue::Metrics.queue_operations_total).to receive(:increment) - .with(operation: :build_temporary_locked) + stub_feature_flags(ci_register_job_temporary_lock: true) - expect(service.execute).not_to be_valid + allow(Gitlab::Ci::Queue::Metrics.queue_operations_total).to receive(:increment) end - context 'when there is another build in queue' do - let!(:next_pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline) } + context 'when a build is temporarily locked' do + let(:service) { described_class.new(project_runner, nil) } - it 'skips this build and picks another build' do + before do + service.send(:acquire_temporary_lock, pending_job.id) + end + + it 'skips this build and marks queue as invalid' do expect(Gitlab::Ci::Queue::Metrics.queue_operations_total).to receive(:increment) - .with(operation: :queue_iteration).twice + .with(operation: :queue_iteration) expect(Gitlab::Ci::Queue::Metrics.queue_operations_total).to receive(:increment) .with(operation: :build_temporary_locked) - result = service.execute + expect(service.execute).not_to be_valid + end + + context 'when there is another build in queue' do + let!(:next_pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline) } + + it 'skips this build and picks another build' do + expect(Gitlab::Ci::Queue::Metrics.queue_operations_total).to receive(:increment) + .with(operation: :queue_iteration).twice + expect(Gitlab::Ci::Queue::Metrics.queue_operations_total).to receive(:increment) + .with(operation: :build_temporary_locked) - expect(result.build).to eq(next_pending_job) - expect(result).to be_valid + result = service.execute + + expect(result.build).to eq(next_pending_job) + expect(result).to be_valid + end end end end end - end - - context 'when using pending builds table' do - include_examples 'handles runner assignment' - context 'when a conflicting data is stored in denormalized table' do - let!(:specific_runner) { create(:ci_runner, :project, projects: [project], tag_list: %w[conflict]) } - let!(:pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline, tag_list: %w[conflict]) } + context 'when using pending builds table' do + include_examples 'handles runner assignment' - before do - pending_job.update_column(:status, :running) - end + context 'when a conflicting data is stored in denormalized table' do + let!(:runner) { create(:ci_runner, :project, projects: [project], tag_list: %w[conflict]) } + let!(:pending_job) { create(:ci_build, :pending, :queued, pipeline: pipeline, tag_list: %w[conflict]) } - it 'removes queuing entry upon build assignment attempt' do - expect(pending_job.reload).to be_running - expect(pending_job.queuing_entry).to be_present + before do + pending_job.update_column(:status, :running) + end - result = described_class.new(specific_runner).execute + it 'removes queuing entry upon build assignment attempt' do + expect(pending_job.reload).to be_running + expect(pending_job.queuing_entry).to be_present - expect(result).not_to be_valid - expect(pending_job.reload.queuing_entry).not_to be_present + expect(execute).not_to be_valid + expect(pending_job.reload.queuing_entry).not_to be_present + end end end end @@ -807,11 +842,11 @@ module Ci # Stub tested metrics allow(Gitlab::Ci::Queue::Metrics) .to receive(:attempt_counter) - .and_return(attempt_counter) + .and_return(attempt_counter) allow(Gitlab::Ci::Queue::Metrics) .to receive(:job_queue_duration_seconds) - .and_return(job_queue_duration_seconds) + .and_return(job_queue_duration_seconds) project.update!(shared_runners_enabled: true) pending_job.update!(created_at: current_time - 3600, queued_at: current_time - 1800) @@ -822,7 +857,7 @@ module Ci allow(job_queue_duration_seconds).to receive(:observe) expect(attempt_counter).to receive(:increment) - execute(runner) + build_on(runner) end end @@ -834,7 +869,7 @@ module Ci jobs_running_for_project: expected_jobs_running_for_project_first_job, shard: expected_shard }, 1800) - execute(runner) + build_on(runner) end context 'when project already has running jobs' do @@ -854,7 +889,7 @@ module Ci jobs_running_for_project: expected_jobs_running_for_project_third_job, shard: expected_shard }, 1800) - execute(runner) + build_on(runner) end end end @@ -913,12 +948,12 @@ module Ci allow(attempt_counter).to receive(:increment) expect(job_queue_duration_seconds).not_to receive(:observe) - execute(runner) + build_on(runner) end end end - context 'when specific runner is used' do + context 'when project runner is used' do let(:runner) { create(:ci_runner, :project, projects: [project], tag_list: %w(tag1 metrics_shard::shard_tag tag2)) } let(:expected_shared_runner) { false } let(:expected_shard) { ::Gitlab::Ci::Queue::Metrics::DEFAULT_METRICS_SHARD } @@ -933,12 +968,12 @@ module Ci it 'present sets runner session configuration in the build' do runner_session_params = { session: { 'url' => 'https://example.com' } } - expect(execute(specific_runner, runner_session_params).runner_session.attributes) + expect(build_on(project_runner, params: runner_session_params).runner_session.attributes) .to include(runner_session_params[:session]) end it 'not present it does not configure the runner session' do - expect(execute(specific_runner).runner_session).to be_nil + expect(build_on(project_runner).runner_session).to be_nil end end @@ -954,7 +989,7 @@ module Ci it 'returns 409 conflict' do expect(Ci::Build.pending.unstarted.count).to eq 3 - result = described_class.new(specific_runner).execute + result = described_class.new(project_runner, nil).execute expect(result).not_to be_valid expect(result.build).to be_nil @@ -962,8 +997,8 @@ module Ci end end - def execute(runner, params = {}) - described_class.new(runner).execute(params).build + def build_on(runner, runner_machine: nil, params: {}) + described_class.new(runner, runner_machine).execute(params).build end end end diff --git a/spec/services/ci/retry_job_service_spec.rb b/spec/services/ci/retry_job_service_spec.rb index c3d80f2cb56..10acf032b1a 100644 --- a/spec/services/ci/retry_job_service_spec.rb +++ b/spec/services/ci/retry_job_service_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Ci::RetryJobService do +RSpec.describe Ci::RetryJobService, feature_category: :continuous_integration do using RSpec::Parameterized::TableSyntax let_it_be(:reporter) { create(:user) } let_it_be(:developer) { create(:user) } @@ -27,6 +27,22 @@ RSpec.describe Ci::RetryJobService do project.add_reporter(reporter) end + shared_context 'retryable bridge' do + let_it_be(:downstream_project) { create(:project, :repository) } + + let_it_be_with_refind(:job) do + create(:ci_bridge, :success, + pipeline: pipeline, downstream: downstream_project, description: 'a trigger job', ci_stage: stage + ) + end + + let_it_be(:job_to_clone) { job } + + before do + job.update!(retried: false) + end + end + shared_context 'retryable build' do let_it_be_with_reload(:job) do create(:ci_build, :success, pipeline: pipeline, ci_stage: stage) @@ -102,6 +118,14 @@ RSpec.describe Ci::RetryJobService do end end + shared_examples_for 'does not retry the job' do + it 'returns :not_retryable and :unprocessable_entity' do + expect(subject.message).to be('Job cannot be retried') + expect(subject.payload[:reason]).to eq(:not_retryable) + expect(subject.payload[:job]).to eq(job) + end + end + shared_examples_for 'retries the job' do it_behaves_like 'clones the job' @@ -189,6 +213,20 @@ RSpec.describe Ci::RetryJobService do expect { service.clone!(create(:ci_build).present) }.to raise_error(TypeError) end + context 'when the job to be cloned is a bridge' 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 include_context 'retryable build' @@ -287,7 +325,33 @@ RSpec.describe Ci::RetryJobService do subject { service.execute(job) } + context 'when the job to be retried is a bridge' do + context 'and it is not retryable' do + let_it_be(:job) { create(:ci_bridge, :failed, :reached_max_descendant_pipelines_depth) } + + it_behaves_like 'does not retry the job' + end + + 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 + context 'and it is not retryable' do + let_it_be(:job) { create(:ci_build, :deployment_rejected, pipeline: pipeline) } + + it_behaves_like 'does not retry the job' + end + include_context 'retryable build' it_behaves_like 'retries the job' diff --git a/spec/services/ci/runners/create_runner_service_spec.rb b/spec/services/ci/runners/create_runner_service_spec.rb new file mode 100644 index 00000000000..673bf3ef90e --- /dev/null +++ b/spec/services/ci/runners/create_runner_service_spec.rb @@ -0,0 +1,135 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ::Ci::Runners::CreateRunnerService, "#execute", feature_category: :runner_fleet do + subject(:execute) { described_class.new(user: current_user, type: type, params: params).execute } + + let(:runner) { execute.payload[:runner] } + + let_it_be(:admin) { create(:admin) } + let_it_be(:non_admin_user) { create(:user) } + let_it_be(:anonymous) { nil } + + shared_context 'when admin user' do + let(:current_user) { admin } + + before do + allow(current_user).to receive(:can?).with(:create_instance_runners).and_return true + end + end + + shared_examples 'it can create a runner' do + it 'creates a runner of the specified type' do + expect(runner.runner_type).to eq expected_type + end + + context 'with default params provided' do + let(:args) do + {} + end + + before do + params.merge!(args) + end + + it { is_expected.to be_success } + + it 'uses default values when none are provided' do + 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.creator).to be current_user + expect(runner.authenticated_user_registration_type?).to be_truthy + expect(runner.runner_type).to eq 'instance_type' + end + end + + context 'with non-default params provided' do + let(:args) do + { + description: 'some description', + maintenance_note: 'a note', + paused: true, + tag_list: %w[tag1 tag2], + access_level: 'ref_protected', + locked: true, + maximum_timeout: 600, + run_untagged: false + } + end + + before do + params.merge!(args) + end + + it { is_expected.to be_success } + + it 'creates runner with specified values', :aggregate_failures do + expect(runner).to be_an_instance_of(::Ci::Runner) + expect(runner.description).to eq 'some description' + expect(runner.maintenance_note).to eq 'a note' + expect(runner.active).to eq !args[:paused] + 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(runner.access_level).to eq args[:access_level] + expect(runner.maximum_timeout).to eq args[:maximum_timeout] + + expect(runner.authenticated_user_registration_type?).to be_truthy + expect(runner.runner_type).to eq 'instance_type' + end + end + end + + shared_examples 'it cannot create a runner' do + it 'runner payload is nil' do + expect(runner).to be nil + end + + it { is_expected.to be_error } + end + + shared_examples 'it can return an error' do + let(:group) { create(:group) } + let(:runner_double) { Ci::Runner.new } + + context 'when the runner fails to save' do + before do + allow(Ci::Runner).to receive(:new).and_return runner_double + end + + it_behaves_like 'it cannot create a runner' + + it 'returns error message' do + expect(execute.errors).not_to be_empty + end + end + end + + context 'with type param set to nil' do + let(:expected_type) { 'instance_type' } + let(:type) { nil } + let(:params) { {} } + + it_behaves_like 'it cannot create a runner' do + let(:current_user) { anonymous } + end + + it_behaves_like 'it cannot create a runner' do + let(:current_user) { non_admin_user } + end + + it_behaves_like 'it can create a runner' do + include_context 'when admin user' + end + + it_behaves_like 'it can return an error' do + include_context 'when admin user' + 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 index d2a7e87b2d5..e62cb1ec3e3 100644 --- a/spec/services/ci/runners/process_runner_version_update_service_spec.rb +++ b/spec/services/ci/runners/process_runner_version_update_service_spec.rb @@ -53,14 +53,14 @@ RSpec.describe Ci::Runners::ProcessRunnerVersionUpdateService, feature_category: end context 'with existing ci_runner_version record' do - let!(:runner_version) { create(:ci_runner_version, version: '1.0.0', status: :not_available) } + let!(:runner_version) { create(:ci_runner_version, version: '1.0.0', status: :unavailable) } 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.to change { runner_version.reload.status }.from('unavailable').to('recommended') 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 39082b5c0f4..8d7e97e5ea8 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 @@ -9,7 +9,7 @@ RSpec.describe ::Ci::Runners::ReconcileExistingRunnerVersionsService, '#execute' let_it_be(:runner_14_0_1) { create(:ci_runner, version: '14.0.1') } let_it_be(:runner_version_14_0_1) do - create(:ci_runner_version, version: '14.0.1', status: :not_available) + create(:ci_runner_version, version: '14.0.1', status: :unavailable) end context 'with RunnerUpgradeCheck recommending 14.0.2' do @@ -23,15 +23,17 @@ RSpec.describe ::Ci::Runners::ReconcileExistingRunnerVersionsService, '#execute' context 'with runner with new version' do let!(:runner_14_0_2) { create(:ci_runner, version: '14.0.2') } - let!(:runner_version_14_0_0) { create(:ci_runner_version, version: '14.0.0', status: :not_available) } let!(:runner_14_0_0) { create(:ci_runner, version: '14.0.0') } + let!(:runner_version_14_0_0) do + create(:ci_runner_version, version: '14.0.0', status: :unavailable) + end before do 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([::Gitlab::VersionInfo.new(14, 0, 2), :not_available]) + .and_return([::Gitlab::VersionInfo.new(14, 0, 2), :unavailable]) .once end @@ -43,9 +45,9 @@ RSpec.describe ::Ci::Runners::ReconcileExistingRunnerVersionsService, '#execute' .and_call_original 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') + .to change { runner_version_14_0_0.reload.status }.from('unavailable').to('recommended') + .and change { runner_version_14_0_1.reload.status }.from('unavailable').to('recommended') + .and change { ::Ci::RunnerVersion.find_by(version: '14.0.2')&.status }.from(nil).to('unavailable') expect(execute).to be_success expect(execute.payload).to eq({ @@ -57,17 +59,19 @@ RSpec.describe ::Ci::Runners::ReconcileExistingRunnerVersionsService, '#execute' end context 'with orphan ci_runner_version' do - let!(:runner_version_14_0_2) { create(:ci_runner_version, version: '14.0.2', status: :not_available) } + let!(:runner_version_14_0_2) do + create(:ci_runner_version, version: '14.0.2', status: :unavailable) + end before do allow(upgrade_check).to receive(:check_runner_upgrade_suggestion) - .and_return([::Gitlab::VersionInfo.new(14, 0, 2), :not_available]) + .and_return([::Gitlab::VersionInfo.new(14, 0, 2), :unavailable]) end it 'deletes orphan ci_runner_versions entry', :aggregate_failures do 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') + .to change { ::Ci::RunnerVersion.find_by_version('14.0.2')&.status }.from('unavailable').to(nil) + .and not_change { runner_version_14_0_1.reload.status }.from('unavailable') expect(execute).to be_success expect(execute.payload).to eq({ @@ -81,11 +85,11 @@ RSpec.describe ::Ci::Runners::ReconcileExistingRunnerVersionsService, '#execute' context 'with no runner version changes' do before do allow(upgrade_check).to receive(:check_runner_upgrade_suggestion) - .and_return([::Gitlab::VersionInfo.new(14, 0, 1), :not_available]) + .and_return([::Gitlab::VersionInfo.new(14, 0, 1), :unavailable]) end it 'does not modify ci_runner_versions entries', :aggregate_failures do - expect { 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('unavailable') expect(execute).to be_success expect(execute.payload).to eq({ @@ -103,7 +107,7 @@ RSpec.describe ::Ci::Runners::ReconcileExistingRunnerVersionsService, '#execute' end it 'makes no changes to ci_runner_versions', :aggregate_failures do - expect { 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('unavailable') expect(execute).to be_success expect(execute.payload).to eq({ @@ -121,7 +125,7 @@ RSpec.describe ::Ci::Runners::ReconcileExistingRunnerVersionsService, '#execute' end it 'does not modify ci_runner_versions entries', :aggregate_failures do - expect { 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('unavailable') expect(execute).to be_success expect(execute.payload).to eq({ diff --git a/spec/services/ci/runners/register_runner_service_spec.rb b/spec/services/ci/runners/register_runner_service_spec.rb index 47d399cb19a..c67040e45eb 100644 --- a/spec/services/ci/runners/register_runner_service_spec.rb +++ b/spec/services/ci/runners/register_runner_service_spec.rb @@ -47,6 +47,7 @@ RSpec.describe ::Ci::Runners::RegisterRunnerService, '#execute', feature_categor expect(runner.run_untagged).to be true expect(runner.active).to be true expect(runner.token).not_to eq(registration_token) + expect(runner.token).not_to start_with(::Ci::Runner::CREATED_RUNNER_TOKEN_PREFIX) expect(runner).to be_instance_type end diff --git a/spec/services/ci/runners/stale_machines_cleanup_service_spec.rb b/spec/services/ci/runners/stale_machines_cleanup_service_spec.rb new file mode 100644 index 00000000000..456dbcebb84 --- /dev/null +++ b/spec/services/ci/runners/stale_machines_cleanup_service_spec.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Ci::Runners::StaleMachinesCleanupService, feature_category: :runner_fleet do + let(:service) { described_class.new } + let!(:runner_machine3) { create(:ci_runner_machine, created_at: 6.months.ago, contacted_at: Time.current) } + + subject(:response) { service.execute } + + context 'with no stale runner machines' do + it 'does not clean any runner machines and returns :success status' do + expect do + expect(response).to be_success + expect(response.payload).to match({ deleted_machines: false }) + end.not_to change { Ci::RunnerMachine.count }.from(1) + end + end + + context 'with some stale runner machines' do + before do + create(:ci_runner_machine, :stale) + create(:ci_runner_machine, :stale, contacted_at: nil) + end + + it 'only leaves non-stale runners' do + expect(response).to be_success + expect(response.payload).to match({ deleted_machines: true }) + expect(Ci::RunnerMachine.all).to contain_exactly(runner_machine3) + end + + context 'with more stale runners than MAX_DELETIONS' do + before do + stub_const("#{described_class}::MAX_DELETIONS", 1) + end + + it 'only leaves non-stale runners' do + expect do + expect(response).to be_success + expect(response.payload).to match({ deleted_machines: true }) + end.to change { Ci::RunnerMachine.count }.by(-Ci::Runners::StaleMachinesCleanupService::MAX_DELETIONS) + end + end + end +end diff --git a/spec/services/ci/update_build_queue_service_spec.rb b/spec/services/ci/update_build_queue_service_spec.rb index d3f537a1aa0..dd26339831c 100644 --- a/spec/services/ci/update_build_queue_service_spec.rb +++ b/spec/services/ci/update_build_queue_service_spec.rb @@ -277,7 +277,7 @@ RSpec.describe Ci::UpdateBuildQueueService do end end - context 'when updating specific runners' do + context 'when updating project runners' do let(:runner) { create(:ci_runner, :project, projects: [project]) } it_behaves_like 'matching build' diff --git a/spec/services/ci/update_build_state_service_spec.rb b/spec/services/ci/update_build_state_service_spec.rb index 90a86e7ae59..f8ecff20728 100644 --- a/spec/services/ci/update_build_state_service_spec.rb +++ b/spec/services/ci/update_build_state_service_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Ci::UpdateBuildStateService do +RSpec.describe Ci::UpdateBuildStateService, feature_category: :continuous_integration do let_it_be(:project) { create(:project) } let_it_be(:pipeline) { create(:ci_pipeline, project: project) } diff --git a/spec/services/clusters/agents/refresh_authorization_service_spec.rb b/spec/services/clusters/agents/refresh_authorization_service_spec.rb index fa38bc202e7..51c054ddc98 100644 --- a/spec/services/clusters/agents/refresh_authorization_service_spec.rb +++ b/spec/services/clusters/agents/refresh_authorization_service_spec.rb @@ -2,17 +2,17 @@ require 'spec_helper' -RSpec.describe Clusters::Agents::RefreshAuthorizationService do +RSpec.describe Clusters::Agents::RefreshAuthorizationService, feature_category: :kubernetes_management do describe '#execute' do let_it_be(:root_ancestor) { create(:group) } let_it_be(:removed_group) { create(:group, parent: root_ancestor) } let_it_be(:modified_group) { create(:group, parent: root_ancestor) } - let_it_be(:added_group) { create(:group, parent: root_ancestor) } + let_it_be(:added_group) { create(:group, path: 'group-path-with-UPPERCASE', parent: root_ancestor) } let_it_be(:removed_project) { create(:project, namespace: root_ancestor) } let_it_be(:modified_project) { create(:project, namespace: root_ancestor) } - let_it_be(:added_project) { create(:project, namespace: root_ancestor) } + let_it_be(:added_project) { create(:project, path: 'project-path-with-UPPERCASE', namespace: root_ancestor) } let(:project) { create(:project, namespace: root_ancestor) } let(:agent) { create(:cluster_agent, project: project) } @@ -22,11 +22,13 @@ RSpec.describe Clusters::Agents::RefreshAuthorizationService do ci_access: { groups: [ { id: added_group.full_path, default_namespace: 'default' }, - { id: modified_group.full_path, default_namespace: 'new-namespace' } + # Uppercase path verifies case-insensitive matching. + { id: modified_group.full_path.upcase, default_namespace: 'new-namespace' } ], projects: [ { id: added_project.full_path, default_namespace: 'default' }, - { id: modified_project.full_path, default_namespace: 'new-namespace' } + # Uppercase path verifies case-insensitive matching. + { id: modified_project.full_path.upcase, default_namespace: 'new-namespace' } ] } }.deep_stringify_keys diff --git a/spec/services/concerns/rate_limited_service_spec.rb b/spec/services/concerns/rate_limited_service_spec.rb index 04007e8e75a..d913cd17067 100644 --- a/spec/services/concerns/rate_limited_service_spec.rb +++ b/spec/services/concerns/rate_limited_service_spec.rb @@ -4,7 +4,7 @@ require 'spec_helper' RSpec.describe RateLimitedService do let(:key) { :issues_create } - let(:scope) { [:project, :current_user] } + let(:scope) { [:container, :current_user] } let(:opts) { { scope: scope, users_allowlist: -> { [User.support_bot.username] } } } let(:rate_limiter) { ::Gitlab::ApplicationRateLimiter } @@ -39,7 +39,7 @@ RSpec.describe RateLimitedService do let_it_be(:project) { create(:project) } let_it_be(:current_user) { create(:user) } - let(:service) { instance_double(Issues::CreateService, project: project, current_user: current_user) } + let(:service) { instance_double(Issues::CreateService, container: project, current_user: current_user) } let(:evaluated_scope) { [project, current_user] } let(:evaluated_opts) { { scope: evaluated_scope, users_allowlist: %w[support-bot] } } diff --git a/spec/services/event_create_service_spec.rb b/spec/services/event_create_service_spec.rb index e60954a19ed..b969bd76053 100644 --- a/spec/services/event_create_service_spec.rb +++ b/spec/services/event_create_service_spec.rb @@ -319,7 +319,6 @@ RSpec.describe EventCreateService, :clean_gitlab_redis_cache, :clean_gitlab_redi let(:category) { described_class.to_s } let(:action) { :push } let(:namespace) { project.namespace } - let(:feature_flag_name) { :route_hll_to_snowplow } let(:label) { 'usage_activity_by_stage_monthly.create.action_monthly_active_users_project_repo' } let(:property) { 'project_action' } end @@ -346,7 +345,6 @@ RSpec.describe EventCreateService, :clean_gitlab_redis_cache, :clean_gitlab_redi let(:category) { described_class.to_s } let(:action) { :push } let(:namespace) { project.namespace } - let(:feature_flag_name) { :route_hll_to_snowplow } let(:label) { 'usage_activity_by_stage_monthly.create.action_monthly_active_users_project_repo' } let(:property) { 'project_action' } end diff --git a/spec/services/export_csv/base_service_spec.rb b/spec/services/export_csv/base_service_spec.rb new file mode 100644 index 00000000000..e2b4d4829af --- /dev/null +++ b/spec/services/export_csv/base_service_spec.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ExportCsv::BaseService, feature_category: :importers do + let_it_be(:issue) { create(:issue) } + let_it_be(:relation) { Issue.all } + let_it_be(:resource_parent) { issue.project } + + subject { described_class.new(relation, resource_parent) } + + describe '#email' do + it 'raises NotImplementedError' do + user = create(:user) + + expect { subject.email(user) }.to raise_error(NotImplementedError) + end + end + + describe '#header_to_value_hash' do + it 'raises NotImplementedError' do + expect { subject.send(:header_to_value_hash) }.to raise_error(NotImplementedError) + end + end + + describe '#associations_to_preload' do + it 'return []' do + expect(subject.send(:associations_to_preload)).to eq([]) + end + end +end diff --git a/spec/services/export_csv/map_export_fields_service_spec.rb b/spec/services/export_csv/map_export_fields_service_spec.rb new file mode 100644 index 00000000000..0060baf30e4 --- /dev/null +++ b/spec/services/export_csv/map_export_fields_service_spec.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ExportCsv::MapExportFieldsService, feature_category: :team_planning do + let(:selected_fields) { ['Title', 'Author username', 'state'] } + let(:invalid_fields) { ['Title', 'Author Username', 'State', 'Invalid Field', 'Other Field'] } + let(:data) do + { + 'Requirement ID' => '1', + 'Title' => 'foo', + 'Description' => 'bar', + 'Author' => 'root', + 'Author Username' => 'admin', + 'Created At (UTC)' => '2023-02-01 15:16:35', + 'State' => 'opened', + 'State Updated At (UTC)' => '2023-02-01 15:16:35' + } + end + + describe '#execute' do + it 'returns a hash with selected fields only' do + result = described_class.new(selected_fields, data).execute + + expect(result).to be_a(Hash) + expect(result.keys).to match_array(selected_fields.map(&:titleize)) + end + + context 'when the fields collection is empty' do + it 'returns a hash with all fields' do + result = described_class.new([], data).execute + + expect(result).to be_a(Hash) + expect(result.keys).to match_array(data.keys) + end + end + + context 'when fields collection includes invalid fields' do + it 'returns a hash with valid selected fields only' do + result = described_class.new(invalid_fields, data).execute + + expect(result).to be_a(Hash) + expect(result.keys).to eq(selected_fields.map(&:titleize)) + end + end + end + + describe '#invalid_fields' do + it 'returns an array containing invalid fields' do + result = described_class.new(invalid_fields, data).invalid_fields + + expect(result).to match_array(['Invalid Field', 'Other Field']) + end + end +end diff --git a/spec/services/git/wiki_push_service_spec.rb b/spec/services/git/wiki_push_service_spec.rb index 878a5c4ccf0..b076b2d51ef 100644 --- a/spec/services/git/wiki_push_service_spec.rb +++ b/spec/services/git/wiki_push_service_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Git::WikiPushService, services: true do +RSpec.describe Git::WikiPushService, services: true, feature_category: :wiki do include RepoHelpers let_it_be(:current_user) { create(:user) } diff --git a/spec/services/google_cloud/fetch_google_ip_list_service_spec.rb b/spec/services/google_cloud/fetch_google_ip_list_service_spec.rb index ef77958fa60..e5f06824b9f 100644 --- a/spec/services/google_cloud/fetch_google_ip_list_service_spec.rb +++ b/spec/services/google_cloud/fetch_google_ip_list_service_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' RSpec.describe GoogleCloud::FetchGoogleIpListService, :use_clean_rails_memory_store_caching, -:clean_gitlab_redis_rate_limiting, feature_category: :continuous_integration do +:clean_gitlab_redis_rate_limiting, feature_category: :build_artifacts do include StubRequests let(:google_cloud_ips) { File.read(Rails.root.join('spec/fixtures/cdn/google_cloud.json')) } diff --git a/spec/services/groups/create_service_spec.rb b/spec/services/groups/create_service_spec.rb index 0425ba3e631..84794b5f6f8 100644 --- a/spec/services/groups/create_service_spec.rb +++ b/spec/services/groups/create_service_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Groups::CreateService, '#execute' do +RSpec.describe Groups::CreateService, '#execute', feature_category: :subgroups do let!(:user) { create(:user) } let!(:group_params) { { path: "group_path", visibility_level: Gitlab::VisibilityLevel::PUBLIC } } @@ -78,10 +78,6 @@ RSpec.describe Groups::CreateService, '#execute' do it { is_expected.to be_persisted } - it 'adds an onboarding progress record' do - expect { subject }.to change(Onboarding::Progress, :count).from(0).to(1) - end - context 'with before_commit callback' do it_behaves_like 'has sync-ed traversal_ids' end @@ -107,10 +103,6 @@ RSpec.describe Groups::CreateService, '#execute' do it { is_expected.to be_persisted } - it 'does not add an onboarding progress record' do - expect { subject }.not_to change(Onboarding::Progress, :count).from(0) - end - it_behaves_like 'has sync-ed traversal_ids' end diff --git a/spec/services/groups/destroy_service_spec.rb b/spec/services/groups/destroy_service_spec.rb index 2791203f395..7c3710aeeb2 100644 --- a/spec/services/groups/destroy_service_spec.rb +++ b/spec/services/groups/destroy_service_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Groups::DestroyService do +RSpec.describe Groups::DestroyService, feature_category: :subgroups do let!(:user) { create(:user) } let!(:group) { create(:group) } let!(:nested_group) { create(:group, parent: group) } diff --git a/spec/services/groups/group_links/destroy_service_spec.rb b/spec/services/groups/group_links/destroy_service_spec.rb index 03de7175edd..a570c28cf8b 100644 --- a/spec/services/groups/group_links/destroy_service_spec.rb +++ b/spec/services/groups/group_links/destroy_service_spec.rb @@ -70,8 +70,8 @@ RSpec.describe Groups::GroupLinks::DestroyService, '#execute' do it 'updates project authorization once per group' do expect(GroupGroupLink).to receive(:delete).and_call_original - expect(group).to receive(:refresh_members_authorized_projects).with(direct_members_only: true, blocking: false).once - expect(another_group).to receive(:refresh_members_authorized_projects).with(direct_members_only: true, blocking: false).once + expect(group).to receive(:refresh_members_authorized_projects).with(direct_members_only: true).once + expect(another_group).to receive(:refresh_members_authorized_projects).with(direct_members_only: true).once subject.execute(links) end diff --git a/spec/services/import/gitlab_projects/file_acquisition_strategies/remote_file_spec.rb b/spec/services/import/gitlab_projects/file_acquisition_strategies/remote_file_spec.rb index 8565299b9b7..a28a552746f 100644 --- a/spec/services/import/gitlab_projects/file_acquisition_strategies/remote_file_spec.rb +++ b/spec/services/import/gitlab_projects/file_acquisition_strategies/remote_file_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe ::Import::GitlabProjects::FileAcquisitionStrategies::RemoteFile, :aggregate_failures do +RSpec.describe ::Import::GitlabProjects::FileAcquisitionStrategies::RemoteFile, :aggregate_failures, feature_category: :importers do let(:remote_url) { 'https://external.file.path/file.tar.gz' } let(:params) { { remote_import_url: remote_url } } @@ -40,23 +40,19 @@ RSpec.describe ::Import::GitlabProjects::FileAcquisitionStrategies::RemoteFile, end end - context 'when import_project_from_remote_file_s3 is enabled' do - before do - stub_feature_flags(import_project_from_remote_file_s3: true) - end - - context 'when the HTTP request fail to recover the headers' do - it 'adds the error message' do - expect(Gitlab::HTTP) - .to receive(:head) - .and_raise(StandardError, 'request invalid') + context 'when the HTTP request fails to recover the headers' do + it 'adds the error message' do + expect(Gitlab::HTTP) + .to receive(:head) + .and_raise(StandardError, 'request invalid') - expect(subject).not_to be_valid - expect(subject.errors.full_messages) - .to include('Failed to retrive headers: request invalid') - end + expect(subject).not_to be_valid + expect(subject.errors.full_messages) + .to include('Failed to retrive headers: request invalid') end + end + context 'when request is not from an S3 server' do it 'validates the remote content-length' do stub_headers_for(remote_url, { 'content-length' => 11.gigabytes }) @@ -72,57 +68,19 @@ RSpec.describe ::Import::GitlabProjects::FileAcquisitionStrategies::RemoteFile, expect(subject.errors.full_messages) .to include("Content type 'unknown' not allowed. (Allowed: application/gzip, application/x-tar, application/x-gzip)") end - - context 'when trying to import from AWS S3' do - it 'adds an error suggesting to use `projects/remote-import-s3`' do - stub_headers_for( - remote_url, - 'Server' => 'AmazonS3', - 'x-amz-request-id' => 'some-id' - ) - - expect(subject).not_to be_valid - expect(subject.errors.full_messages) - .to include('To import from AWS S3 use `projects/remote-import-s3`') - end - end end - context 'when import_project_from_remote_file_s3 is disabled' do - before do - stub_feature_flags(import_project_from_remote_file_s3: false) - end - - context 'when trying to import from AWS S3' do - it 'does not validate the remote content-length or content-type' do - stub_headers_for( - remote_url, - 'Server' => 'AmazonS3', - 'x-amz-request-id' => 'some-id', - 'content-length' => 11.gigabytes, - 'content-type' => 'unknown' - ) - - expect(subject).to be_valid - end - end - - context 'when NOT trying to import from AWS S3' do - it 'validates content-length and content-type' do - stub_headers_for( - remote_url, - 'Server' => 'NOT AWS S3', - 'content-length' => 11.gigabytes, - 'content-type' => 'unknown' - ) - - expect(subject).not_to be_valid - - expect(subject.errors.full_messages) - .to include("Content type 'unknown' not allowed. (Allowed: application/gzip, application/x-tar, application/x-gzip)") - expect(subject.errors.full_messages) - .to include('Content length is too big (should be at most 10 GB)') - end + context 'when request is from an S3 server' do + it 'does not validate the remote content-length or content-type' do + stub_headers_for( + remote_url, + 'Server' => 'AmazonS3', + 'x-amz-request-id' => 'some-id', + 'content-length' => 11.gigabytes, + 'content-type' => 'unknown' + ) + + expect(subject).to be_valid end end end diff --git a/spec/services/import_csv/base_service_spec.rb b/spec/services/import_csv/base_service_spec.rb new file mode 100644 index 00000000000..0c0ed40ff4d --- /dev/null +++ b/spec/services/import_csv/base_service_spec.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ImportCsv::BaseService, feature_category: :importers do + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project) } + let_it_be(:csv_io) { double } + + subject { described_class.new(user, project, csv_io) } + + shared_examples 'abstract method' do |method, args| + it "raises NotImplemented error when #{method} is called" do + if args + expect { subject.send(method, args) }.to raise_error(NotImplementedError) + else + expect { subject.send(method) }.to raise_error(NotImplementedError) + end + end + end + + it_behaves_like 'abstract method', :email_results_to_user + it_behaves_like 'abstract method', :attributes_for, "any" + it_behaves_like 'abstract method', :validate_headers_presence!, "any" + it_behaves_like 'abstract method', :create_object_class + + describe '#detect_col_sep' do + context 'when header contains invalid separators' do + it 'raises error' do + header = 'Name&email' + + expect { subject.send(:detect_col_sep, header) }.to raise_error(CSV::MalformedCSVError) + end + end + + context 'when header is valid' do + shared_examples 'header with valid separators' do + let(:header) { "Name#{separator}email" } + + it 'returns separator value' do + expect(subject.send(:detect_col_sep, header)).to eq(separator) + end + end + + context 'with ; as separator' do + let(:separator) { ';' } + + it_behaves_like 'header with valid separators' + end + + context 'with \t as separator' do + let(:separator) { "\t" } + + it_behaves_like 'header with valid separators' + end + + context 'with , as separator' do + let(:separator) { ',' } + + it_behaves_like 'header with valid separators' + end + end + end +end 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 a3810879c65..fa5f4c64a43 100644 --- a/spec/services/incident_management/timeline_events/create_service_spec.rb +++ b/spec/services/incident_management/timeline_events/create_service_spec.rb @@ -171,7 +171,7 @@ RSpec.describe IncidentManagement::TimelineEvents::CreateService do occurred_at: Time.current, action: 'new comment', promoted_from_note: comment, - timeline_event_tag_names: ['start time', 'end time'] + timeline_event_tag_names: ['start time', 'end time', 'Impact mitigated'] } end @@ -180,11 +180,11 @@ RSpec.describe IncidentManagement::TimelineEvents::CreateService do it 'matches the two tags on the event and creates on project' do result = execute.payload[:timeline_event] - expect(result.timeline_event_tags.count).to eq(2) - expect(result.timeline_event_tags.by_names(['Start time', 'End time']).pluck_names) - .to match_array(['Start time', 'End time']) + expect(result.timeline_event_tags.count).to eq(3) + expect(result.timeline_event_tags.by_names(['Start time', 'End time', 'Impact mitigated']).pluck_names) + .to match_array(['Start time', 'End time', 'Impact mitigated']) expect(project.incident_management_timeline_event_tags.pluck_names) - .to include('Start time', 'End time') + .to include('Start time', 'End time', 'Impact mitigated') 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 ff802109715..ebaa4dde7a2 100644 --- a/spec/services/incident_management/timeline_events/update_service_spec.rb +++ b/spec/services/incident_management/timeline_events/update_service_spec.rb @@ -201,20 +201,22 @@ RSpec.describe IncidentManagement::TimelineEvents::UpdateService, feature_catego { note: 'Updated note', occurred_at: occurred_at, - timeline_event_tag_names: ['start time', 'end time'] + timeline_event_tag_names: ['start time', 'end time', 'response initiated'] } end it 'returns the new tag in response' do timeline_event = execute.payload[:timeline_event] - expect(timeline_event.timeline_event_tags.pluck_names).to contain_exactly('Start time', 'End time') + expect(timeline_event.timeline_event_tags.pluck_names).to contain_exactly( + 'Start time', 'End time', 'Response initiated') end it 'creates the predefined tags on the project' do execute - expect(project.incident_management_timeline_event_tags.pluck_names).to include('Start time', 'End time') + expect(project.incident_management_timeline_event_tags.pluck_names).to include( + 'Start time', 'End time', 'Response initiated') end end diff --git a/spec/services/issuable/bulk_update_service_spec.rb b/spec/services/issuable/bulk_update_service_spec.rb index dc72cf04776..7ba349ceeae 100644 --- a/spec/services/issuable/bulk_update_service_spec.rb +++ b/spec/services/issuable/bulk_update_service_spec.rb @@ -117,11 +117,37 @@ RSpec.describe Issuable::BulkUpdateService do end end + shared_examples 'bulk update service' do + it 'result count only includes authorized issuables' do + all_issues = issues + [create(:issue, project: create(:project, :private))] + result = bulk_update(all_issues, { assignee_ids: [user.id] }) + + expect(result[:count]).to eq(issues.count) + end + + context 'when issuable_ids are passed as an array' do + it 'updates assignees' do + expect do + described_class.new( + parent, + user, + { issuable_ids: issues.map(&:id), assignee_ids: [user.id] } + ).execute('issue') + + issues.each(&:reset) + end.to change { issues.flat_map(&:assignee_ids) }.from([]).to([user.id] * 2) + end + end + end + context 'with issuables at a project level' do + let_it_be_with_reload(:issues) { create_list(:issue, 2, project: project) } + let(:parent) { project } + it_behaves_like 'bulk update service' + context 'with unpermitted attributes' do - let(:issues) { create_list(:issue, 2, project: project) } let(:label) { create(:label, project: project) } it 'does not update the issues' do @@ -131,9 +157,23 @@ RSpec.describe Issuable::BulkUpdateService do end end - describe 'close issues' do - let(:issues) { create_list(:issue, 2, project: project) } + context 'when issuable update service raises an ArgumentError' do + before do + allow_next_instance_of(Issues::UpdateService) do |update_service| + allow(update_service).to receive(:execute).and_raise(ArgumentError, 'update error') + end + end + + it 'returns an error response' do + result = bulk_update(issues, add_label_ids: []) + expect(result).to be_error + expect(result.errors).to contain_exactly('update error') + expect(result.http_status).to eq(422) + end + end + + describe 'close issues' do it 'succeeds and returns the correct number of issues updated' do result = bulk_update(issues, state_event: 'close') @@ -155,24 +195,24 @@ RSpec.describe Issuable::BulkUpdateService do end describe 'reopen issues' do - let(:issues) { create_list(:closed_issue, 2, project: project) } + let_it_be_with_reload(:closed_issues) { create_list(:closed_issue, 2, project: project) } it 'succeeds and returns the correct number of issues updated' do - result = bulk_update(issues, state_event: 'reopen') + result = bulk_update(closed_issues, state_event: 'reopen') expect(result.success?).to be_truthy - expect(result.payload[:count]).to eq(issues.count) + expect(result.payload[:count]).to eq(closed_issues.count) end it 'reopens all the issues passed' do - bulk_update(issues, state_event: 'reopen') + bulk_update(closed_issues, state_event: 'reopen') expect(project.issues.closed).to be_empty expect(project.issues.opened).not_to be_empty end it_behaves_like 'scheduling cached group count clear' do - let(:issuables) { issues } + let(:issuables) { closed_issues } let(:params) { { state_event: 'reopen' } } end end @@ -207,10 +247,10 @@ RSpec.describe Issuable::BulkUpdateService do end end - context 'when the new assignee ID is not present' do - it 'does not unassign' do + context 'when the new assignee IDs array is empty' do + it 'removes all assignees' do expect { bulk_update(merge_request, assignee_ids: []) } - .not_to change { merge_request.reload.assignee_ids } + .to change(merge_request.assignees, :count).by(-1) end end end @@ -244,10 +284,10 @@ RSpec.describe Issuable::BulkUpdateService do end end - context 'when the new assignee ID is not present' do - it 'does not unassign' do + context 'when the new assignee IDs array is empty' do + it 'removes all assignees' do expect { bulk_update(issue, assignee_ids: []) } - .not_to change(issue.assignees, :count) + .to change(issue.assignees, :count).by(-1) end end end @@ -321,6 +361,10 @@ RSpec.describe Issuable::BulkUpdateService do group.add_reporter(user) end + it_behaves_like 'bulk update service' do + let_it_be_with_reload(:issues) { create_list(:issue, 2, project: create(:project, group: group)) } + end + describe 'updating milestones' do let(:milestone) { create(:milestone, group: group) } let(:project) { create(:project, :repository, group: group) } @@ -372,4 +416,13 @@ RSpec.describe Issuable::BulkUpdateService do end end end + + context 'when no parent is provided' do + it 'returns an unscoped update error' do + result = described_class.new(nil, user, { assignee_ids: [user.id], issuable_ids: [] }).execute('issue') + + expect(result).to be_error + expect(result.errors).to contain_exactly(_('A parent must be provided when bulk updating issuables')) + end + end end diff --git a/spec/services/issuable/destroy_service_spec.rb b/spec/services/issuable/destroy_service_spec.rb index c72d48d5b77..29f548e1c47 100644 --- a/spec/services/issuable/destroy_service_spec.rb +++ b/spec/services/issuable/destroy_service_spec.rb @@ -7,7 +7,7 @@ RSpec.describe Issuable::DestroyService do let(:group) { create(:group, :public) } let(:project) { create(:project, :public, group: group) } - subject(:service) { described_class.new(project: project, current_user: user) } + subject(:service) { described_class.new(container: project, current_user: user) } describe '#execute' do context 'when issuable is an issue' do diff --git a/spec/services/issuable/discussions_list_service_spec.rb b/spec/services/issuable/discussions_list_service_spec.rb index ecdd8d031c9..a6f57088ad1 100644 --- a/spec/services/issuable/discussions_list_service_spec.rb +++ b/spec/services/issuable/discussions_list_service_spec.rb @@ -8,6 +8,7 @@ RSpec.describe Issuable::DiscussionsListService do let_it_be(:project) { create(:project, :repository, :private, group: group) } let_it_be(:milestone) { create(:milestone, project: project) } let_it_be(:label) { create(:label, project: project) } + let_it_be(:label_2) { create(:label, project: project) } let(:finder_params_for_issuable) { {} } @@ -22,8 +23,7 @@ RSpec.describe Issuable::DiscussionsListService do let_it_be(:issuable) { create(:work_item, :issue, project: project) } before do - stub_const('WorkItems::Type::BASE_TYPES', { issue: { name: 'NoNotesWidget', enum_value: 0 } }) - stub_const('WorkItems::Type::WIDGETS_FOR_TYPE', { issue: [::WorkItems::Widgets::Description] }) + WorkItems::Type.default_by_type(:issue).widget_definitions.find_by_widget_type(:notes).update!(disabled: true) end it "returns no notes" do diff --git a/spec/services/issues/after_create_service_spec.rb b/spec/services/issues/after_create_service_spec.rb index 6b720d6e687..39a6799dbad 100644 --- a/spec/services/issues/after_create_service_spec.rb +++ b/spec/services/issues/after_create_service_spec.rb @@ -11,7 +11,7 @@ RSpec.describe Issues::AfterCreateService do let_it_be(:milestone) { create(:milestone, project: project) } let_it_be(:issue) { create(:issue, project: project, author: current_user, milestone: milestone, assignee_ids: [assignee.id]) } - subject(:after_create_service) { described_class.new(project: project, current_user: current_user) } + subject(:after_create_service) { described_class.new(container: project, current_user: current_user) } describe '#execute' do it 'creates a pending todo for new assignee' do diff --git a/spec/services/issues/build_service_spec.rb b/spec/services/issues/build_service_spec.rb index 838e0675372..2160c45d079 100644 --- a/spec/services/issues/build_service_spec.rb +++ b/spec/services/issues/build_service_spec.rb @@ -19,7 +19,7 @@ RSpec.describe Issues::BuildService do end def build_issue(issue_params = {}) - described_class.new(project: project, current_user: user, params: issue_params).execute + described_class.new(container: project, current_user: user, params: issue_params).execute end context 'for a single discussion' do @@ -45,7 +45,7 @@ RSpec.describe Issues::BuildService do describe '#items_for_discussions' do it 'has an item for each discussion' do create(:diff_note_on_merge_request, noteable: merge_request, project: merge_request.source_project, line_number: 13) - service = described_class.new(project: project, current_user: user, params: { merge_request_to_resolve_discussions_of: merge_request.iid }) + service = described_class.new(container: project, current_user: user, params: { merge_request_to_resolve_discussions_of: merge_request.iid }) service.execute @@ -54,7 +54,7 @@ RSpec.describe Issues::BuildService do end describe '#item_for_discussion' do - let(:service) { described_class.new(project: project, current_user: user, params: { merge_request_to_resolve_discussions_of: merge_request.iid }) } + let(:service) { described_class.new(container: project, current_user: user, params: { merge_request_to_resolve_discussions_of: merge_request.iid }) } it 'mentions the author of the note' do discussion = create(:diff_note_on_merge_request, author: create(:user, username: 'author')).to_discussion diff --git a/spec/services/issues/clone_service_spec.rb b/spec/services/issues/clone_service_spec.rb index 67f32b85350..eafaea93015 100644 --- a/spec/services/issues/clone_service_spec.rb +++ b/spec/services/issues/clone_service_spec.rb @@ -22,7 +22,7 @@ RSpec.describe Issues::CloneService do let(:with_notes) { false } subject(:clone_service) do - described_class.new(project: old_project, current_user: user) + described_class.new(container: old_project, current_user: user) end shared_context 'user can clone issue' do diff --git a/spec/services/issues/close_service_spec.rb b/spec/services/issues/close_service_spec.rb index ef24d1e940e..803808e667c 100644 --- a/spec/services/issues/close_service_spec.rb +++ b/spec/services/issues/close_service_spec.rb @@ -20,13 +20,13 @@ RSpec.describe Issues::CloseService do end describe '#execute' do - let(:service) { described_class.new(project: project, current_user: user) } + let(:service) { described_class.new(container: project, current_user: user) } context 'when skip_authorization is true' do it 'does close the issue even if user is not authorized' do non_authorized_user = create(:user) - service = described_class.new(project: project, current_user: non_authorized_user) + service = described_class.new(container: project, current_user: non_authorized_user) expect do service.execute(issue, skip_authorization: true) @@ -167,7 +167,7 @@ RSpec.describe Issues::CloseService do project.reload expect(project.external_issue_tracker).to receive(:close_issue) - described_class.new(project: project, current_user: user).close_issue(external_issue) + described_class.new(container: project, current_user: user).close_issue(external_issue) end end @@ -178,7 +178,7 @@ RSpec.describe Issues::CloseService do project.reload expect(project.external_issue_tracker).not_to receive(:close_issue) - described_class.new(project: project, current_user: user).close_issue(external_issue) + described_class.new(container: project, current_user: user).close_issue(external_issue) end end @@ -189,7 +189,7 @@ RSpec.describe Issues::CloseService do project.reload expect(project.external_issue_tracker).not_to receive(:close_issue) - described_class.new(project: project, current_user: user).close_issue(external_issue) + described_class.new(container: project, current_user: user).close_issue(external_issue) end end end @@ -197,7 +197,7 @@ RSpec.describe Issues::CloseService do context "closed by a merge request", :sidekiq_might_not_need_inline do subject(:close_issue) do perform_enqueued_jobs do - described_class.new(project: project, current_user: user).close_issue(issue, closed_via: closing_merge_request) + described_class.new(container: project, current_user: user).close_issue(issue, closed_via: closing_merge_request) end end @@ -266,7 +266,7 @@ RSpec.describe Issues::CloseService do context "closed by a commit", :sidekiq_might_not_need_inline do it 'mentions closure via a commit' do perform_enqueued_jobs do - described_class.new(project: project, current_user: user).close_issue(issue, closed_via: closing_commit) + described_class.new(container: project, current_user: user).close_issue(issue, closed_via: closing_commit) end email = ActionMailer::Base.deliveries.last @@ -280,7 +280,7 @@ RSpec.describe Issues::CloseService do it 'does not mention the commit id' do project.project_feature.update_attribute(:repository_access_level, ProjectFeature::DISABLED) perform_enqueued_jobs do - described_class.new(project: project, current_user: user).close_issue(issue, closed_via: closing_commit) + described_class.new(container: project, current_user: user).close_issue(issue, closed_via: closing_commit) end email = ActionMailer::Base.deliveries.last @@ -296,7 +296,7 @@ RSpec.describe Issues::CloseService do context "valid params" do subject(:close_issue) do perform_enqueued_jobs do - described_class.new(project: project, current_user: user).close_issue(issue) + described_class.new(container: project, current_user: user).close_issue(issue) end end @@ -438,7 +438,7 @@ RSpec.describe Issues::CloseService do expect(project).to receive(:execute_hooks).with(expected_payload, :issue_hooks) expect(project).to receive(:execute_integrations).with(expected_payload, :issue_hooks) - described_class.new(project: project, current_user: user).close_issue(issue) + described_class.new(container: project, current_user: user).close_issue(issue) end end @@ -449,7 +449,7 @@ RSpec.describe Issues::CloseService do expect(project).to receive(:execute_hooks).with(an_instance_of(Hash), :confidential_issue_hooks) expect(project).to receive(:execute_integrations).with(an_instance_of(Hash), :confidential_issue_hooks) - described_class.new(project: project, current_user: user).close_issue(issue) + described_class.new(container: project, current_user: user).close_issue(issue) end end diff --git a/spec/services/issues/create_service_spec.rb b/spec/services/issues/create_service_spec.rb index 7ab2046b6be..ada5b300d7a 100644 --- a/spec/services/issues/create_service_spec.rb +++ b/spec/services/issues/create_service_spec.rb @@ -11,7 +11,7 @@ RSpec.describe Issues::CreateService do let(:opts) { { title: 'title' } } let(:spam_params) { double } - let(:service) { described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params) } + let(:service) { described_class.new(container: project, current_user: user, params: opts, spam_params: spam_params) } it_behaves_like 'rate limited service' do let(:key) { :issues_create } @@ -147,7 +147,7 @@ RSpec.describe Issues::CreateService do end context 'when a build_service is provided' do - let(:result) { described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params, build_service: build_service).execute } + let(:result) { described_class.new(container: project, current_user: user, params: opts, spam_params: spam_params, build_service: build_service).execute } let(:issue_from_builder) { WorkItem.new(project: project, title: 'Issue from builder') } let(:build_service) { double(:build_service, execute: issue_from_builder) } @@ -160,7 +160,7 @@ RSpec.describe Issues::CreateService do end context 'when skip_system_notes is true' do - let(:issue) { described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params).execute(skip_system_notes: true) } + let(:issue) { described_class.new(container: project, current_user: user, params: opts, spam_params: spam_params).execute(skip_system_notes: true) } it 'does not call Issuable::CommonSystemNotesService' do expect(Issuable::CommonSystemNotesService).not_to receive(:new) @@ -256,7 +256,7 @@ RSpec.describe Issues::CreateService do let_it_be(:non_member) { create(:user) } it 'filters out params that cannot be set without the :set_issue_metadata permission' do - result = described_class.new(project: project, current_user: non_member, params: opts, spam_params: spam_params).execute + result = described_class.new(container: project, current_user: non_member, params: opts, spam_params: spam_params).execute issue = result[:issue] expect(result).to be_success @@ -270,7 +270,7 @@ RSpec.describe Issues::CreateService do end it 'can create confidential issues' do - result = described_class.new(project: project, current_user: non_member, params: opts.merge(confidential: true), spam_params: spam_params).execute + result = described_class.new(container: project, current_user: non_member, params: opts.merge(confidential: true), spam_params: spam_params).execute issue = result[:issue] expect(result).to be_success @@ -281,7 +281,7 @@ RSpec.describe Issues::CreateService do it 'moves the issue to the end, in an asynchronous worker' do expect(Issues::PlacementWorker).to receive(:perform_async).with(be_nil, Integer) - described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params).execute + described_class.new(container: project, current_user: user, params: opts, spam_params: spam_params).execute end context 'when label belongs to project group' do @@ -368,7 +368,7 @@ RSpec.describe Issues::CreateService do it 'invalidates open issues counter for assignees when issue is assigned' do project.add_maintainer(assignee) - described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params).execute + described_class.new(container: project, current_user: user, params: opts, spam_params: spam_params).execute expect(assignee.assigned_open_issues_count).to eq 1 end @@ -439,7 +439,7 @@ RSpec.describe Issues::CreateService do expect(project).to receive(:execute_hooks).with(expected_payload, :issue_hooks) expect(project).to receive(:execute_integrations).with(expected_payload, :issue_hooks) - described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params).execute + described_class.new(container: project, current_user: user, params: opts, spam_params: spam_params).execute end context 'when issue is confidential' do @@ -462,7 +462,7 @@ RSpec.describe Issues::CreateService do expect(project).to receive(:execute_hooks).with(expected_payload, :confidential_issue_hooks) expect(project).to receive(:execute_integrations).with(expected_payload, :confidential_issue_hooks) - described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params).execute + described_class.new(container: project, current_user: user, params: opts, spam_params: spam_params).execute end end end @@ -508,7 +508,7 @@ RSpec.describe Issues::CreateService do it 'removes assignee when user id is invalid' do opts = { title: 'Title', description: 'Description', assignee_ids: [-1] } - result = described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params).execute + result = described_class.new(container: project, current_user: user, params: opts, spam_params: spam_params).execute issue = result[:issue] expect(result).to be_success @@ -518,7 +518,7 @@ RSpec.describe Issues::CreateService do it 'removes assignee when user id is 0' do opts = { title: 'Title', description: 'Description', assignee_ids: [0] } - result = described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params).execute + result = described_class.new(container: project, current_user: user, params: opts, spam_params: spam_params).execute issue = result[:issue] expect(result).to be_success @@ -529,7 +529,7 @@ RSpec.describe Issues::CreateService do project.add_maintainer(assignee) opts = { title: 'Title', description: 'Description', assignee_ids: [assignee.id] } - result = described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params).execute + result = described_class.new(container: project, current_user: user, params: opts, spam_params: spam_params).execute issue = result[:issue] expect(result).to be_success @@ -549,7 +549,7 @@ RSpec.describe Issues::CreateService do project.update!(visibility_level: level) opts = { title: 'Title', description: 'Description', assignee_ids: [assignee.id] } - result = described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params).execute + result = described_class.new(container: project, current_user: user, params: opts, spam_params: spam_params).execute issue = result[:issue] expect(result).to be_success @@ -561,10 +561,40 @@ RSpec.describe Issues::CreateService do end it_behaves_like 'issuable record that supports quick actions' do - let(:issuable) { described_class.new(project: project, current_user: user, params: params, spam_params: spam_params).execute[:issue] } + let(:issuable) { described_class.new(container: project, current_user: user, params: params, spam_params: spam_params).execute[:issue] } end context 'Quick actions' do + context 'as work item' do + let(:opts) do + { + title: "My work item", + work_item_type: work_item_type, + description: "/shrug" + } + end + + context 'when work item type is not the default Issue' do + let(:work_item_type) { create(:work_item_type, namespace: project.namespace) } + + it 'saves the work item without applying the quick action' do + expect(result).to be_success + expect(issue).to be_persisted + expect(issue.description).to eq("/shrug") + end + end + + context 'when work item type is the default Issue' do + let(:work_item_type) { WorkItems::Type.default_by_type(:issue) } + + it 'saves the work item and applies the quick action' do + expect(result).to be_success + expect(issue).to be_persisted + expect(issue.description).to eq(" ¯\\_(ツ)_/¯") + end + end + end + context 'with assignee, milestone, and contact in params and command' do let_it_be(:contact) { create(:contact, group: group) } @@ -672,14 +702,14 @@ RSpec.describe Issues::CreateService do let(:opts) { { discussion_to_resolve: discussion.id, merge_request_to_resolve_discussions_of: merge_request.iid } } it 'resolves the discussion' do - described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params).execute + described_class.new(container: project, current_user: user, params: opts, spam_params: spam_params).execute discussion.first_note.reload expect(discussion.resolved?).to be(true) end it 'added a system note to the discussion' do - described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params).execute + described_class.new(container: project, current_user: user, params: opts, spam_params: spam_params).execute reloaded_discussion = MergeRequest.find(merge_request.id).discussions.first @@ -688,7 +718,7 @@ RSpec.describe Issues::CreateService do it 'sets default title and description values if not provided' do result = described_class.new( - project: project, current_user: user, + container: project, current_user: user, params: opts, spam_params: spam_params ).execute @@ -702,7 +732,7 @@ RSpec.describe Issues::CreateService do it 'takes params from the request over the default values' do result = described_class.new( - project: project, + container: project, current_user: user, params: opts.merge( description: 'Custom issue description', @@ -723,14 +753,14 @@ RSpec.describe Issues::CreateService do let(:opts) { { merge_request_to_resolve_discussions_of: merge_request.iid } } it 'resolves the discussion' do - described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params).execute + described_class.new(container: project, current_user: user, params: opts, spam_params: spam_params).execute discussion.first_note.reload expect(discussion.resolved?).to be(true) end it 'added a system note to the discussion' do - described_class.new(project: project, current_user: user, params: opts, spam_params: spam_params).execute + described_class.new(container: project, current_user: user, params: opts, spam_params: spam_params).execute reloaded_discussion = MergeRequest.find(merge_request.id).discussions.first @@ -739,7 +769,7 @@ RSpec.describe Issues::CreateService do it 'sets default title and description values if not provided' do result = described_class.new( - project: project, current_user: user, + container: project, current_user: user, params: opts, spam_params: spam_params ).execute @@ -753,7 +783,7 @@ RSpec.describe Issues::CreateService do it 'takes params from the request over the default values' do result = described_class.new( - project: project, + container: project, current_user: user, params: opts.merge( description: 'Custom issue description', @@ -806,7 +836,7 @@ RSpec.describe Issues::CreateService do end subject do - described_class.new(project: project, current_user: user, params: params, spam_params: spam_params) + described_class.new(container: project, current_user: user, params: params, spam_params: spam_params) end it 'executes SpamActionService' do diff --git a/spec/services/issues/duplicate_service_spec.rb b/spec/services/issues/duplicate_service_spec.rb index 0eb0bbb1480..f49bce70cd0 100644 --- a/spec/services/issues/duplicate_service_spec.rb +++ b/spec/services/issues/duplicate_service_spec.rb @@ -10,7 +10,7 @@ RSpec.describe Issues::DuplicateService do let(:canonical_issue) { create(:issue, project: canonical_project) } let(:duplicate_issue) { create(:issue, project: duplicate_project) } - subject { described_class.new(project: duplicate_project, current_user: user) } + subject { described_class.new(container: duplicate_project, current_user: user) } describe '#execute' do context 'when the issues passed are the same' do diff --git a/spec/services/issues/export_csv_service_spec.rb b/spec/services/issues/export_csv_service_spec.rb index d3359447fd8..1ac64c0301d 100644 --- a/spec/services/issues/export_csv_service_spec.rb +++ b/spec/services/issues/export_csv_service_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Issues::ExportCsvService, :with_license do +RSpec.describe Issues::ExportCsvService, :with_license, feature_category: :team_planning do let_it_be(:user) { create(:user) } let_it_be(:group) { create(:group) } let_it_be(:project) { create(:project, :public, group: group) } @@ -57,137 +57,151 @@ RSpec.describe Issues::ExportCsvService, :with_license do time_estimate: 72000) end - it 'includes the columns required for import' do - expect(csv.headers).to include('Title', 'Description') - end - - it 'returns two issues' do - expect(csv.count).to eq(2) - end + shared_examples 'exports CSVs for issues' do + it 'includes the columns required for import' do + expect(csv.headers).to include('Title', 'Description') + end - specify 'iid' do - expect(csv[0]['Issue ID']).to eq issue.iid.to_s - end + it 'returns two issues' do + expect(csv.count).to eq(2) + end - specify 'url' do - expect(csv[0]['URL']).to match(/http.*#{project.full_path}.*#{issue.iid}/) - end + specify 'iid' do + expect(csv[0]['Issue ID']).to eq issue.iid.to_s + end - specify 'title' do - expect(csv[0]['Title']).to eq issue.title - end + specify 'url' do + expect(csv[0]['URL']).to match(/http.*#{project.full_path}.*#{issue.iid}/) + end - specify 'state' do - expect(csv[0]['State']).to eq 'Open' - end + specify 'title' do + expect(csv[0]['Title']).to eq issue.title + end - specify 'description' do - expect(csv[0]['Description']).to eq issue.description - expect(csv[1]['Description']).to eq nil - end + specify 'state' do + expect(csv[0]['State']).to eq 'Open' + end - specify 'author name' do - expect(csv[0]['Author']).to eq issue.author_name - end + specify 'description' do + expect(csv[0]['Description']).to eq issue.description + expect(csv[1]['Description']).to eq nil + end - specify 'author username' do - expect(csv[0]['Author Username']).to eq issue.author.username - end + specify 'author name' do + expect(csv[0]['Author']).to eq issue.author_name + end - specify 'assignee name' do - expect(csv[0]['Assignee']).to eq user.name - expect(csv[1]['Assignee']).to eq '' - end + specify 'author username' do + expect(csv[0]['Author Username']).to eq issue.author.username + end - specify 'assignee username' do - expect(csv[0]['Assignee Username']).to eq user.username - expect(csv[1]['Assignee Username']).to eq '' - end + specify 'assignee name' do + expect(csv[0]['Assignee']).to eq user.name + expect(csv[1]['Assignee']).to eq '' + end - specify 'confidential' do - expect(csv[0]['Confidential']).to eq 'No' - end + specify 'assignee username' do + expect(csv[0]['Assignee Username']).to eq user.username + expect(csv[1]['Assignee Username']).to eq '' + end - specify 'milestone' do - expect(csv[0]['Milestone']).to eq issue.milestone.title - expect(csv[1]['Milestone']).to eq nil - end + specify 'confidential' do + expect(csv[0]['Confidential']).to eq 'No' + end - specify 'labels' do - expect(csv[0]['Labels']).to eq 'Feature,Idea' - expect(csv[1]['Labels']).to eq nil - end + specify 'milestone' do + expect(csv[0]['Milestone']).to eq issue.milestone.title + expect(csv[1]['Milestone']).to eq nil + end - specify 'due_date' do - expect(csv[0]['Due Date']).to eq '2014-03-02' - expect(csv[1]['Due Date']).to eq nil - end + specify 'labels' do + expect(csv[0]['Labels']).to eq 'Feature,Idea' + expect(csv[1]['Labels']).to eq nil + end - specify 'created_at' do - expect(csv[0]['Created At (UTC)']).to eq '2015-04-03 02:01:00' - end + specify 'due_date' do + expect(csv[0]['Due Date']).to eq '2014-03-02' + expect(csv[1]['Due Date']).to eq nil + end - specify 'updated_at' do - expect(csv[0]['Updated At (UTC)']).to eq '2016-05-04 03:02:01' - end + specify 'created_at' do + expect(csv[0]['Created At (UTC)']).to eq '2015-04-03 02:01:00' + end - specify 'closed_at' do - expect(csv[0]['Closed At (UTC)']).to eq '2017-06-05 04:03:02' - expect(csv[1]['Closed At (UTC)']).to eq nil - end + specify 'updated_at' do + expect(csv[0]['Updated At (UTC)']).to eq '2016-05-04 03:02:01' + end - specify 'discussion_locked' do - expect(csv[0]['Locked']).to eq 'Yes' - end + specify 'closed_at' do + expect(csv[0]['Closed At (UTC)']).to eq '2017-06-05 04:03:02' + expect(csv[1]['Closed At (UTC)']).to eq nil + end - specify 'weight' do - expect(csv[0]['Weight']).to eq '4' - end + specify 'discussion_locked' do + expect(csv[0]['Locked']).to eq 'Yes' + end - specify 'time estimate' do - expect(csv[0]['Time Estimate']).to eq '72000' - expect(csv[1]['Time Estimate']).to eq '0' - end + specify 'weight' do + expect(csv[0]['Weight']).to eq '4' + end - specify 'time spent' do - expect(csv[0]['Time Spent']).to eq '560' - expect(csv[1]['Time Spent']).to eq '0' - end + specify 'time estimate' do + expect(csv[0]['Time Estimate']).to eq '72000' + expect(csv[1]['Time Estimate']).to eq '0' + end - context 'with issues filtered by labels and project' do - subject do - described_class.new( - IssuesFinder.new(user, - project_id: project.id, - label_name: %w(Idea Feature)).execute, project) + specify 'time spent' do + expect(csv[0]['Time Spent']).to eq '560' + expect(csv[1]['Time Spent']).to eq '0' end - it 'returns only filtered objects' do - expect(csv.count).to eq(1) - expect(csv[0]['Issue ID']).to eq issue.iid.to_s + context 'with issues filtered by labels and project' do + subject do + described_class.new( + IssuesFinder.new(user, + project_id: project.id, + label_name: %w(Idea Feature)).execute, project) + end + + it 'returns only filtered objects' do + expect(csv.count).to eq(1) + expect(csv[0]['Issue ID']).to eq issue.iid.to_s + end end - end - context 'with label links' do - let(:labeled_issues) { create_list(:labeled_issue, 2, project: project, author: user, labels: [feature_label, idea_label]) } + context 'with label links' do + let(:labeled_issues) { create_list(:labeled_issue, 2, project: project, author: user, labels: [feature_label, idea_label]) } - it 'does not run a query for each label link' do - control_count = ActiveRecord::QueryRecorder.new { csv }.count + it 'does not run a query for each label link' do + control_count = ActiveRecord::QueryRecorder.new { csv }.count - labeled_issues + labeled_issues - expect { csv }.not_to exceed_query_limit(control_count) - expect(csv.count).to eq(4) - end + expect { csv }.not_to exceed_query_limit(control_count) + expect(csv.count).to eq(4) + end - it 'returns the labels in sorted order' do - labeled_issues + it 'returns the labels in sorted order' do + labeled_issues - labeled_rows = csv.select { |entry| labeled_issues.map(&:iid).include?(entry['Issue ID'].to_i) } - expect(labeled_rows.count).to eq(2) - expect(labeled_rows.map { |entry| entry['Labels'] }).to all(eq("Feature,Idea")) + labeled_rows = csv.select { |entry| labeled_issues.map(&:iid).include?(entry['Issue ID'].to_i) } + expect(labeled_rows.count).to eq(2) + expect(labeled_rows.map { |entry| entry['Labels'] }).to all(eq("Feature,Idea")) + end end end + + context 'with export_csv_preload_in_batches feature flag disabled' do + before do + stub_feature_flags(export_csv_preload_in_batches: false) + end + + it_behaves_like 'exports CSVs for issues' + end + + context 'with export_csv_preload_in_batches feature flag enabled' do + it_behaves_like 'exports CSVs for issues' + end end context 'with minimal details' do diff --git a/spec/services/issues/import_csv_service_spec.rb b/spec/services/issues/import_csv_service_spec.rb index 9ad1d7dba9f..90e360f9cf1 100644 --- a/spec/services/issues/import_csv_service_spec.rb +++ b/spec/services/issues/import_csv_service_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Issues::ImportCsvService do +RSpec.describe Issues::ImportCsvService, feature_category: :team_planning do let(:project) { create(:project) } let(:user) { create(:user) } let(:assignee) { create(:user, username: 'csv_assignee') } diff --git a/spec/services/issues/move_service_spec.rb b/spec/services/issues/move_service_spec.rb index 324b2aa9fe2..12924df3200 100644 --- a/spec/services/issues/move_service_spec.rb +++ b/spec/services/issues/move_service_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Issues::MoveService do +RSpec.describe Issues::MoveService, feature_category: :team_planning do include DesignManagementTestHelpers let_it_be(:user) { create(:user) } @@ -20,7 +20,7 @@ RSpec.describe Issues::MoveService do end subject(:move_service) do - described_class.new(project: old_project, current_user: user) + described_class.new(container: old_project, current_user: user) end shared_context 'user can move issue' do diff --git a/spec/services/issues/referenced_merge_requests_service_spec.rb b/spec/services/issues/referenced_merge_requests_service_spec.rb index 16166c1fa33..aee3583b834 100644 --- a/spec/services/issues/referenced_merge_requests_service_spec.rb +++ b/spec/services/issues/referenced_merge_requests_service_spec.rb @@ -26,7 +26,7 @@ RSpec.describe Issues::ReferencedMergeRequestsService do let_it_be(:referencing_mr) { create_referencing_mr(source_project: project, source_branch: 'csv') } let_it_be(:referencing_mr_other_project) { create_referencing_mr(source_project: other_project, source_branch: 'csv') } - let(:service) { described_class.new(project: project, current_user: user) } + let(:service) { described_class.new(container: project, current_user: user) } describe '#execute' do it 'returns a list of sorted merge requests' do diff --git a/spec/services/issues/related_branches_service_spec.rb b/spec/services/issues/related_branches_service_spec.rb index 95d456c1b05..05c61d0abfc 100644 --- a/spec/services/issues/related_branches_service_spec.rb +++ b/spec/services/issues/related_branches_service_spec.rb @@ -9,7 +9,7 @@ RSpec.describe Issues::RelatedBranchesService do let(:user) { developer } - subject { described_class.new(project: project, current_user: user) } + subject { described_class.new(container: project, current_user: user) } before_all do project.add_developer(developer) @@ -54,7 +54,7 @@ RSpec.describe Issues::RelatedBranchesService do merge_request.create_cross_references!(user) referenced_merge_requests = Issues::ReferencedMergeRequestsService - .new(project: issue.project, current_user: user) + .new(container: issue.project, current_user: user) .referenced_merge_requests(issue) expect(referenced_merge_requests).not_to be_empty diff --git a/spec/services/issues/reopen_service_spec.rb b/spec/services/issues/reopen_service_spec.rb index 529b3ff266b..68015a2327e 100644 --- a/spec/services/issues/reopen_service_spec.rb +++ b/spec/services/issues/reopen_service_spec.rb @@ -12,7 +12,7 @@ RSpec.describe Issues::ReopenService do guest = create(:user) project.add_guest(guest) - described_class.new(project: project, current_user: guest).execute(issue) + described_class.new(container: project, current_user: guest).execute(issue) expect(issue).to be_closed end @@ -21,7 +21,7 @@ RSpec.describe Issues::ReopenService do it 'does close the issue even if user is not authorized' do non_authorized_user = create(:user) - service = described_class.new(project: project, current_user: non_authorized_user) + service = described_class.new(container: project, current_user: non_authorized_user) expect do service.execute(issue, skip_authorization: true) @@ -33,7 +33,7 @@ RSpec.describe Issues::ReopenService do context 'when user is authorized to reopen issue' do let(:user) { create(:user) } - subject(:execute) { described_class.new(project: project, current_user: user).execute(issue) } + subject(:execute) { described_class.new(container: project, current_user: user).execute(issue) } before do project.add_maintainer(user) diff --git a/spec/services/issues/reorder_service_spec.rb b/spec/services/issues/reorder_service_spec.rb index 392930c1b9f..430a9e9f526 100644 --- a/spec/services/issues/reorder_service_spec.rb +++ b/spec/services/issues/reorder_service_spec.rb @@ -85,6 +85,6 @@ RSpec.describe Issues::ReorderService do end def service(params) - described_class.new(project: project, current_user: user, params: params) + described_class.new(container: project, current_user: user, params: params) end end diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb index 930766c520b..973025bd2e3 100644 --- a/spec/services/issues/update_service_spec.rb +++ b/spec/services/issues/update_service_spec.rb @@ -45,7 +45,7 @@ RSpec.describe Issues::UpdateService, :mailer do end def update_issue(opts) - described_class.new(project: project, current_user: user, params: opts).execute(issue) + described_class.new(container: project, current_user: user, params: opts).execute(issue) end it_behaves_like 'issuable update service updating last_edited_at values' do @@ -106,29 +106,29 @@ RSpec.describe Issues::UpdateService, :mailer do context 'when updating milestone' do before do - update_issue({ milestone: nil }) + update_issue({ milestone_id: nil }) end it 'updates issue milestone when passing `milestone` param' do - expect { update_issue({ milestone: milestone }) } + expect { update_issue({ milestone_id: milestone.id }) } .to change(issue, :milestone).to(milestone).from(nil) end it "triggers 'issuableMilestoneUpdated'" do expect(GraphqlTriggers).to receive(:issuable_milestone_updated).with(issue).and_call_original - update_issue({ milestone: milestone }) + update_issue({ milestone_id: milestone.id }) end context 'when milestone remains unchanged' do before do - update_issue({ title: 'abc', milestone: milestone }) + update_issue({ title: 'abc', milestone_id: milestone.id }) end it "does not trigger 'issuableMilestoneUpdated'" do expect(GraphqlTriggers).not_to receive(:issuable_milestone_updated) - update_issue({ milestone: milestone }) + update_issue({ milestone_id: milestone.id }) end end end @@ -420,7 +420,7 @@ RSpec.describe Issues::UpdateService, :mailer do opts[:move_between_ids] = [issue_1.id, issue_2.id] - described_class.new(project: issue_3.project, current_user: user, params: opts).execute(issue_3) + described_class.new(container: issue_3.project, current_user: user, params: opts).execute(issue_3) expect(issue_2.relative_position).to be_between(issue_1.relative_position, issue_2.relative_position) end end @@ -428,7 +428,7 @@ RSpec.describe Issues::UpdateService, :mailer do context 'when current user cannot admin issues in the project' do it 'filters out params that cannot be set without the :admin_issue permission' do described_class.new( - project: project, current_user: guest, params: opts.merge( + container: project, current_user: guest, params: opts.merge( confidential: true, issue_type: 'test_case' ) @@ -755,14 +755,14 @@ RSpec.describe Issues::UpdateService, :mailer do end it 'marks todos as done' do - update_issue(milestone: create(:milestone, project: project)) + update_issue(milestone_id: create(:milestone, project: project).id) expect(todo.reload.done?).to eq true end it 'sends notifications for subscribers of changed milestone', :sidekiq_might_not_need_inline do perform_enqueued_jobs do - update_issue(milestone: create(:milestone, project: project)) + update_issue(milestone_id: create(:milestone, project: project).id) end should_email(subscriber) @@ -779,7 +779,7 @@ RSpec.describe Issues::UpdateService, :mailer do expect(service).to receive(:delete_cache).and_call_original end - update_issue(milestone: milestone) + update_issue(milestone_id: milestone.id) end end @@ -803,7 +803,7 @@ RSpec.describe Issues::UpdateService, :mailer do expect(service).to receive(:delete_cache).and_call_original end - update_issue(milestone: new_milestone) + update_issue(milestone_id: new_milestone.id) end end @@ -838,7 +838,7 @@ RSpec.describe Issues::UpdateService, :mailer do opts = { label_ids: [label.id] } perform_enqueued_jobs do - @issue = described_class.new(project: project, current_user: user, params: opts).execute(issue) + @issue = described_class.new(container: project, current_user: user, params: opts).execute(issue) end should_email(subscriber) @@ -854,7 +854,7 @@ RSpec.describe Issues::UpdateService, :mailer do opts = { label_ids: [label.id, label2.id] } perform_enqueued_jobs do - @issue = described_class.new(project: project, current_user: user, params: opts).execute(issue) + @issue = described_class.new(container: project, current_user: user, params: opts).execute(issue) end should_not_email(subscriber) @@ -865,7 +865,7 @@ RSpec.describe Issues::UpdateService, :mailer do opts = { label_ids: [label2.id] } perform_enqueued_jobs do - @issue = described_class.new(project: project, current_user: user, params: opts).execute(issue) + @issue = described_class.new(container: project, current_user: user, params: opts).execute(issue) end should_not_email(subscriber) @@ -897,7 +897,7 @@ RSpec.describe Issues::UpdateService, :mailer do line_number: 1 } } - service = described_class.new(project: project, current_user: user, params: params) + service = described_class.new(container: project, current_user: user, params: params) expect(Spam::SpamActionService).not_to receive(:new) @@ -915,7 +915,7 @@ RSpec.describe Issues::UpdateService, :mailer do line_number: 1 } } - service = described_class.new(project: project, current_user: user, params: params) + service = described_class.new(container: project, current_user: user, params: params) expect(service).to receive(:after_update).with(issue, {}) @@ -991,7 +991,7 @@ RSpec.describe Issues::UpdateService, :mailer do context 'updating labels' do let(:label3) { create(:label, project: project) } - let(:result) { described_class.new(project: project, current_user: user, params: params).execute(issue).reload } + let(:result) { described_class.new(container: project, current_user: user, params: params).execute(issue).reload } context 'when add_label_ids and label_ids are passed' do let(:params) { { label_ids: [label.id], add_label_ids: [label3.id] } } @@ -1063,7 +1063,7 @@ RSpec.describe Issues::UpdateService, :mailer do end context 'updating dates' do - subject(:result) { described_class.new(project: project, current_user: user, params: params).execute(issue) } + subject(:result) { described_class.new(container: project, current_user: user, params: params).execute(issue) } let(:updated_date) { 1.week.from_now.to_date } @@ -1428,7 +1428,7 @@ RSpec.describe Issues::UpdateService, :mailer do it 'raises an error for invalid move ids' do opts = { move_between_ids: [9000, non_existing_record_id] } - expect { described_class.new(project: issue.project, current_user: user, params: opts).execute(issue) } + expect { described_class.new(container: issue.project, current_user: user, params: opts).execute(issue) } .to raise_error(ActiveRecord::RecordNotFound) end end @@ -1473,7 +1473,33 @@ RSpec.describe Issues::UpdateService, :mailer do it_behaves_like 'issuable record that supports quick actions' do let(:existing_issue) { create(:issue, project: project) } - let(:issuable) { described_class.new(project: project, current_user: user, params: params).execute(existing_issue) } + let(:issuable) { described_class.new(container: project, current_user: user, params: params).execute(existing_issue) } + end + + context 'with quick actions' do + context 'as work item' do + let(:opts) { { description: "/shrug" } } + + context 'when work item type is not the default Issue' do + let(:issue) { create(:work_item, :task, description: "") } + + it 'does not apply the quick action' do + expect do + update_issue(opts) + end.to change(issue, :description).to("/shrug") + end + end + + context 'when work item type is the default Issue' do + let(:issue) { create(:work_item, :issue, description: "") } + + it 'does not apply the quick action' do + expect do + update_issue(opts) + end.to change(issue, :description).to(" ¯\\_(ツ)_/¯") + end + end + end end end end diff --git a/spec/services/issues/zoom_link_service_spec.rb b/spec/services/issues/zoom_link_service_spec.rb index ad1f91ab5e6..230e4c1b5e1 100644 --- a/spec/services/issues/zoom_link_service_spec.rb +++ b/spec/services/issues/zoom_link_service_spec.rb @@ -7,7 +7,7 @@ RSpec.describe Issues::ZoomLinkService do let_it_be(:issue) { create(:issue) } let(:project) { issue.project } - let(:service) { described_class.new(project: project, current_user: user, params: { issue: issue }) } + let(:service) { described_class.new(container: project, current_user: user, params: { issue: issue }) } let(:zoom_link) { 'https://zoom.us/j/123456789' } before do diff --git a/spec/services/jira_connect_installations/update_service_spec.rb b/spec/services/jira_connect_installations/update_service_spec.rb index ec5bb5d6d6a..15f3b485b20 100644 --- a/spec/services/jira_connect_installations/update_service_spec.rb +++ b/spec/services/jira_connect_installations/update_service_spec.rb @@ -45,8 +45,9 @@ RSpec.describe JiraConnectInstallations::UpdateService, feature_category: :integ let_it_be_with_reload(:installation) { create(:jira_connect_installation, instance_url: 'https://other_gitlab.example.com') } it 'sends an installed event to the instance', :aggregate_failures do - expect_next_instance_of(JiraConnectInstallations::ProxyLifecycleEventService, installation, :installed, -'https://other_gitlab.example.com') do |proxy_lifecycle_events_service| + expect_next_instance_of( + JiraConnectInstallations::ProxyLifecycleEventService, installation, :installed, 'https://other_gitlab.example.com' + ) do |proxy_lifecycle_events_service| expect(proxy_lifecycle_events_service).to receive(:execute).and_return(ServiceResponse.new(status: :success)) end @@ -62,19 +63,19 @@ RSpec.describe JiraConnectInstallations::UpdateService, feature_category: :integ stub_request(:post, 'https://other_gitlab.example.com/-/jira_connect/events/uninstalled') end - it 'starts an async worker to send an uninstalled event to the previous instance' do - expect(JiraConnect::SendUninstalledHookWorker).to receive(:perform_async).with(installation.id, 'https://other_gitlab.example.com') - + it 'sends an installed event to the instance and updates instance_url' do expect(JiraConnectInstallations::ProxyLifecycleEventService) .to receive(:execute).with(installation, :installed, 'https://gitlab.example.com') .and_return(ServiceResponse.new(status: :success)) + expect(JiraConnect::SendUninstalledHookWorker).not_to receive(:perform_async) + execute_service expect(installation.instance_url).to eq(update_params[:instance_url]) end - context 'and the new instance_url is empty' do + context 'and the new instance_url is nil' do let(:update_params) { { instance_url: nil } } it 'starts an async worker to send an uninstalled event to the previous instance' do @@ -98,8 +99,9 @@ RSpec.describe JiraConnectInstallations::UpdateService, feature_category: :integ let(:update_params) { { instance_url: 'https://gitlab.example.com' } } it 'sends an installed event to the instance and updates instance_url' do - expect_next_instance_of(JiraConnectInstallations::ProxyLifecycleEventService, installation, :installed, -'https://gitlab.example.com') do |proxy_lifecycle_events_service| + expect_next_instance_of( + JiraConnectInstallations::ProxyLifecycleEventService, installation, :installed, 'https://gitlab.example.com' + ) do |proxy_lifecycle_events_service| expect(proxy_lifecycle_events_service).to receive(:execute).and_return(ServiceResponse.new(status: :success)) end diff --git a/spec/services/keys/revoke_service_spec.rb b/spec/services/keys/revoke_service_spec.rb new file mode 100644 index 00000000000..ec07701b4b7 --- /dev/null +++ b/spec/services/keys/revoke_service_spec.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Keys::RevokeService, feature_category: :source_code_management do + let(:user) { create(:user) } + + subject(:service) { described_class.new(user) } + + it 'destroys a key' do + key = create(:key) + + expect { service.execute(key) }.to change { key.persisted? }.from(true).to(false) + end + + it 'unverifies associated signatures' do + key = create(:key) + signature = create(:ssh_signature, key: key) + + expect do + service.execute(key) + end.to change { signature.reload.key }.from(key).to(nil) + .and change { signature.reload.verification_status }.from('verified').to('revoked_key') + end + + it 'does not unverifies signatures if destroy fails' do + key = create(:key) + signature = create(:ssh_signature, key: key) + + expect(key).to receive(:destroy).and_return(false) + + expect { service.execute(key) }.not_to change { signature.reload.verification_status } + expect(key).to be_persisted + end + + context 'when revoke_ssh_signatures disabled' do + before do + stub_feature_flags(revoke_ssh_signatures: false) + end + + it 'does not unverifies signatures' do + key = create(:key) + signature = create(:ssh_signature, key: key) + + expect { service.execute(key) }.not_to change { signature.reload.verification_status } + end + end +end diff --git a/spec/services/lfs/file_transformer_spec.rb b/spec/services/lfs/file_transformer_spec.rb index 9d4d8851c2d..c90d7af022f 100644 --- a/spec/services/lfs/file_transformer_spec.rb +++ b/spec/services/lfs/file_transformer_spec.rb @@ -2,7 +2,7 @@ require "spec_helper" -RSpec.describe Lfs::FileTransformer, feature_category: :git_lfs do +RSpec.describe Lfs::FileTransformer, feature_category: :source_code_management do let(:project) { create(:project, :repository, :wiki_repo) } let(:repository) { project.repository } let(:file_content) { 'Test file content' } diff --git a/spec/services/members/approve_access_request_service_spec.rb b/spec/services/members/approve_access_request_service_spec.rb index d26bab7bb0a..ca5c052d032 100644 --- a/spec/services/members/approve_access_request_service_spec.rb +++ b/spec/services/members/approve_access_request_service_spec.rb @@ -30,6 +30,14 @@ RSpec.describe Members::ApproveAccessRequestService do expect(member.requested_at).to be_nil end + it 'calls the method to resolve access request for the approver' do + expect_next_instance_of(described_class) do |instance| + expect(instance).to receive(:resolve_access_request_todos).with(current_user, access_requester) + end + + described_class.new(current_user, params).execute(access_requester, **opts) + end + context 'with a custom access level' do let(:params) { { access_level: custom_access_level } } diff --git a/spec/services/members/base_service_spec.rb b/spec/services/members/base_service_spec.rb new file mode 100644 index 00000000000..b2db599db9c --- /dev/null +++ b/spec/services/members/base_service_spec.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Members::BaseService, feature_category: :projects do + let_it_be(:current_user) { create(:user) } + let_it_be(:access_requester) { create(:group_member) } + + describe '#resolve_access_request_todos' do + it 'calls the resolve_access_request_todos of todo service' do + expect_next_instance_of(TodoService) do |todo_service| + expect(todo_service) + .to receive(:resolve_access_request_todos).with(current_user, access_requester) + end + + described_class.new.send(:resolve_access_request_todos, current_user, access_requester) + end + end +end diff --git a/spec/services/members/destroy_service_spec.rb b/spec/services/members/destroy_service_spec.rb index d8a8d5881bf..2b956bec469 100644 --- a/spec/services/members/destroy_service_spec.rb +++ b/spec/services/members/destroy_service_spec.rb @@ -41,6 +41,14 @@ RSpec.describe Members::DestroyService, feature_category: :subgroups do .not_to change { member_user.notification_settings.count } end end + + it 'resolves the access request todos for the owner' do + expect_next_instance_of(described_class) do |instance| + expect(instance).to receive(:resolve_access_request_todos).with(current_user, member) + end + + described_class.new(current_user).execute(member, **opts) + end end shared_examples 'a service destroying a member with access' do @@ -111,26 +119,6 @@ RSpec.describe Members::DestroyService, feature_category: :subgroups do subject(:destroy_member) { service_object.execute(member_to_delete, **opts) } - shared_examples_for 'deletes the member without using a lock' do - it 'does not try to perform the delete within a lock' do - # `UpdateHighestRole` concern also uses locks to peform work - # whenever a Member is committed, so that needs to be accounted for. - lock_key_for_update_highest_role = "update_highest_role:#{member_to_delete.user_id}" - expect(Gitlab::ExclusiveLease) - .to receive(:new).with(lock_key_for_update_highest_role, timeout: 10.minutes.to_i).and_call_original - - # We do not use any locks for member deletion process. - expect(Gitlab::ExclusiveLease) - .not_to receive(:new).with(lock_key, timeout: timeout) - - destroy_member - end - - it 'destroys the membership' do - expect { destroy_member }.to change { entity.members.count }.by(-1) - end - end - context 'for group members' do before do group.add_owner(current_user) @@ -171,13 +159,70 @@ RSpec.describe Members::DestroyService, feature_category: :subgroups do context 'deleting group members that are not owners' do let!(:member_to_delete) { group.add_developer(member_user) } - it_behaves_like 'deletes the member without using a lock' do - let(:entity) { group } + it 'does not try to perform the deletion of the member within a lock' do + # We need to account for other places involved in the Member deletion process that + # uses ExclusiveLease. + + # 1. `UpdateHighestRole` concern uses locks to peform work + # whenever a Member is committed, so that needs to be accounted for. + lock_key_for_update_highest_role = "update_highest_role:#{member_to_delete.user_id}" + + expect(Gitlab::ExclusiveLease) + .to receive(:new).with(lock_key_for_update_highest_role, timeout: 10.minutes.to_i).and_call_original + + # 2. `Users::RefreshAuthorizedProjectsService` also uses locks to perform work, + # whenever a user's authorizations has to be refreshed, so that needs to be accounted for as well. + lock_key_for_authorizations_refresh = "refresh_authorized_projects:#{member_to_delete.user_id}" + + expect(Gitlab::ExclusiveLease) + .to receive(:new).with(lock_key_for_authorizations_refresh, timeout: 1.minute.to_i).and_call_original + + # We do not use any locks for the member deletion process, from within this service. + expect(Gitlab::ExclusiveLease) + .not_to receive(:new).with(lock_key, timeout: timeout) + + destroy_member + end + + it 'destroys the membership' do + expect { destroy_member }.to change { group.members.count }.by(-1) end end end context 'for project members' do + shared_examples_for 'deletes the project member without using a lock' do + it 'does not try to perform the deletion of a project member within a lock' do + # We need to account for other places involved in the Member deletion process that + # uses ExclusiveLease. + + # 1. `UpdateHighestRole` concern uses locks to peform work + # whenever a Member is committed, so that needs to be accounted for. + lock_key_for_update_highest_role = "update_highest_role:#{member_to_delete.user_id}" + + expect(Gitlab::ExclusiveLease) + .to receive(:new).with(lock_key_for_update_highest_role, timeout: 10.minutes.to_i).and_call_original + + # 2. `AuthorizedProjectUpdate::ProjectRecalculatePerUserWorker` also uses locks to perform work, + # whenever a user's authorizations has to be refreshed, so that needs to be accounted for as well. + lock_key_for_authorizations_refresh = + "authorized_project_update/project_recalculate_worker/projects/#{member_to_delete.project.id}" + + expect(Gitlab::ExclusiveLease) + .to receive(:new).with(lock_key_for_authorizations_refresh, timeout: 10.seconds).and_call_original + + # We do not use any locks for the member deletion process, from within this service. + expect(Gitlab::ExclusiveLease) + .not_to receive(:new).with(lock_key, timeout: timeout) + + destroy_member + end + + it 'destroys the membership' do + expect { destroy_member }.to change { entity.members.count }.by(-1) + end + end + before do group_project.add_owner(current_user) end @@ -186,16 +231,16 @@ RSpec.describe Members::DestroyService, feature_category: :subgroups do context 'deleting project owners' do let!(:member_to_delete) { entity.add_owner(member_user) } - it_behaves_like 'deletes the member without using a lock' do + it_behaves_like 'deletes the project member without using a lock' do let(:entity) { group_project } end end end - context 'deleting project memebrs that are not owners' do + context 'deleting project members that are not owners' do let!(:member_to_delete) { group_project.add_developer(member_user) } - it_behaves_like 'deletes the member without using a lock' do + it_behaves_like 'deletes the project member without using a lock' do let(:entity) { group_project } end end diff --git a/spec/services/members/projects/creator_service_spec.rb b/spec/services/members/projects/creator_service_spec.rb index 8304bee2ffc..5dfba7adf0f 100644 --- a/spec/services/members/projects/creator_service_spec.rb +++ b/spec/services/members/projects/creator_service_spec.rb @@ -27,6 +27,8 @@ RSpec.describe Members::Projects::CreatorService do context 'authorized projects update' do it 'schedules a single project authorization update job when called multiple times' do + stub_feature_flags(do_not_run_safety_net_auth_refresh_jobs: false) + expect(AuthorizedProjectUpdate::UserRefreshFromReplicaWorker).to receive(:bulk_perform_in).once 1.upto(3) do diff --git a/spec/services/merge_requests/after_create_service_spec.rb b/spec/services/merge_requests/after_create_service_spec.rb index f477b2166d9..f2823b1f0c7 100644 --- a/spec/services/merge_requests/after_create_service_spec.rb +++ b/spec/services/merge_requests/after_create_service_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe MergeRequests::AfterCreateService do +RSpec.describe MergeRequests::AfterCreateService, feature_category: :code_review_workflow do let_it_be(:merge_request) { create(:merge_request) } subject(:after_create_service) do @@ -126,6 +126,17 @@ RSpec.describe MergeRequests::AfterCreateService do end end + it 'updates the prepared_at' do + # Need to reset the `prepared_at` since it can be already set in preceding tests. + merge_request.update!(prepared_at: nil) + + freeze_time do + expect { execute_service }.to change { merge_request.prepared_at } + .from(nil) + .to(Time.current) + end + end + it 'increments the usage data counter of create event' do counter = Gitlab::UsageDataCounters::MergeRequestCounter diff --git a/spec/services/merge_requests/build_service_spec.rb b/spec/services/merge_requests/build_service_spec.rb index 79c779678a4..0fcfc16af73 100644 --- a/spec/services/merge_requests/build_service_spec.rb +++ b/spec/services/merge_requests/build_service_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true require 'spec_helper' -RSpec.describe MergeRequests::BuildService do +RSpec.describe MergeRequests::BuildService, feature_category: :code_review_workflow do using RSpec::Parameterized::TableSyntax include RepoHelpers include ProjectForksHelper diff --git a/spec/services/merge_requests/close_service_spec.rb b/spec/services/merge_requests/close_service_spec.rb index b3c4ed4c544..2c0817550c6 100644 --- a/spec/services/merge_requests/close_service_spec.rb +++ b/spec/services/merge_requests/close_service_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe MergeRequests::CloseService do +RSpec.describe MergeRequests::CloseService, feature_category: :code_review_workflow do let(:user) { create(:user) } let(:user2) { create(:user) } let(:guest) { create(:user) } diff --git a/spec/services/merge_requests/create_from_issue_service_spec.rb b/spec/services/merge_requests/create_from_issue_service_spec.rb index 0eefbed252b..7bb0dd723a1 100644 --- a/spec/services/merge_requests/create_from_issue_service_spec.rb +++ b/spec/services/merge_requests/create_from_issue_service_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe MergeRequests::CreateFromIssueService do +RSpec.describe MergeRequests::CreateFromIssueService, feature_category: :code_review_workflow do include ProjectForksHelper let(:project) { create(:project, :repository) } diff --git a/spec/services/merge_requests/create_pipeline_service_spec.rb b/spec/services/merge_requests/create_pipeline_service_spec.rb index 7984fff3031..f11e3d0d1df 100644 --- a/spec/services/merge_requests/create_pipeline_service_spec.rb +++ b/spec/services/merge_requests/create_pipeline_service_spec.rb @@ -5,7 +5,7 @@ require 'spec_helper' RSpec.describe MergeRequests::CreatePipelineService, :clean_gitlab_redis_cache do include ProjectForksHelper - let_it_be(:project, reload: true) { create(:project, :repository) } + let_it_be(:project, refind: true) { create(:project, :repository) } let_it_be(:user) { create(:user) } let(:service) { described_class.new(project: project, current_user: actor, params: params) } diff --git a/spec/services/merge_requests/create_service_spec.rb b/spec/services/merge_requests/create_service_spec.rb index da8e8d944d6..394fc269ac3 100644 --- a/spec/services/merge_requests/create_service_spec.rb +++ b/spec/services/merge_requests/create_service_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe MergeRequests::CreateService, :clean_gitlab_redis_shared_state do +RSpec.describe MergeRequests::CreateService, :clean_gitlab_redis_shared_state, feature_category: :code_review_workflow do include ProjectForksHelper let(:project) { create(:project, :repository) } @@ -501,40 +501,12 @@ RSpec.describe MergeRequests::CreateService, :clean_gitlab_redis_shared_state do project.add_developer(user) end - context 'when async_merge_request_diff_creation is enabled' do - before do - stub_feature_flags(async_merge_request_diff_creation: true) - end - - it 'creates the merge request', :sidekiq_inline do - expect_next_instance_of(MergeRequest) do |instance| - expect(instance).not_to receive(:eager_fetch_ref!) - end - - merge_request = described_class.new(project: project, current_user: user, params: opts).execute - - expect(merge_request).to be_persisted - expect(merge_request.iid).to be > 0 - expect(merge_request.merge_request_diff).not_to be_empty - end - end - - context 'when async_merge_request_diff_creation is disabled' do - before do - stub_feature_flags(async_merge_request_diff_creation: false) - end - - it 'creates the merge request' do - expect_next_instance_of(MergeRequest) do |instance| - expect(instance).to receive(:eager_fetch_ref!).and_call_original - end - - merge_request = described_class.new(project: project, current_user: user, params: opts).execute + it 'creates the merge request', :sidekiq_inline do + merge_request = described_class.new(project: project, current_user: user, params: opts).execute - expect(merge_request).to be_persisted - expect(merge_request.iid).to be > 0 - expect(merge_request.merge_request_diff).not_to be_empty - end + expect(merge_request).to be_persisted + expect(merge_request.iid).to be > 0 + expect(merge_request.merge_request_diff).not_to be_empty end it 'does not create the merge request when the target project is archived' do diff --git a/spec/services/merge_requests/export_csv_service_spec.rb b/spec/services/merge_requests/export_csv_service_spec.rb index 97217e979a5..2f0036845e7 100644 --- a/spec/services/merge_requests/export_csv_service_spec.rb +++ b/spec/services/merge_requests/export_csv_service_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe MergeRequests::ExportCsvService do +RSpec.describe MergeRequests::ExportCsvService, feature_category: :importers do let_it_be(:merge_request) { create(:merge_request) } let(:csv) { CSV.parse(subject.csv_data, headers: true).first } @@ -113,5 +113,21 @@ RSpec.describe MergeRequests::ExportCsvService do end end end + + describe '#email' do + let_it_be(:user) { create(:user) } + + it 'emails csv' do + expect { subject.email(user) }.to change { ActionMailer::Base.deliveries.count } + end + + it 'renders with a target filesize' do + expect_next_instance_of(CsvBuilder) do |csv_builder| + expect(csv_builder).to receive(:render).with(described_class::TARGET_FILESIZE).once + end + + subject.email(user) + end + end end end diff --git a/spec/services/merge_requests/link_lfs_objects_service_spec.rb b/spec/services/merge_requests/link_lfs_objects_service_spec.rb index 96cb72baac2..9762b600eab 100644 --- a/spec/services/merge_requests/link_lfs_objects_service_spec.rb +++ b/spec/services/merge_requests/link_lfs_objects_service_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe MergeRequests::LinkLfsObjectsService, :sidekiq_inline do +RSpec.describe MergeRequests::LinkLfsObjectsService, :sidekiq_inline, feature_category: :code_review_workflow do include ProjectForksHelper include RepoHelpers diff --git a/spec/services/merge_requests/pushed_branches_service_spec.rb b/spec/services/merge_requests/pushed_branches_service_spec.rb index 59424263ec5..cb5d0a6bd25 100644 --- a/spec/services/merge_requests/pushed_branches_service_spec.rb +++ b/spec/services/merge_requests/pushed_branches_service_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe MergeRequests::PushedBranchesService do +RSpec.describe MergeRequests::PushedBranchesService, feature_category: :source_code_management do let(:project) { create(:project) } let!(:service) { described_class.new(project: project, current_user: nil, params: { changes: pushed_branches }) } diff --git a/spec/services/merge_requests/rebase_service_spec.rb b/spec/services/merge_requests/rebase_service_spec.rb index 316f20d8276..704dc1f9000 100644 --- a/spec/services/merge_requests/rebase_service_spec.rb +++ b/spec/services/merge_requests/rebase_service_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe MergeRequests::RebaseService do +RSpec.describe MergeRequests::RebaseService, feature_category: :source_code_management do include ProjectForksHelper let(:user) { create(:user) } diff --git a/spec/services/merge_requests/remove_approval_service_spec.rb b/spec/services/merge_requests/remove_approval_service_spec.rb index fd8240935e8..e4e54db5013 100644 --- a/spec/services/merge_requests/remove_approval_service_spec.rb +++ b/spec/services/merge_requests/remove_approval_service_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe MergeRequests::RemoveApprovalService do +RSpec.describe MergeRequests::RemoveApprovalService, feature_category: :code_review_workflow do describe '#execute' do let(:user) { create(:user) } let(:project) { create(:project) } diff --git a/spec/services/merge_requests/retarget_chain_service_spec.rb b/spec/services/merge_requests/retarget_chain_service_spec.rb index 187dd0cf589..ef8cd0a861e 100644 --- a/spec/services/merge_requests/retarget_chain_service_spec.rb +++ b/spec/services/merge_requests/retarget_chain_service_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe MergeRequests::RetargetChainService do +RSpec.describe MergeRequests::RetargetChainService, feature_category: :code_review_workflow do include ProjectForksHelper let_it_be(:user) { create(:user) } diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb index 344d93fc5ca..e20ebf18e7c 100644 --- a/spec/services/merge_requests/update_service_spec.rb +++ b/spec/services/merge_requests/update_service_spec.rb @@ -196,7 +196,7 @@ RSpec.describe MergeRequests::UpdateService, :mailer, feature_category: :code_re expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter) .to receive(:track_milestone_changed_action).once.with(user: user) - opts[:milestone] = milestone + opts[:milestone_id] = milestone.id MergeRequests::UpdateService.new(project: project, current_user: user, params: opts).execute(merge_request) end @@ -236,27 +236,17 @@ RSpec.describe MergeRequests::UpdateService, :mailer, feature_category: :code_re end context 'updating milestone' do - RSpec.shared_examples 'updates milestone' do + context 'with milestone_id param' do + let(:opts) { { milestone_id: milestone.id } } + it 'sets milestone' do expect(@merge_request.milestone).to eq milestone end end - context 'when milestone_id param' do - let(:opts) { { milestone_id: milestone.id } } - - it_behaves_like 'updates milestone' - end - - context 'when milestone param' do - let(:opts) { { milestone: milestone } } - - it_behaves_like 'updates milestone' - end - context 'milestone counters cache reset' do let(:milestone_old) { create(:milestone, project: project) } - let(:opts) { { milestone: milestone_old } } + let(:opts) { { milestone_id: milestone_old.id } } it 'deletes milestone counters' do expect_next_instance_of(Milestones::MergeRequestsCountService, milestone_old) do |service| @@ -267,7 +257,7 @@ RSpec.describe MergeRequests::UpdateService, :mailer, feature_category: :code_re expect(service).to receive(:delete_cache).and_call_original end - update_merge_request(milestone: milestone) + update_merge_request(milestone_id: milestone.id) end it 'deletes milestone counters when the milestone is removed' do @@ -275,17 +265,17 @@ RSpec.describe MergeRequests::UpdateService, :mailer, feature_category: :code_re expect(service).to receive(:delete_cache).and_call_original end - update_merge_request(milestone: nil) + update_merge_request(milestone_id: nil) end it 'deletes milestone counters when the milestone was not set' do - update_merge_request(milestone: nil) + update_merge_request(milestone_id: nil) expect_next_instance_of(Milestones::MergeRequestsCountService, milestone) do |service| expect(service).to receive(:delete_cache).and_call_original end - update_merge_request(milestone: milestone) + update_merge_request(milestone_id: milestone.id) end end end @@ -754,12 +744,12 @@ RSpec.describe MergeRequests::UpdateService, :mailer, feature_category: :code_re expect(service).to receive(:async_execute) end - update_merge_request({ milestone: create(:milestone, project: project) }) + update_merge_request(milestone_id: create(:milestone, project: project).id) end it 'sends notifications for subscribers of changed milestone', :sidekiq_might_not_need_inline do perform_enqueued_jobs do - update_merge_request(milestone: create(:milestone, project: project)) + update_merge_request(milestone_id: create(:milestone, project: project).id) end should_email(subscriber) diff --git a/spec/services/notes/create_service_spec.rb b/spec/services/notes/create_service_spec.rb index 22606cc2461..1ee9e51433e 100644 --- a/spec/services/notes/create_service_spec.rb +++ b/spec/services/notes/create_service_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Notes::CreateService do +RSpec.describe Notes::CreateService, feature_category: :team_planning do let_it_be(:project) { create(:project, :repository) } let_it_be(:issue) { create(:issue, project: project) } let_it_be(:user) { create(:user) } @@ -116,6 +116,35 @@ RSpec.describe Notes::CreateService do end end + context 'in a commit', :snowplow do + let_it_be(:commit) { create(:commit, project: project) } + let(:opts) { { note: 'Awesome comment', noteable_type: 'Commit', commit_id: commit.id } } + + let(:counter) { Gitlab::UsageDataCounters::NoteCounter } + + let(:execute_create_service) { described_class.new(project, user, opts).execute } + + before do + stub_feature_flags(notes_create_service_tracking: false) + end + + it 'tracks commit comment usage data', :clean_gitlab_redis_shared_state do + expect(counter).to receive(:count).with(:create, 'Commit').and_call_original + + expect do + execute_create_service + end.to change { counter.read(:create, 'Commit') }.by(1) + end + + it_behaves_like 'Snowplow event tracking with Redis context' do + let(:category) { described_class.name } + let(:action) { 'create_commit_comment' } + let(:label) { 'counts.commit_comment' } + let(:namespace) { project.namespace } + let(:feature_flag_name) { :route_hll_to_snowplow_phase4 } + end + end + describe 'event tracking', :snowplow do let(:event) { Gitlab::UsageDataCounters::IssueActivityUniqueCounter::ISSUE_COMMENT_ADDED } let(:execute_create_service) { described_class.new(project, user, opts).execute } @@ -409,7 +438,7 @@ RSpec.describe Notes::CreateService do end end - context 'for merge requests' do + context 'for merge requests', feature_category: :code_review_workflow do let_it_be(:merge_request) { create(:merge_request, source_project: project, labels: [bug_label]) } let(:issuable) { merge_request } @@ -483,7 +512,7 @@ RSpec.describe Notes::CreateService do end end - context 'personal snippet note' do + context 'personal snippet note', feature_category: :source_code_management do subject { described_class.new(nil, user, params).execute } let(:snippet) { create(:personal_snippet) } @@ -504,7 +533,7 @@ RSpec.describe Notes::CreateService do end end - context 'design note' do + context 'design note', feature_category: :design_management do subject(:service) { described_class.new(project, user, params) } let_it_be(:design) { create(:design, :with_file) } diff --git a/spec/services/notes/destroy_service_spec.rb b/spec/services/notes/destroy_service_spec.rb index 82caec52aee..744808525f5 100644 --- a/spec/services/notes/destroy_service_spec.rb +++ b/spec/services/notes/destroy_service_spec.rb @@ -91,5 +91,13 @@ RSpec.describe Notes::DestroyService do end end end + + it 'tracks design comment removal' do + note = create(:note_on_design, project: project) + expect(Gitlab::UsageDataCounters::IssueActivityUniqueCounter).to receive(:track_issue_design_comment_removed_action).with(author: note.author, + project: project) + + described_class.new(project, user).execute(note) + end end end diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index 1ad9234c939..4161f93cdac 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -99,7 +99,7 @@ RSpec.describe NotificationService, :mailer, feature_category: :team_planning do end end - shared_examples 'is not able to send notifications' do + shared_examples 'is not able to send notifications' do |check_delivery_jobs_queue: false| it 'does not send any notification' do user_1 = create(:user) recipient_1 = NotificationRecipient.new(user_1, :custom, custom_action: :new_release) @@ -107,12 +107,21 @@ RSpec.describe NotificationService, :mailer, feature_category: :team_planning do expect(Gitlab::AppLogger).to receive(:warn).with(message: 'Skipping sending notifications', user: current_user.id, klass: object.class.to_s, object_id: object.id) - action + if check_delivery_jobs_queue + expect do + action + end.to not_enqueue_mail_with(Notify, notification_method, @u_mentioned, anything, anything) + .and(not_enqueue_mail_with(Notify, notification_method, @u_guest_watcher, anything, anything)) + .and(not_enqueue_mail_with(Notify, notification_method, user_1, anything, anything)) + .and(not_enqueue_mail_with(Notify, notification_method, current_user, anything, anything)) + else + action - should_not_email(@u_mentioned) - should_not_email(@u_guest_watcher) - should_not_email(user_1) - should_not_email(current_user) + should_not_email(@u_mentioned) + should_not_email(@u_guest_watcher) + should_not_email(user_1) + should_not_email(current_user) + end end end @@ -123,13 +132,19 @@ RSpec.describe NotificationService, :mailer, feature_category: :team_planning do # * notification trigger # * participant # - shared_examples 'participating by note notification' do + shared_examples 'participating by note notification' do |check_delivery_jobs_queue: false| it 'emails the participant' do create(:note_on_issue, noteable: issuable, project_id: project.id, note: 'anything', author: participant) - notification_trigger + if check_delivery_jobs_queue + expect do + notification_trigger + end.to enqueue_mail_with(Notify, mailer_method, *expectation_args_for_user(participant)) + else + notification_trigger - should_email(participant) + should_email(participant) + end end context 'for subgroups' do @@ -140,14 +155,20 @@ RSpec.describe NotificationService, :mailer, feature_category: :team_planning do it 'emails the participant' do create(:note_on_issue, noteable: issuable, project_id: project.id, note: 'anything', author: @pg_participant) - notification_trigger + if check_delivery_jobs_queue + expect do + notification_trigger + end.to enqueue_mail_with(Notify, mailer_method, *expectation_args_for_user(@pg_participant)) + else + notification_trigger - should_email_nested_group_user(@pg_participant) + should_email_nested_group_user(@pg_participant) + end end end end - shared_examples 'participating by confidential note notification' do + shared_examples 'participating by confidential note notification' do |check_delivery_jobs_queue: false| context 'when user is mentioned on confidential note' do let_it_be(:guest_1) { create(:user) } let_it_be(:guest_2) { create(:user) } @@ -164,34 +185,55 @@ RSpec.describe NotificationService, :mailer, feature_category: :team_planning do note_text = "Mentions #{guest_2.to_reference}" create(:note_on_issue, noteable: issuable, project_id: project.id, note: confidential_note_text, confidential: true) create(:note_on_issue, noteable: issuable, project_id: project.id, note: note_text) - reset_delivered_emails! - notification_trigger + if check_delivery_jobs_queue + expect do + notification_trigger + end.to enqueue_mail_with(Notify, mailer_method, *expectation_args_for_user(guest_2)) + .and(enqueue_mail_with(Notify, mailer_method, *expectation_args_for_user(reporter))) + .and(not_enqueue_mail_with(Notify, mailer_method, *expectation_args_for_user(guest_1))) + else + reset_delivered_emails! + + notification_trigger - should_not_email(guest_1) - should_email(guest_2) - should_email(reporter) + should_not_email(guest_1) + should_email(guest_2) + should_email(reporter) + end end end end - shared_examples 'participating by assignee notification' do + shared_examples 'participating by assignee notification' do |check_delivery_jobs_queue: false| it 'emails the participant' do issuable.assignees << participant - notification_trigger + if check_delivery_jobs_queue + expect do + notification_trigger + end.to enqueue_mail_with(Notify, mailer_method, *expectation_args_for_user(participant)) + else + notification_trigger - should_email(participant) + should_email(participant) + end end end - shared_examples 'participating by author notification' do + shared_examples 'participating by author notification' do |check_delivery_jobs_queue: false| it 'emails the participant' do issuable.author = participant - notification_trigger + if check_delivery_jobs_queue + expect do + notification_trigger + end.to enqueue_mail_with(Notify, mailer_method, *expectation_args_for_user(participant)) + else + notification_trigger - should_email(participant) + should_email(participant) + end end end @@ -205,10 +247,10 @@ RSpec.describe NotificationService, :mailer, feature_category: :team_planning do end end - shared_examples_for 'participating notifications' do - it_behaves_like 'participating by note notification' - it_behaves_like 'participating by author notification' - it_behaves_like 'participating by assignee notification' + shared_examples_for 'participating notifications' do |check_delivery_jobs_queue: false| + it_behaves_like 'participating by note notification', check_delivery_jobs_queue: check_delivery_jobs_queue + it_behaves_like 'participating by author notification', check_delivery_jobs_queue: check_delivery_jobs_queue + it_behaves_like 'participating by assignee notification', check_delivery_jobs_queue: check_delivery_jobs_queue end describe '.permitted_actions' do @@ -1159,7 +1201,7 @@ RSpec.describe NotificationService, :mailer, feature_category: :team_planning do end end - describe 'Issues', :deliver_mails_inline do + describe 'Issues', :aggregate_failures do let(:another_project) { create(:project, :public, namespace: group) } let(:issue) { create :issue, project: project, assignees: [assignee], description: 'cc @participant @unsubscribed_mentioned' } @@ -1184,79 +1226,77 @@ RSpec.describe NotificationService, :mailer, feature_category: :team_planning do describe '#new_issue' do it 'notifies the expected users' do - notification.new_issue(issue, @u_disabled) - - should_email(assignee) - should_email(@u_watcher) - should_email(@u_guest_watcher) - should_email(@u_guest_custom) - should_email(@u_custom_global) - should_email(@u_participant_mentioned) - should_email(@g_global_watcher) - should_email(@g_watcher) - should_email(@unsubscribed_mentioned) - should_email_nested_group_user(@pg_watcher) - should_not_email(@u_mentioned) - should_not_email(@u_participating) - should_not_email(@u_disabled) - should_not_email(@u_lazy_participant) - should_not_email_nested_group_user(@pg_disabled) - should_not_email_nested_group_user(@pg_mention) - end - - it do - create_global_setting_for(issue.assignees.first, :mention) - notification.new_issue(issue, @u_disabled) + expect do + notification.new_issue(issue, @u_disabled) + end.to enqueue_mail_with(Notify, :new_issue_email, assignee, issue, 'assigned') + .and(enqueue_mail_with(Notify, :new_issue_email, @u_watcher, issue, nil)) + .and(enqueue_mail_with(Notify, :new_issue_email, @u_guest_watcher, issue, nil)) + .and(enqueue_mail_with(Notify, :new_issue_email, @u_guest_custom, issue, nil)) + .and(enqueue_mail_with(Notify, :new_issue_email, @u_custom_global, issue, nil)) + .and(enqueue_mail_with(Notify, :new_issue_email, @u_participant_mentioned, issue, 'mentioned')) + .and(enqueue_mail_with(Notify, :new_issue_email, @g_global_watcher.id, issue.id, nil)) + .and(enqueue_mail_with(Notify, :new_issue_email, @g_watcher, issue, nil)) + .and(enqueue_mail_with(Notify, :new_issue_email, @unsubscribed_mentioned, issue, 'mentioned')) + .and(enqueue_mail_with(Notify, :new_issue_email, @pg_watcher, issue, nil)) + .and(not_enqueue_mail_with(Notify, :new_issue_email, @u_mentioned, anything, anything)) + .and(not_enqueue_mail_with(Notify, :new_issue_email, @u_participating, anything, anything)) + .and(not_enqueue_mail_with(Notify, :new_issue_email, @u_disabled, anything, anything)) + .and(not_enqueue_mail_with(Notify, :new_issue_email, @u_lazy_participant, anything, anything)) + .and(not_enqueue_mail_with(Notify, :new_issue_email, @pg_disabled, anything, anything)) + .and(not_enqueue_mail_with(Notify, :new_issue_email, @pg_mention, anything, anything)) + end + + context 'when user has an only mention notification setting' do + before do + create_global_setting_for(issue.assignees.first, :mention) + end - should_not_email(issue.assignees.first) + it 'does not send assignee notifications' do + expect do + notification.new_issue(issue, @u_disabled) + end.to not_enqueue_mail_with(Notify, :new_issue_email, issue.assignees.first, anything, anything) + end end it 'properly prioritizes notification reason' do # have assignee be both assigned and mentioned issue.update_attribute(:description, "/cc #{assignee.to_reference} #{@u_mentioned.to_reference}") - notification.new_issue(issue, @u_disabled) - - email = find_email_for(assignee) - expect(email).to have_header('X-GitLab-NotificationReason', 'assigned') - - email = find_email_for(@u_mentioned) - expect(email).to have_header('X-GitLab-NotificationReason', 'mentioned') + expect do + notification.new_issue(issue, @u_disabled) + end.to enqueue_mail_with(Notify, :new_issue_email, assignee, issue, 'assigned') + .and(enqueue_mail_with(Notify, :new_issue_email, @u_mentioned, issue, 'mentioned')) end it 'adds "assigned" reason for assignees if any' do - notification.new_issue(issue, @u_disabled) - - email = find_email_for(assignee) - - expect(email).to have_header('X-GitLab-NotificationReason', 'assigned') + expect do + notification.new_issue(issue, @u_disabled) + end.to enqueue_mail_with(Notify, :new_issue_email, assignee, issue, 'assigned') end it "emails any mentioned users with the mention level" do issue.description = @u_mentioned.to_reference - notification.new_issue(issue, @u_disabled) - - email = find_email_for(@u_mentioned) - expect(email).not_to be_nil - expect(email).to have_header('X-GitLab-NotificationReason', 'mentioned') + expect do + notification.new_issue(issue, @u_disabled) + end.to enqueue_mail_with(Notify, :new_issue_email, @u_mentioned, issue, 'mentioned') end it "emails the author if they've opted into notifications about their activity" do issue.author.notified_of_own_activity = true - notification.new_issue(issue, issue.author) - - should_email(issue.author) + expect do + notification.new_issue(issue, issue.author) + end.to enqueue_mail_with(Notify, :new_issue_email, issue.author, issue, 'own_activity') end it "doesn't email the author if they haven't opted into notifications about their activity" do - notification.new_issue(issue, issue.author) - - should_not_email(issue.author) + expect do + notification.new_issue(issue, issue.author) + end.to not_enqueue_mail_with(Notify, :new_issue_email, issue.author, anything, anything) end - it "emails subscribers of the issue's labels" do + it "emails subscribers of the issue's labels and adds `subscribed` reason" do user_1 = create(:user) user_2 = create(:user) user_3 = create(:user) @@ -1269,27 +1309,15 @@ RSpec.describe NotificationService, :mailer, feature_category: :team_planning do group_label.toggle_subscription(user_3, another_project) group_label.toggle_subscription(user_4) - notification.new_issue(issue, @u_disabled) - - should_email(user_1) - should_email(user_2) - should_not_email(user_3) - should_email(user_4) - end - - it 'adds "subscribed" reason to subscriber emails' do - user_1 = create(:user) - label = create(:label, project: project, issues: [issue]) - issue.reload - label.subscribe(user_1) - - notification.new_issue(issue, @u_disabled) - - email = find_email_for(user_1) - expect(email).to have_header('X-GitLab-NotificationReason', NotificationReason::SUBSCRIBED) + expect do + notification.new_issue(issue, issue.author) + end.to enqueue_mail_with(Notify, :new_issue_email, user_1, issue, NotificationReason::SUBSCRIBED) + .and(enqueue_mail_with(Notify, :new_issue_email, user_2, issue, NotificationReason::SUBSCRIBED)) + .and(enqueue_mail_with(Notify, :new_issue_email, user_4, issue, NotificationReason::SUBSCRIBED)) + .and(not_enqueue_mail_with(Notify, :new_issue_email, user_3, anything, anything)) end - it_behaves_like 'project emails are disabled' do + it_behaves_like 'project emails are disabled', check_delivery_jobs_queue: true do let(:notification_target) { issue } let(:notification_trigger) { notification.new_issue(issue, @u_disabled) } end @@ -1315,35 +1343,33 @@ RSpec.describe NotificationService, :mailer, feature_category: :team_planning do label.toggle_subscription(guest, project) label.toggle_subscription(admin, project) - reset_delivered_emails! - - notification.new_issue(confidential_issue, @u_disabled) - - should_not_email(@u_guest_watcher) - should_not_email(non_member) - should_not_email(author) - should_not_email(guest) - should_email(assignee) - should_email(member) - should_email(admin) + expect do + notification.new_issue(confidential_issue, issue.author) + end.to enqueue_mail_with(Notify, :new_issue_email, assignee, confidential_issue, NotificationReason::ASSIGNED) + .and(enqueue_mail_with(Notify, :new_issue_email, member, confidential_issue, NotificationReason::SUBSCRIBED)) + .and(enqueue_mail_with(Notify, :new_issue_email, admin, confidential_issue, NotificationReason::SUBSCRIBED)) + .and(not_enqueue_mail_with(Notify, :new_issue_email, @u_guest_watcher, anything, anything)) + .and(not_enqueue_mail_with(Notify, :new_issue_email, non_member, anything, anything)) + .and(not_enqueue_mail_with(Notify, :new_issue_email, author, anything, anything)) + .and(not_enqueue_mail_with(Notify, :new_issue_email, guest, anything, anything)) end end context 'when the author is not allowed to trigger notifications' do - let(:current_user) { nil } let(:object) { issue } let(:action) { notification.new_issue(issue, current_user) } + let(:notification_method) { :new_issue_email } context 'because they are blocked' do let(:current_user) { create(:user, :blocked) } - include_examples 'is not able to send notifications' + include_examples 'is not able to send notifications', check_delivery_jobs_queue: true end context 'because they are a ghost' do let(:current_user) { create(:user, :ghost) } - include_examples 'is not able to send notifications' + include_examples 'is not able to send notifications', check_delivery_jobs_queue: true end end end @@ -1354,9 +1380,52 @@ RSpec.describe NotificationService, :mailer, feature_category: :team_planning do let(:object) { mentionable } let(:action) { send_notifications(@u_mentioned, current_user: current_user) } - include_examples 'notifications for new mentions' + it 'sends no emails when no new mentions are present' do + send_notifications - it_behaves_like 'project emails are disabled' do + expect_no_delivery_jobs + end + + it 'emails new mentions with a watch level higher than mention' do + expect do + send_notifications(@u_watcher, @u_participant_mentioned, @u_custom_global, @u_mentioned) + end.to have_only_enqueued_mail_with_args( + Notify, + :new_mention_in_issue_email, + [@u_watcher.id, mentionable.id, anything, anything], + [@u_participant_mentioned.id, mentionable.id, anything, anything], + [@u_custom_global.id, mentionable.id, anything, anything], + [@u_mentioned.id, mentionable.id, anything, anything] + ) + end + + it 'does not email new mentions with a watch level equal to or less than mention' do + send_notifications(@u_disabled) + + expect_no_delivery_jobs + end + + it 'emails new mentions despite being unsubscribed' do + expect do + send_notifications(@unsubscribed_mentioned) + end.to have_only_enqueued_mail_with_args( + Notify, + :new_mention_in_issue_email, + [@unsubscribed_mentioned.id, mentionable.id, anything, anything] + ) + end + + it 'sends the proper notification reason header' do + expect do + send_notifications(@u_watcher) + end.to have_only_enqueued_mail_with_args( + Notify, + :new_mention_in_issue_email, + [@u_watcher.id, mentionable.id, anything, NotificationReason::MENTIONED] + ) + end + + it_behaves_like 'project emails are disabled', check_delivery_jobs_queue: true do let(:notification_target) { issue } let(:notification_trigger) { send_notifications(@u_watcher, @u_participant_mentioned, @u_custom_global, @u_mentioned) } end @@ -1364,117 +1433,130 @@ RSpec.describe NotificationService, :mailer, feature_category: :team_planning do context 'where current_user is blocked' do let(:current_user) { create(:user, :blocked) } - include_examples 'is not able to send notifications' + include_examples 'is not able to send notifications', check_delivery_jobs_queue: true end context 'where current_user is a ghost' do let(:current_user) { create(:user, :ghost) } - include_examples 'is not able to send notifications' + include_examples 'is not able to send notifications', check_delivery_jobs_queue: true end end describe '#reassigned_issue' do + let(:anything_args) { [anything, anything, anything, anything] } + let(:mailer_method) { :reassigned_issue_email } + before do update_custom_notification(:reassign_issue, @u_guest_custom, resource: project) update_custom_notification(:reassign_issue, @u_custom_global) end it 'emails new assignee' do - notification.reassigned_issue(issue, @u_disabled, [assignee]) - - should_email(issue.assignees.first) - should_email(@u_watcher) - should_email(@u_guest_watcher) - should_email(@u_guest_custom) - should_email(@u_custom_global) - should_email(@u_participant_mentioned) - should_email(@subscriber) - should_not_email(@unsubscriber) - should_not_email(@u_participating) - should_not_email(@u_disabled) - should_not_email(@u_lazy_participant) + expect do + notification.reassigned_issue(issue, @u_disabled, [assignee]) + end.to enqueue_mail_with(Notify, :reassigned_issue_email, issue.assignees.first, *anything_args) + .and(enqueue_mail_with(Notify, :reassigned_issue_email, @u_watcher, *anything_args)) + .and(enqueue_mail_with(Notify, :reassigned_issue_email, @u_guest_watcher, *anything_args)) + .and(enqueue_mail_with(Notify, :reassigned_issue_email, @u_guest_custom, *anything_args)) + .and(enqueue_mail_with(Notify, :reassigned_issue_email, @u_custom_global, *anything_args)) + .and(enqueue_mail_with(Notify, :reassigned_issue_email, @u_participant_mentioned, *anything_args)) + .and(enqueue_mail_with(Notify, :reassigned_issue_email, @subscriber, *anything_args)) + .and(not_enqueue_mail_with(Notify, :reassigned_issue_email, @unsubscriber, *anything_args)) + .and(not_enqueue_mail_with(Notify, :reassigned_issue_email, @u_participating, *anything_args)) + .and(not_enqueue_mail_with(Notify, :reassigned_issue_email, @u_disabled, *anything_args)) + .and(not_enqueue_mail_with(Notify, :reassigned_issue_email, @u_lazy_participant, *anything_args)) end it 'adds "assigned" reason for new assignee' do - notification.reassigned_issue(issue, @u_disabled, [assignee]) - - email = find_email_for(assignee) - - expect(email).to have_header('X-GitLab-NotificationReason', NotificationReason::ASSIGNED) + expect do + notification.reassigned_issue(issue, @u_disabled, [assignee]) + end.to enqueue_mail_with( + Notify, + :reassigned_issue_email, + issue.assignees.first, + anything, + anything, + anything, + NotificationReason::ASSIGNED + ) end it 'emails previous assignee even if they have the "on mention" notif level' do issue.assignees = [@u_mentioned] - notification.reassigned_issue(issue, @u_disabled, [@u_watcher]) - should_email(@u_mentioned) - should_email(@u_watcher) - should_email(@u_guest_watcher) - should_email(@u_guest_custom) - should_email(@u_participant_mentioned) - should_email(@subscriber) - should_email(@u_custom_global) - should_not_email(@unsubscriber) - should_not_email(@u_participating) - should_not_email(@u_disabled) - should_not_email(@u_lazy_participant) + expect do + notification.reassigned_issue(issue, @u_disabled, [@u_watcher]) + end.to enqueue_mail_with(Notify, :reassigned_issue_email, @u_mentioned, *anything_args) + .and(enqueue_mail_with(Notify, :reassigned_issue_email, @u_watcher, *anything_args)) + .and(enqueue_mail_with(Notify, :reassigned_issue_email, @u_guest_watcher, *anything_args)) + .and(enqueue_mail_with(Notify, :reassigned_issue_email, @u_guest_custom, *anything_args)) + .and(enqueue_mail_with(Notify, :reassigned_issue_email, @u_participant_mentioned, *anything_args)) + .and(enqueue_mail_with(Notify, :reassigned_issue_email, @subscriber, *anything_args)) + .and(enqueue_mail_with(Notify, :reassigned_issue_email, @u_custom_global, *anything_args)) + .and(not_enqueue_mail_with(Notify, :reassigned_issue_email, @unsubscriber, *anything_args)) + .and(not_enqueue_mail_with(Notify, :reassigned_issue_email, @u_participating, *anything_args)) + .and(not_enqueue_mail_with(Notify, :reassigned_issue_email, @u_disabled, *anything_args)) + .and(not_enqueue_mail_with(Notify, :reassigned_issue_email, @u_lazy_participant, *anything_args)) end it 'emails new assignee even if they have the "on mention" notif level' do issue.assignees = [@u_mentioned] - notification.reassigned_issue(issue, @u_disabled, [@u_mentioned]) - expect(issue.assignees.first).to be @u_mentioned - should_email(issue.assignees.first) - should_email(@u_watcher) - should_email(@u_guest_watcher) - should_email(@u_guest_custom) - should_email(@u_participant_mentioned) - should_email(@subscriber) - should_email(@u_custom_global) - should_not_email(@unsubscriber) - should_not_email(@u_participating) - should_not_email(@u_disabled) - should_not_email(@u_lazy_participant) + expect(issue.assignees.first).to eq(@u_mentioned) + expect do + notification.reassigned_issue(issue, @u_disabled, [@u_mentioned]) + end.to enqueue_mail_with(Notify, :reassigned_issue_email, issue.assignees.first, *anything_args) + .and(enqueue_mail_with(Notify, :reassigned_issue_email, @u_watcher, *anything_args)) + .and(enqueue_mail_with(Notify, :reassigned_issue_email, @u_guest_watcher, *anything_args)) + .and(enqueue_mail_with(Notify, :reassigned_issue_email, @u_guest_custom, *anything_args)) + .and(enqueue_mail_with(Notify, :reassigned_issue_email, @u_participant_mentioned, *anything_args)) + .and(enqueue_mail_with(Notify, :reassigned_issue_email, @subscriber, *anything_args)) + .and(enqueue_mail_with(Notify, :reassigned_issue_email, @u_custom_global, *anything_args)) + .and(not_enqueue_mail_with(Notify, :reassigned_issue_email, @unsubscriber, *anything_args)) + .and(not_enqueue_mail_with(Notify, :reassigned_issue_email, @u_participating, *anything_args)) + .and(not_enqueue_mail_with(Notify, :reassigned_issue_email, @u_disabled, *anything_args)) + .and(not_enqueue_mail_with(Notify, :reassigned_issue_email, @u_lazy_participant, *anything_args)) end it 'does not email new assignee if they are the current user' do issue.assignees = [@u_mentioned] notification.reassigned_issue(issue, @u_mentioned, [@u_mentioned]) - expect(issue.assignees.first).to be @u_mentioned - should_email(@u_watcher) - should_email(@u_guest_watcher) - should_email(@u_guest_custom) - should_email(@u_participant_mentioned) - should_email(@subscriber) - should_email(@u_custom_global) - should_not_email(issue.assignees.first) - should_not_email(@unsubscriber) - should_not_email(@u_participating) - should_not_email(@u_disabled) - should_not_email(@u_lazy_participant) - end - - it_behaves_like 'participating notifications' do + expect(issue.assignees.first).to eq(@u_mentioned) + expect do + notification.reassigned_issue(issue, @u_mentioned, [@u_mentioned]) + end.to enqueue_mail_with(Notify, :reassigned_issue_email, @u_watcher, *anything_args) + .and(enqueue_mail_with(Notify, :reassigned_issue_email, @u_guest_watcher, *anything_args)) + .and(enqueue_mail_with(Notify, :reassigned_issue_email, @u_guest_custom, *anything_args)) + .and(enqueue_mail_with(Notify, :reassigned_issue_email, @u_participant_mentioned, *anything_args)) + .and(enqueue_mail_with(Notify, :reassigned_issue_email, @subscriber, *anything_args)) + .and(enqueue_mail_with(Notify, :reassigned_issue_email, @u_custom_global, *anything_args)) + .and(not_enqueue_mail_with(Notify, :reassigned_issue_email, issue.assignees.first, *anything_args)) + .and(not_enqueue_mail_with(Notify, :reassigned_issue_email, @unsubscriber, *anything_args)) + .and(not_enqueue_mail_with(Notify, :reassigned_issue_email, @u_participating, *anything_args)) + .and(not_enqueue_mail_with(Notify, :reassigned_issue_email, @u_disabled, *anything_args)) + .and(not_enqueue_mail_with(Notify, :reassigned_issue_email, @u_lazy_participant, *anything_args)) + end + + it_behaves_like 'participating notifications', check_delivery_jobs_queue: true do let(:participant) { create(:user, username: 'user-participant') } let(:issuable) { issue } let(:notification_trigger) { notification.reassigned_issue(issue, @u_disabled, [assignee]) } end - it_behaves_like 'participating by confidential note notification' do + it_behaves_like 'participating by confidential note notification', check_delivery_jobs_queue: true do let(:issuable) { issue } let(:notification_trigger) { notification.reassigned_issue(issue, @u_disabled, [assignee]) } end - it_behaves_like 'project emails are disabled' do + it_behaves_like 'project emails are disabled', check_delivery_jobs_queue: true do let(:notification_target) { issue } let(:notification_trigger) { notification.reassigned_issue(issue, @u_disabled, [assignee]) } end end - describe '#relabeled_issue' do + describe '#relabeled_issue', :deliver_mails_inline do let(:group_label_1) { create(:group_label, group: group, title: 'Group Label 1', issues: [issue]) } let(:group_label_2) { create(:group_label, group: group, title: 'Group Label 2') } let(:label_1) { create(:label, project: project, title: 'Label 1', issues: [issue]) } @@ -1571,25 +1653,25 @@ RSpec.describe NotificationService, :mailer, feature_category: :team_planning do end end - describe '#removed_milestone_issue' do + describe '#removed_milestone on Issue', :deliver_mails_inline do context do let(:milestone) { create(:milestone, project: project, issues: [issue]) } let!(:subscriber_to_new_milestone) { create(:user) { |u| issue.toggle_subscription(u, project) } } it_behaves_like 'altered milestone notification on issue' do before do - notification.removed_milestone_issue(issue, issue.author) + notification.removed_milestone(issue, issue.author) end end it_behaves_like 'project emails are disabled' do let(:notification_target) { issue } - let(:notification_trigger) { notification.removed_milestone_issue(issue, issue.author) } + let(:notification_trigger) { notification.removed_milestone(issue, issue.author) } end it_behaves_like 'participating by confidential note notification' do let(:issuable) { issue } - let(:notification_trigger) { notification.removed_milestone_issue(issue, issue.author) } + let(:notification_trigger) { notification.removed_milestone(issue, issue.author) } end end @@ -1615,7 +1697,7 @@ RSpec.describe NotificationService, :mailer, feature_category: :team_planning do reset_delivered_emails! - notification.removed_milestone_issue(confidential_issue, @u_disabled) + notification.removed_milestone(confidential_issue, @u_disabled) should_not_email(non_member) should_not_email(guest) @@ -1627,20 +1709,20 @@ RSpec.describe NotificationService, :mailer, feature_category: :team_planning do end end - describe '#changed_milestone_issue' do + describe '#changed_milestone on Issue', :deliver_mails_inline do context do let(:new_milestone) { create(:milestone, project: project, issues: [issue]) } let!(:subscriber_to_new_milestone) { create(:user) { |u| issue.toggle_subscription(u, project) } } it_behaves_like 'altered milestone notification on issue' do before do - notification.changed_milestone_issue(issue, new_milestone, issue.author) + notification.changed_milestone(issue, new_milestone, issue.author) end end it_behaves_like 'project emails are disabled' do let(:notification_target) { issue } - let(:notification_trigger) { notification.changed_milestone_issue(issue, new_milestone, issue.author) } + let(:notification_trigger) { notification.changed_milestone(issue, new_milestone, issue.author) } end end @@ -1666,7 +1748,7 @@ RSpec.describe NotificationService, :mailer, feature_category: :team_planning do reset_delivered_emails! - notification.changed_milestone_issue(confidential_issue, new_milestone, @u_disabled) + notification.changed_milestone(confidential_issue, new_milestone, @u_disabled) should_not_email(non_member) should_not_email(guest) @@ -1678,7 +1760,7 @@ RSpec.describe NotificationService, :mailer, feature_category: :team_planning do end end - describe '#close_issue' do + describe '#close_issue', :deliver_mails_inline do before do update_custom_notification(:close_issue, @u_guest_custom, resource: project) update_custom_notification(:close_issue, @u_custom_global) @@ -1730,7 +1812,7 @@ RSpec.describe NotificationService, :mailer, feature_category: :team_planning do end end - describe '#reopen_issue' do + describe '#reopen_issue', :deliver_mails_inline do before do update_custom_notification(:reopen_issue, @u_guest_custom, resource: project) update_custom_notification(:reopen_issue, @u_custom_global) @@ -1771,7 +1853,7 @@ RSpec.describe NotificationService, :mailer, feature_category: :team_planning do end end - describe '#issue_moved' do + describe '#issue_moved', :deliver_mails_inline do let(:new_issue) { create(:issue) } it 'sends email to issue notification recipients' do @@ -1807,7 +1889,7 @@ RSpec.describe NotificationService, :mailer, feature_category: :team_planning do end end - describe '#issue_cloned' do + describe '#issue_cloned', :deliver_mails_inline do let(:new_issue) { create(:issue) } it 'sends email to issue notification recipients' do @@ -1843,7 +1925,7 @@ RSpec.describe NotificationService, :mailer, feature_category: :team_planning do end end - describe '#issue_due' do + describe '#issue_due', :deliver_mails_inline do before do issue.update!(due_date: Date.today) @@ -2395,35 +2477,35 @@ RSpec.describe NotificationService, :mailer, feature_category: :team_planning do end end - describe '#removed_milestone_merge_request' do + describe '#removed_milestone on MergeRequest' do let(:milestone) { create(:milestone, project: project, merge_requests: [merge_request]) } let!(:subscriber_to_new_milestone) { create(:user) { |u| merge_request.toggle_subscription(u, project) } } it_behaves_like 'altered milestone notification on merge request' do before do - notification.removed_milestone_merge_request(merge_request, merge_request.author) + notification.removed_milestone(merge_request, merge_request.author) end end it_behaves_like 'project emails are disabled' do let(:notification_target) { merge_request } - let(:notification_trigger) { notification.removed_milestone_merge_request(merge_request, merge_request.author) } + let(:notification_trigger) { notification.removed_milestone(merge_request, merge_request.author) } end end - describe '#changed_milestone_merge_request' do + describe '#changed_milestone on MergeRequest' do let(:new_milestone) { create(:milestone, project: project, merge_requests: [merge_request]) } let!(:subscriber_to_new_milestone) { create(:user) { |u| merge_request.toggle_subscription(u, project) } } it_behaves_like 'altered milestone notification on merge request' do before do - notification.changed_milestone_merge_request(merge_request, new_milestone, merge_request.author) + notification.changed_milestone(merge_request, new_milestone, merge_request.author) end end it_behaves_like 'project emails are disabled' do let(:notification_target) { merge_request } - let(:notification_trigger) { notification.changed_milestone_merge_request(merge_request, new_milestone, merge_request.author) } + let(:notification_trigger) { notification.changed_milestone(merge_request, new_milestone, merge_request.author) } end end @@ -3920,4 +4002,8 @@ RSpec.describe NotificationService, :mailer, feature_category: :team_planning do # Make the watcher a subscriber to detect dupes issuable.subscriptions.create!(user: @watcher_and_subscriber, project: project, subscribed: true) end + + def expectation_args_for_user(user) + [user, *anything_args] + end end diff --git a/spec/services/packages/debian/create_distribution_service_spec.rb b/spec/services/packages/debian/create_distribution_service_spec.rb index ecf82c6a1db..1c53f75cfb6 100644 --- a/spec/services/packages/debian/create_distribution_service_spec.rb +++ b/spec/services/packages/debian/create_distribution_service_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Packages::Debian::CreateDistributionService do +RSpec.describe Packages::Debian::CreateDistributionService, feature_category: :package_registry do RSpec.shared_examples 'Create Debian Distribution' do |expected_message, expected_components, expected_architectures| let_it_be(:container) { create(container_type) } # rubocop:disable Rails/SaveBang diff --git a/spec/services/packages/debian/create_package_file_service_spec.rb b/spec/services/packages/debian/create_package_file_service_spec.rb index 291f6df991c..43928669eb1 100644 --- a/spec/services/packages/debian/create_package_file_service_spec.rb +++ b/spec/services/packages/debian/create_package_file_service_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Packages::Debian::CreatePackageFileService do +RSpec.describe Packages::Debian::CreatePackageFileService, feature_category: :package_registry do include WorkhorseHelpers let_it_be(:package) { create(:debian_incoming, without_package_files: true) } @@ -11,8 +11,9 @@ RSpec.describe Packages::Debian::CreatePackageFileService do describe '#execute' do let(:file_name) { 'libsample0_1.2.3~alpha2_amd64.deb' } let(:fixture_path) { "spec/fixtures/packages/debian/#{file_name}" } + let(:params) { default_params } - let(:params) do + let(:default_params) do { file: file, file_name: file_name, @@ -25,8 +26,15 @@ RSpec.describe Packages::Debian::CreatePackageFileService do subject(:package_file) { service.execute } - shared_examples 'a valid deb' do + shared_examples 'a valid deb' do |process_package_file_worker| it 'creates a new package file', :aggregate_failures do + if process_package_file_worker + expect(::Packages::Debian::ProcessPackageFileWorker) + .to receive(:perform_async).with(an_instance_of(Integer), params[:distribution], params[:component]) + else + expect(::Packages::Debian::ProcessPackageFileWorker).not_to receive(:perform_async) + end + expect(::Packages::Debian::ProcessChangesWorker).not_to receive(:perform_async) expect(package_file).to be_valid expect(package_file.file.read).to start_with('!<arch>') @@ -44,7 +52,8 @@ RSpec.describe Packages::Debian::CreatePackageFileService do shared_examples 'a valid changes' do it 'creates a new package file', :aggregate_failures do - expect(::Packages::Debian::ProcessChangesWorker).to receive(:perform_async) + expect(::Packages::Debian::ProcessChangesWorker) + .to receive(:perform_async).with(an_instance_of(Integer), current_user.id) expect(package_file).to be_valid expect(package_file.file.read).to start_with('Format: 1.8') @@ -80,6 +89,12 @@ RSpec.describe Packages::Debian::CreatePackageFileService do it_behaves_like 'a valid changes' end + context 'with distribution' do + let(:params) { default_params.merge(distribution: 'unstable', component: 'main') } + + it_behaves_like 'a valid deb', true + end + context 'when current_user is missing' do let(:current_user) { nil } @@ -137,13 +152,5 @@ RSpec.describe Packages::Debian::CreatePackageFileService do expect { package_file }.to raise_error(ActiveRecord::RecordInvalid) end end - - context 'when FIPS mode enabled', :fips_mode do - let(:file) { nil } - - it 'raises an error' do - expect { package_file }.to raise_error(::Packages::FIPS::DisabledError) - end - end end end diff --git a/spec/services/packages/debian/extract_changes_metadata_service_spec.rb b/spec/services/packages/debian/extract_changes_metadata_service_spec.rb index 4765e6c3bd4..4d6acac219b 100644 --- a/spec/services/packages/debian/extract_changes_metadata_service_spec.rb +++ b/spec/services/packages/debian/extract_changes_metadata_service_spec.rb @@ -1,10 +1,9 @@ # frozen_string_literal: true require 'spec_helper' -RSpec.describe Packages::Debian::ExtractChangesMetadataService do +RSpec.describe Packages::Debian::ExtractChangesMetadataService, feature_category: :package_registry do describe '#execute' do - let_it_be(:distribution) { create(:debian_project_distribution, codename: 'unstable') } - let_it_be(:incoming) { create(:debian_incoming, project: distribution.project) } + let_it_be(:incoming) { create(:debian_incoming) } let(:source_file) { incoming.package_files.first } let(:dsc_file) { incoming.package_files.second } @@ -13,12 +12,6 @@ RSpec.describe Packages::Debian::ExtractChangesMetadataService do subject { service.execute } - context 'with FIPS mode enabled', :fips_mode do - it 'raises an error' do - expect { subject }.to raise_error(::Packages::FIPS::DisabledError) - end - end - context 'with valid package file' do it 'extract metadata', :aggregate_failures do expected_fields = { 'Architecture' => 'source amd64', 'Binary' => 'libsample0 sample-dev sample-udeb' } 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 66a9ca5f9e0..1f5cf2ace5a 100644 --- a/spec/services/packages/debian/extract_deb_metadata_service_spec.rb +++ b/spec/services/packages/debian/extract_deb_metadata_service_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true require 'spec_helper' -RSpec.describe Packages::Debian::ExtractDebMetadataService do +RSpec.describe Packages::Debian::ExtractDebMetadataService, feature_category: :package_registry do subject { described_class.new(file_path) } let(:file_name) { 'libsample0_1.2.3~alpha2_amd64.deb' } diff --git a/spec/services/packages/debian/extract_metadata_service_spec.rb b/spec/services/packages/debian/extract_metadata_service_spec.rb index 02c81ad1644..412f285152b 100644 --- a/spec/services/packages/debian/extract_metadata_service_spec.rb +++ b/spec/services/packages/debian/extract_metadata_service_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true require 'spec_helper' -RSpec.describe Packages::Debian::ExtractMetadataService do +RSpec.describe Packages::Debian::ExtractMetadataService, feature_category: :package_registry do let(:service) { described_class.new(package_file) } subject { service.execute } diff --git a/spec/services/packages/debian/find_or_create_incoming_service_spec.rb b/spec/services/packages/debian/find_or_create_incoming_service_spec.rb index e1393c774b1..27c389b5312 100644 --- a/spec/services/packages/debian/find_or_create_incoming_service_spec.rb +++ b/spec/services/packages/debian/find_or_create_incoming_service_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Packages::Debian::FindOrCreateIncomingService do +RSpec.describe Packages::Debian::FindOrCreateIncomingService, feature_category: :package_registry do let_it_be(:project) { create(:project) } let_it_be(:user) { create(:user) } diff --git a/spec/services/packages/debian/find_or_create_package_service_spec.rb b/spec/services/packages/debian/find_or_create_package_service_spec.rb index 84a0e1465e8..36f96008582 100644 --- a/spec/services/packages/debian/find_or_create_package_service_spec.rb +++ b/spec/services/packages/debian/find_or_create_package_service_spec.rb @@ -2,54 +2,57 @@ require 'spec_helper' -RSpec.describe Packages::Debian::FindOrCreatePackageService do - let_it_be(:distribution) { create(:debian_project_distribution) } +RSpec.describe Packages::Debian::FindOrCreatePackageService, feature_category: :package_registry do + let_it_be(:distribution) { create(:debian_project_distribution, :with_suite) } let_it_be(:project) { distribution.project } let_it_be(:user) { create(:user) } - let(:params) { { name: 'foo', version: '1.0+debian', distribution_name: distribution.codename } } + let(:service) { described_class.new(project, user, params) } - subject(:service) { described_class.new(project, user, params) } + let(:package) { subject.payload[:package] } + let(:package2) { service.execute.payload[:package] } - describe '#execute' do - subject { service.execute } + shared_examples 'find or create Debian package' do + it 'returns the same object' do + expect { subject }.to change { ::Packages::Package.count }.by(1) + expect(subject).to be_success + expect(package).to be_valid + expect(package.project_id).to eq(project.id) + expect(package.creator_id).to eq(user.id) + expect(package.name).to eq('foo') + expect(package.version).to eq('1.0+debian') + expect(package).to be_debian + expect(package.debian_publication.distribution).to eq(distribution) - let(:package) { subject.payload[:package] } + expect { package2 }.not_to change { ::Packages::Package.count } + expect(package2.id).to eq(package.id) + end - context 'run once' do - it 'creates a new package', :aggregate_failures do + context 'with package marked as pending_destruction' do + it 'creates a new package' do expect { subject }.to change { ::Packages::Package.count }.by(1) - expect(subject).to be_success - - expect(package).to be_valid - expect(package.project_id).to eq(project.id) - expect(package.creator_id).to eq(user.id) - expect(package.name).to eq('foo') - expect(package.version).to eq('1.0+debian') - expect(package).to be_debian - expect(package.debian_publication.distribution).to eq(distribution) + + package.pending_destruction! + + expect { package2 }.to change { ::Packages::Package.count }.by(1) + expect(package2.id).not_to eq(package.id) end end + end - context 'run twice' do - let(:package2) { service.execute.payload[:package] } + describe '#execute' do + subject { service.execute } - it 'returns the same object' do - expect { subject }.to change { ::Packages::Package.count }.by(1) - expect { package2 }.not_to change { ::Packages::Package.count } + context 'with a codename as distribution name' do + let(:params) { { name: 'foo', version: '1.0+debian', distribution_name: distribution.codename } } - expect(package2.id).to eq(package.id) - end + it_behaves_like 'find or create Debian package' + end - context 'with package marked as pending_destruction' do - it 'creates a new package' do - expect { subject }.to change { ::Packages::Package.count }.by(1) - package.pending_destruction! - expect { package2 }.to change { ::Packages::Package.count }.by(1) + context 'with a suite as distribution name' do + let(:params) { { name: 'foo', version: '1.0+debian', distribution_name: distribution.suite } } - expect(package2.id).not_to eq(package.id) - end - end + it_behaves_like 'find or create Debian package' end context 'with non-existing distribution' do diff --git a/spec/services/packages/debian/generate_distribution_key_service_spec.rb b/spec/services/packages/debian/generate_distribution_key_service_spec.rb index f82d577f071..bc86a9592d0 100644 --- a/spec/services/packages/debian/generate_distribution_key_service_spec.rb +++ b/spec/services/packages/debian/generate_distribution_key_service_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Packages::Debian::GenerateDistributionKeyService do +RSpec.describe Packages::Debian::GenerateDistributionKeyService, feature_category: :package_registry do let(:params) { {} } subject { described_class.new(params: params) } diff --git a/spec/services/packages/debian/generate_distribution_service_spec.rb b/spec/services/packages/debian/generate_distribution_service_spec.rb index fe5fbfbbe1f..6d179c791a3 100644 --- a/spec/services/packages/debian/generate_distribution_service_spec.rb +++ b/spec/services/packages/debian/generate_distribution_service_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Packages::Debian::GenerateDistributionService do +RSpec.describe Packages::Debian::GenerateDistributionService, feature_category: :package_registry do describe '#execute' do subject { described_class.new(distribution).execute } @@ -15,12 +15,6 @@ RSpec.describe Packages::Debian::GenerateDistributionService do context "for #{container_type}" do include_context 'with Debian distribution', container_type - context 'with FIPS mode enabled', :fips_mode do - it 'raises an error' do - expect { subject }.to raise_error(::Packages::FIPS::DisabledError) - end - end - it_behaves_like 'Generate Debian Distribution and component files' end end diff --git a/spec/services/packages/debian/parse_debian822_service_spec.rb b/spec/services/packages/debian/parse_debian822_service_spec.rb index a2731816459..35b7ead9209 100644 --- a/spec/services/packages/debian/parse_debian822_service_spec.rb +++ b/spec/services/packages/debian/parse_debian822_service_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true require 'spec_helper' -RSpec.describe Packages::Debian::ParseDebian822Service do +RSpec.describe Packages::Debian::ParseDebian822Service, feature_category: :package_registry do subject { described_class.new(input) } context 'with dpkg-deb --field output' do diff --git a/spec/services/packages/debian/process_changes_service_spec.rb b/spec/services/packages/debian/process_changes_service_spec.rb index 27b49a13d52..e3ed744377e 100644 --- a/spec/services/packages/debian/process_changes_service_spec.rb +++ b/spec/services/packages/debian/process_changes_service_spec.rb @@ -1,14 +1,14 @@ # frozen_string_literal: true require 'spec_helper' -RSpec.describe Packages::Debian::ProcessChangesService do +RSpec.describe Packages::Debian::ProcessChangesService, feature_category: :package_registry do describe '#execute' do let_it_be(:user) { create(:user) } - let_it_be_with_reload(:distribution) { create(:debian_project_distribution, :with_file, codename: 'unstable') } + let_it_be_with_reload(:distribution) { create(:debian_project_distribution, :with_file, suite: 'unstable') } let!(:incoming) { create(:debian_incoming, project: distribution.project) } - let(:package_file) { incoming.package_files.last } + let(:package_file) { incoming.package_files.with_file_name('sample_1.2.3~alpha2_amd64.changes').first } subject { described_class.new(package_file, user) } @@ -27,11 +27,37 @@ RSpec.describe Packages::Debian::ProcessChangesService do expect(created_package.creator).to eq user end - context 'with existing package' do - let_it_be_with_reload(:existing_package) { create(:debian_package, name: 'sample', version: '1.2.3~alpha2', project: distribution.project) } - + context 'with non-matching distribution' do before do - existing_package.update!(debian_distribution: distribution) + distribution.update! suite: FFaker::Lorem.word + end + + it { expect { subject.execute }.to raise_error(ActiveRecord::RecordNotFound) } + end + + context 'with missing field in .changes file' do + shared_examples 'raises error with missing field' do |missing_field| + before do + allow_next_instance_of(::Packages::Debian::ExtractChangesMetadataService) do |extract_changes_metadata_service| + expect(extract_changes_metadata_service).to receive(:execute).once.and_wrap_original do |m, *args| + metadata = m.call(*args) + metadata[:fields].delete(missing_field) + metadata + end + end + end + + it { expect { subject.execute }.to raise_error(ArgumentError, "missing #{missing_field} field") } + end + + it_behaves_like 'raises error with missing field', 'Source' + it_behaves_like 'raises error with missing field', 'Version' + it_behaves_like 'raises error with missing field', 'Distribution' + end + + context 'with existing package' do + let_it_be_with_reload(:existing_package) do + create(:debian_package, name: 'sample', version: '1.2.3~alpha2', project: distribution.project, published_in: distribution) end it 'does not create a package and assigns the package_file to the existing package' do diff --git a/spec/services/packages/debian/process_package_file_service_spec.rb b/spec/services/packages/debian/process_package_file_service_spec.rb index 571861f42cf..caf29cfc4fa 100644 --- a/spec/services/packages/debian/process_package_file_service_spec.rb +++ b/spec/services/packages/debian/process_package_file_service_spec.rb @@ -1,20 +1,33 @@ # frozen_string_literal: true require 'spec_helper' -RSpec.describe Packages::Debian::ProcessPackageFileService do +RSpec.describe Packages::Debian::ProcessPackageFileService, feature_category: :package_registry do describe '#execute' do - let_it_be(:user) { create(:user) } - let_it_be_with_reload(:distribution) { create(:debian_project_distribution, :with_file, codename: 'unstable') } - - let!(:incoming) { create(:debian_incoming, project: distribution.project) } + let_it_be_with_reload(:distribution) { create(:debian_project_distribution, :with_suite, :with_file) } + let!(:package) { create(:debian_package, :processing, project: distribution.project, published_in: nil) } let(:distribution_name) { distribution.codename } + let(:component_name) { 'main' } let(:debian_file_metadatum) { package_file.debian_file_metadatum } - subject { described_class.new(package_file, user, distribution_name, component_name) } + subject { described_class.new(package_file, distribution_name, component_name) } - RSpec.shared_context 'with Debian package file' do |file_name| - let(:package_file) { incoming.package_files.with_file_name(file_name).first } + shared_examples 'updates package and package file' do + it 'updates package and package file', :aggregate_failures do + expect(::Packages::Debian::GenerateDistributionWorker) + .to receive(:perform_async).with(:project, distribution.id) + expect { subject.execute } + .to not_change(Packages::Package, :count) + .and not_change(Packages::PackageFile, :count) + .and change(Packages::Debian::Publication, :count).by(1) + .and not_change(package.package_files, :count) + .and change { package.reload.name }.to('sample') + .and change { package.reload.version }.to('1.2.3~alpha2') + .and change { package.reload.status }.from('processing').to('default') + .and change { package.reload.debian_publication }.from(nil) + .and change(debian_file_metadatum, :file_type).from('unknown').to(expected_file_type) + .and change(debian_file_metadatum, :component).from(nil).to(component_name) + end end using RSpec::Parameterized::TableSyntax @@ -25,59 +38,68 @@ RSpec.describe Packages::Debian::ProcessPackageFileService do end with_them do - include_context 'with Debian package file', params[:file_name] do - it 'creates package and updates package file', :aggregate_failures do - expect(::Packages::Debian::GenerateDistributionWorker) - .to receive(:perform_async).with(:project, distribution.id) - expect { subject.execute } - .to change(Packages::Package, :count).from(1).to(2) - .and not_change(Packages::PackageFile, :count) - .and change(incoming.package_files, :count).from(7).to(6) - .and change(debian_file_metadatum, :file_type).from('unknown').to(expected_file_type) - .and change(debian_file_metadatum, :component).from(nil).to(component_name) - - created_package = Packages::Package.last - expect(created_package.name).to eq 'sample' - expect(created_package.version).to eq '1.2.3~alpha2' - expect(created_package.creator).to eq user - end + context 'with Debian package file' do + let(:package_file) { package.package_files.with_file_name(file_name).first } - context 'with existing package' do - let_it_be_with_reload(:existing_package) do - create(:debian_package, name: 'sample', version: '1.2.3~alpha2', project: distribution.project) + context 'when there is no matching published package' do + it_behaves_like 'updates package and package file' + + context 'with suite as distribution name' do + let(:distribution_name) { distribution.suite } + + it_behaves_like 'updates package and package file' end + end - before do - existing_package.update!(debian_distribution: distribution) + context 'when there is a matching published package' do + let!(:matching_package) do + create( + :debian_package, + project: distribution.project, + published_in: distribution, + name: 'sample', + version: '1.2.3~alpha2' + ) end - it 'does not create a package and assigns the package_file to the existing package' do + it 'reuses existing package and update package file', :aggregate_failures do expect(::Packages::Debian::GenerateDistributionWorker) .to receive(:perform_async).with(:project, distribution.id) expect { subject.execute } - .to not_change(Packages::Package, :count) - .and not_change(Packages::PackageFile, :count) - .and change(incoming.package_files, :count).from(7).to(6) - .and change(package_file, :package).from(incoming).to(existing_package) - .and change(debian_file_metadatum, :file_type).from('unknown').to(expected_file_type.to_s) + .to change(Packages::Package, :count).from(2).to(1) + .and change(Packages::PackageFile, :count).from(14).to(8) + .and not_change(Packages::Debian::Publication, :count) + .and change(package.package_files, :count).from(7).to(0) + .and change(package_file, :package).from(package).to(matching_package) + .and not_change(matching_package, :name) + .and not_change(matching_package, :version) + .and change(debian_file_metadatum, :file_type).from('unknown').to(expected_file_type) .and change(debian_file_metadatum, :component).from(nil).to(component_name) - end - context 'when marked as pending_destruction' do - it 'does not re-use the existing package' do - existing_package.pending_destruction! + expect { package.reload } + .to raise_error(ActiveRecord::RecordNotFound) + end + end - expect { subject.execute } - .to change(Packages::Package, :count).by(1) - .and not_change(Packages::PackageFile, :count) - end + context 'when there is a matching published package pending destruction' do + let!(:matching_package) do + create( + :debian_package, + :pending_destruction, + project: distribution.project, + published_in: distribution, + name: 'sample', + version: '1.2.3~alpha2' + ) end + + it_behaves_like 'updates package and package file' end end end context 'without a distribution' do - let(:package_file) { incoming.package_files.with_file_name('libsample0_1.2.3~alpha2_amd64.deb').first } + let(:package_file) { package.package_files.with_file_name('libsample0_1.2.3~alpha2_amd64.deb').first } let(:component_name) { 'main' } before do @@ -89,42 +111,41 @@ RSpec.describe Packages::Debian::ProcessPackageFileService do expect { subject.execute } .to not_change(Packages::Package, :count) .and not_change(Packages::PackageFile, :count) - .and not_change(incoming.package_files, :count) + .and not_change(package.package_files, :count) .and raise_error(ActiveRecord::RecordNotFound) end end - context 'with package file without Debian metadata' do + context 'without distribution name' do let!(:package_file) { create(:debian_package_file, without_loaded_metadatum: true) } - let(:component_name) { 'main' } + let(:distribution_name) { '' } it 'raise ArgumentError', :aggregate_failures do expect(::Packages::Debian::GenerateDistributionWorker).not_to receive(:perform_async) expect { subject.execute } .to not_change(Packages::Package, :count) .and not_change(Packages::PackageFile, :count) - .and not_change(incoming.package_files, :count) - .and raise_error(ArgumentError, 'package file without Debian metadata') + .and not_change(package.package_files, :count) + .and raise_error(ArgumentError, 'missing distribution name') end end - context 'with already processed package file' do - let_it_be(:package_file) { create(:debian_package_file) } - - let(:component_name) { 'main' } + context 'without component name' do + let!(:package_file) { create(:debian_package_file, without_loaded_metadatum: true) } + let(:component_name) { '' } it 'raise ArgumentError', :aggregate_failures do expect(::Packages::Debian::GenerateDistributionWorker).not_to receive(:perform_async) expect { subject.execute } .to not_change(Packages::Package, :count) .and not_change(Packages::PackageFile, :count) - .and not_change(incoming.package_files, :count) - .and raise_error(ArgumentError, 'already processed package file') + .and not_change(package.package_files, :count) + .and raise_error(ArgumentError, 'missing component name') end end - context 'with invalid package file type' do - let(:package_file) { incoming.package_files.with_file_name('sample_1.2.3~alpha2.tar.xz').first } + context 'with package file without Debian metadata' do + let!(:package_file) { create(:debian_package_file, without_loaded_metadatum: true) } let(:component_name) { 'main' } it 'raise ArgumentError', :aggregate_failures do @@ -132,29 +153,37 @@ RSpec.describe Packages::Debian::ProcessPackageFileService do expect { subject.execute } .to not_change(Packages::Package, :count) .and not_change(Packages::PackageFile, :count) - .and not_change(incoming.package_files, :count) - .and raise_error(ArgumentError, 'invalid package file type: source') + .and not_change(package.package_files, :count) + .and raise_error(ArgumentError, 'package file without Debian metadata') end end - context 'when creating package fails' do - let(:package_file) { incoming.package_files.with_file_name('libsample0_1.2.3~alpha2_amd64.deb').first } + context 'with already processed package file' do + let_it_be(:package_file) { create(:debian_package_file) } + let(:component_name) { 'main' } - before do - allow_next_instance_of(::Packages::Debian::FindOrCreatePackageService) do |find_or_create_package_service| - allow(find_or_create_package_service) - .to receive(:execute).and_raise(ActiveRecord::ConnectionTimeoutError, 'connect timeout') - end + it 'raise ArgumentError', :aggregate_failures do + expect(::Packages::Debian::GenerateDistributionWorker).not_to receive(:perform_async) + expect { subject.execute } + .to not_change(Packages::Package, :count) + .and not_change(Packages::PackageFile, :count) + .and not_change(package.package_files, :count) + .and raise_error(ArgumentError, 'already processed package file') end + end - it 're-raise error', :aggregate_failures do + context 'with invalid package file type' do + let(:package_file) { package.package_files.with_file_name('sample_1.2.3~alpha2.tar.xz').first } + let(:component_name) { 'main' } + + it 'raise ArgumentError', :aggregate_failures do expect(::Packages::Debian::GenerateDistributionWorker).not_to receive(:perform_async) expect { subject.execute } .to not_change(Packages::Package, :count) .and not_change(Packages::PackageFile, :count) - .and not_change(incoming.package_files, :count) - .and raise_error(ActiveRecord::ConnectionTimeoutError, 'connect timeout') + .and not_change(package.package_files, :count) + .and raise_error(ArgumentError, 'invalid package file type: source') 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 fc070b6e45e..50c34443495 100644 --- a/spec/services/packages/debian/sign_distribution_service_spec.rb +++ b/spec/services/packages/debian/sign_distribution_service_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Packages::Debian::SignDistributionService do +RSpec.describe Packages::Debian::SignDistributionService, feature_category: :package_registry do let_it_be(:group) { create(:group, :public) } let(:content) { FFaker::Lorem.paragraph } diff --git a/spec/services/packages/debian/update_distribution_service_spec.rb b/spec/services/packages/debian/update_distribution_service_spec.rb index 3dff2754cec..cfafed5841f 100644 --- a/spec/services/packages/debian/update_distribution_service_spec.rb +++ b/spec/services/packages/debian/update_distribution_service_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Packages::Debian::UpdateDistributionService do +RSpec.describe Packages::Debian::UpdateDistributionService, feature_category: :package_registry do RSpec.shared_examples 'Update Debian Distribution' do |expected_message, expected_components, expected_architectures, component_file_delta = 0| it 'returns ServiceResponse', :aggregate_failures do expect(distribution).to receive(:update).with(simple_params).and_call_original if expected_message.nil? diff --git a/spec/services/pages/destroy_deployments_service_spec.rb b/spec/services/pages/destroy_deployments_service_spec.rb index 0f8e8b6573e..0ca8cbbb681 100644 --- a/spec/services/pages/destroy_deployments_service_spec.rb +++ b/spec/services/pages/destroy_deployments_service_spec.rb @@ -2,28 +2,26 @@ require 'spec_helper' -RSpec.describe Pages::DestroyDeploymentsService do - let(:project) { create(:project) } +RSpec.describe Pages::DestroyDeploymentsService, feature_category: :pages do + let_it_be(:project) { create(:project) } let!(:old_deployments) { create_list(:pages_deployment, 2, project: project) } let!(:last_deployment) { create(:pages_deployment, project: project) } let!(:newer_deployment) { create(:pages_deployment, project: project) } let!(:deployment_from_another_project) { create(:pages_deployment) } it 'destroys all deployments of the project' do - expect do - described_class.new(project).execute - end.to change { PagesDeployment.count }.by(-4) + expect { described_class.new(project).execute } + .to change { PagesDeployment.count }.by(-4) - expect(deployment_from_another_project.reload).to be + expect(deployment_from_another_project.reload).to be_persisted end it 'destroy only deployments older than last deployment if it is provided' do - expect do - described_class.new(project, last_deployment.id).execute - end.to change { PagesDeployment.count }.by(-2) + expect { described_class.new(project, last_deployment.id).execute } + .to change { PagesDeployment.count }.by(-2) - expect(last_deployment.reload).to be - expect(newer_deployment.reload).to be - expect(deployment_from_another_project.reload).to be + expect(last_deployment.reload).to be_persisted + expect(newer_deployment.reload).to be_persisted + expect(deployment_from_another_project.reload).to be_persisted end end diff --git a/spec/services/pages/migrate_from_legacy_storage_service_spec.rb b/spec/services/pages/migrate_from_legacy_storage_service_spec.rb index d058324f3bb..4348ce4a271 100644 --- a/spec/services/pages/migrate_from_legacy_storage_service_spec.rb +++ b/spec/services/pages/migrate_from_legacy_storage_service_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Pages::MigrateFromLegacyStorageService do +RSpec.describe Pages::MigrateFromLegacyStorageService, feature_category: :pages do let(:batch_size) { 10 } let(:mark_projects_as_not_deployed) { false } let(:service) { described_class.new(Rails.logger, ignore_invalid_entries: false, mark_projects_as_not_deployed: mark_projects_as_not_deployed) } diff --git a/spec/services/preview_markdown_service_spec.rb b/spec/services/preview_markdown_service_spec.rb index fe1ab6b1d58..d1bc10cfd28 100644 --- a/spec/services/preview_markdown_service_spec.rb +++ b/spec/services/preview_markdown_service_spec.rb @@ -192,4 +192,21 @@ RSpec.describe PreviewMarkdownService do "Sets time estimate to 2y.<br>Assigns #{user.to_reference}." end end + + context 'work item quick action types' do + let(:work_item) { create(:work_item, :task, project: project) } + let(:params) do + { + text: "/title new title", + target_type: 'WorkItem', + target_id: work_item.iid + } + end + + let(:result) { described_class.new(project, user, params).execute } + + it 'renders the quick action preview' do + expect(result[:commands]).to eq "Changes the title to \"new title\"." + end + end end diff --git a/spec/services/projects/container_repository/destroy_service_spec.rb b/spec/services/projects/container_repository/destroy_service_spec.rb index 0ec0aecaa04..fed1d13daa5 100644 --- a/spec/services/projects/container_repository/destroy_service_spec.rb +++ b/spec/services/projects/container_repository/destroy_service_spec.rb @@ -5,86 +5,131 @@ require 'spec_helper' RSpec.describe Projects::ContainerRepository::DestroyService do let_it_be(:user) { create(:user) } let_it_be(:project) { create(:project, :private) } + let_it_be(:params) { {} } - subject { described_class.new(project, user) } + subject { described_class.new(project, user, params) } before do stub_container_registry_config(enabled: true) end - context 'when user does not have access to registry' do - let!(:repository) { create(:container_repository, :root, project: project) } + shared_examples 'returning an error status with message' do |error_message| + it 'returns an error status' do + response = subject.execute(repository) - it 'does not delete a repository' do - expect { subject.execute(repository) }.not_to change { ContainerRepository.count } + expect(response).to include(status: :error, message: error_message) end end - context 'when user has access to registry' do + shared_examples 'executing with permissions' do + let_it_be_with_refind(:repository) { create(:container_repository, :root, project: project) } + before do - project.add_developer(user) + stub_container_registry_tags(repository: :any, tags: %w[latest stable]) end - context 'when root container repository exists' do - let!(:repository) { create(:container_repository, :root, project: project) } + it 'deletes the repository' do + expect_cleanup_tags_service_with(container_repository: repository, return_status: :success) + expect { subject.execute(repository) }.to change { ContainerRepository.count }.by(-1) + end + + it 'sends disable_timeout = true as part of the params as default' do + expect_cleanup_tags_service_with(container_repository: repository, return_status: :success, disable_timeout: true) + expect { subject.execute(repository) }.to change { ContainerRepository.count }.by(-1) + end + + it 'sends disable_timeout = false as part of the params if it is set to false' do + expect_cleanup_tags_service_with(container_repository: repository, return_status: :success, disable_timeout: false) + expect { subject.execute(repository, disable_timeout: false) }.to change { ContainerRepository.count }.by(-1) + end + context 'when deleting the tags fails' do before do - stub_container_registry_tags(repository: :any, tags: %w[latest stable]) + expect_cleanup_tags_service_with(container_repository: repository, return_status: :error) + allow(Gitlab::AppLogger).to receive(:error).and_call_original end - it 'deletes the repository' do - expect_cleanup_tags_service_with(container_repository: repository, return_status: :success) - expect { subject.execute(repository) }.to change { ContainerRepository.count }.by(-1) - end + it 'sets status as deleted_failed' do + subject.execute(repository) - it 'sends disable_timeout = true as part of the params as default' do - expect_cleanup_tags_service_with(container_repository: repository, return_status: :success, disable_timeout: true) - expect { subject.execute(repository) }.to change { ContainerRepository.count }.by(-1) + expect(repository).to be_delete_failed end - it 'sends disable_timeout = false as part of the params if it is set to false' do - expect_cleanup_tags_service_with(container_repository: repository, return_status: :success, disable_timeout: false) - expect { subject.execute(repository, disable_timeout: false) }.to change { ContainerRepository.count }.by(-1) - end + it 'logs the error' do + subject.execute(repository) - context 'when deleting the tags fails' do - it 'sets status as deleted_failed' do - expect_cleanup_tags_service_with(container_repository: repository, return_status: :error) - allow(Gitlab::AppLogger).to receive(:error).and_call_original + expect(Gitlab::AppLogger).to have_received(:error) + .with("Container repository with ID: #{repository.id} and path: #{repository.path} failed with message: error in deleting tags") + end - subject.execute(repository) + it_behaves_like 'returning an error status with message', 'Deletion failed for container repository' + end - expect(repository).to be_delete_failed - expect(Gitlab::AppLogger).to have_received(:error) - .with("Container repository with ID: #{repository.id} and path: #{repository.path} failed with message: error in deleting tags") - end + context 'when destroying the repository fails' do + before do + expect_cleanup_tags_service_with(container_repository: repository, return_status: :success) + allow(repository).to receive(:destroy).and_return(false) + allow(repository.errors).to receive(:full_messages).and_return(['Error 1', 'Error 2']) + allow(Gitlab::AppLogger).to receive(:error).and_call_original end - context 'when destroying the repository fails' do - it 'sets status as deleted_failed' do - expect_cleanup_tags_service_with(container_repository: repository, return_status: :success) - allow(repository).to receive(:destroy).and_return(false) - allow(repository.errors).to receive(:full_messages).and_return(['Error 1', 'Error 2']) - allow(Gitlab::AppLogger).to receive(:error).and_call_original + it 'sets status as deleted_failed' do + subject.execute(repository) + + expect(repository).to be_delete_failed + end - subject.execute(repository) + it 'logs the error' do + subject.execute(repository) - expect(repository).to be_delete_failed - expect(Gitlab::AppLogger).to have_received(:error) - .with("Container repository with ID: #{repository.id} and path: #{repository.path} failed with message: Error 1. Error 2") - end + expect(Gitlab::AppLogger).to have_received(:error) + .with("Container repository with ID: #{repository.id} and path: #{repository.path} failed with message: Error 1. Error 2") end - def expect_cleanup_tags_service_with(container_repository:, return_status:, disable_timeout: true) - delete_tags_service = instance_double(Projects::ContainerRepository::CleanupTagsService) + it_behaves_like 'returning an error status with message', 'Deletion failed for container repository' + end + end + + context 'when user has access to registry' do + before do + project.add_developer(user) + end - expect(Projects::ContainerRepository::CleanupTagsService).to receive(:new).with( - container_repository: container_repository, - params: described_class::CLEANUP_TAGS_SERVICE_PARAMS.merge('disable_timeout' => disable_timeout) - ).and_return(delete_tags_service) + it_behaves_like 'executing with permissions' + end - expect(delete_tags_service).to receive(:execute).and_return(status: return_status) - end + context 'when user does not have access to registry' do + let_it_be(:repository) { create(:container_repository, :root, project: project) } + + it 'does not delete a repository' do + expect { subject.execute(repository) }.not_to change { ContainerRepository.count } end + + it_behaves_like 'returning an error status with message', 'Unauthorized access' + end + + context 'when called during project deletion' do + let(:user) { nil } + let(:params) { { skip_permission_check: true } } + + it_behaves_like 'executing with permissions' + end + + context 'when there is no user' do + let(:user) { nil } + let(:repository) { create(:container_repository, :root, project: project) } + + it_behaves_like 'returning an error status with message', 'Unauthorized access' + end + + def expect_cleanup_tags_service_with(container_repository:, return_status:, disable_timeout: true) + delete_tags_service = instance_double(Projects::ContainerRepository::CleanupTagsService) + + expect(Projects::ContainerRepository::CleanupTagsService).to receive(:new).with( + container_repository: container_repository, + params: described_class::CLEANUP_TAGS_SERVICE_PARAMS.merge('disable_timeout' => disable_timeout) + ).and_return(delete_tags_service) + + expect(delete_tags_service).to receive(:execute).and_return(status: return_status) end end diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb index f85a8eda7ee..e435db4efa6 100644 --- a/spec/services/projects/create_service_spec.rb +++ b/spec/services/projects/create_service_spec.rb @@ -163,7 +163,7 @@ RSpec.describe Projects::CreateService, '#execute', feature_category: :projects describe 'after create actions' do it 'invalidate personal_projects_count caches' do - expect(user).to receive(:invalidate_personal_projects_count) + expect(Rails.cache).to receive(:delete).with(['users', user.id, 'personal_projects_count']) create_project(user, opts) end @@ -947,6 +947,8 @@ RSpec.describe Projects::CreateService, '#execute', feature_category: :projects end it 'schedules authorization update for users with access to group', :sidekiq_inline do + stub_feature_flags(do_not_run_safety_net_auth_refresh_jobs: false) + expect(AuthorizedProjectsWorker).not_to( receive(:bulk_perform_async) ) diff --git a/spec/services/projects/destroy_service_spec.rb b/spec/services/projects/destroy_service_spec.rb index ff2de45661f..0689a65c2f4 100644 --- a/spec/services/projects/destroy_service_spec.rb +++ b/spec/services/projects/destroy_service_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Projects::DestroyService, :aggregate_failures, :event_store_publisher do +RSpec.describe Projects::DestroyService, :aggregate_failures, :event_store_publisher, feature_category: :projects do include ProjectForksHelper include BatchDestroyDependentAssociationsHelper @@ -151,10 +151,22 @@ RSpec.describe Projects::DestroyService, :aggregate_failures, :event_store_publi it_behaves_like 'deleting the project' - it 'invalidates personal_project_count cache' do - expect(user).to receive(:invalidate_personal_projects_count) + context 'personal projects count cache' do + context 'when the executor is the creator of the project itself' do + it 'invalidates personal_project_count cache of the the owner of the personal namespace' do + expect(user).to receive(:invalidate_personal_projects_count) - destroy_project(project, user, {}) + destroy_project(project, user, {}) + end + end + + context 'when the executor is the instance administrator', :enable_admin_mode do + it 'invalidates personal_project_count cache of the the owner of the personal namespace' do + expect(user).to receive(:invalidate_personal_projects_count) + + destroy_project(project, create(:admin), {}) + end + end end context 'with running pipelines' do @@ -331,18 +343,30 @@ RSpec.describe Projects::DestroyService, :aggregate_failures, :event_store_publi end context 'when image repository deletion succeeds' do - it 'removes tags' do - expect_any_instance_of(Projects::ContainerRepository::CleanupTagsService) - .to receive(:execute).and_return({ status: :success }) + it 'returns true' do + expect_next_instance_of(Projects::ContainerRepository::CleanupTagsService) do |instance| + expect(instance).to receive(:execute).and_return(status: :success) + end - destroy_project(project, user) + expect(destroy_project(project, user)).to be true + end + end + + context 'when image repository deletion raises an error' do + it 'returns false' do + expect_next_instance_of(Projects::ContainerRepository::CleanupTagsService) do |service| + expect(service).to receive(:execute).and_raise(RuntimeError) + end + + expect(destroy_project(project, user)).to be false end end context 'when image repository deletion fails' do - it 'raises an exception' do - expect_any_instance_of(Projects::ContainerRepository::CleanupTagsService) - .to receive(:execute).and_raise(RuntimeError) + it 'returns false' do + expect_next_instance_of(Projects::ContainerRepository::DestroyService) do |service| + expect(service).to receive(:execute).and_return({ status: :error }) + end expect(destroy_project(project, user)).to be false end @@ -369,8 +393,9 @@ RSpec.describe Projects::DestroyService, :aggregate_failures, :event_store_publi context 'when image repository tags deletion succeeds' do it 'removes tags' do - expect_any_instance_of(ContainerRepository) - .to receive(:delete_tags!).and_return(true) + expect_next_instance_of(Projects::ContainerRepository::DestroyService) do |service| + expect(service).to receive(:execute).and_return({ status: :sucess }) + end destroy_project(project, user) end @@ -378,13 +403,27 @@ RSpec.describe Projects::DestroyService, :aggregate_failures, :event_store_publi context 'when image repository tags deletion fails' do it 'raises an exception' do - expect_any_instance_of(ContainerRepository) - .to receive(:delete_tags!).and_return(false) + expect_next_instance_of(Projects::ContainerRepository::DestroyService) do |service| + expect(service).to receive(:execute).and_return({ status: :error }) + end expect(destroy_project(project, user)).to be false end end end + + context 'when there are no tags for legacy root repository' do + before do + stub_container_registry_tags(repository: project.full_path, + tags: []) + end + + it 'does not try to destroy the repository' do + expect(Projects::ContainerRepository::DestroyService).not_to receive(:new) + + destroy_project(project, user) + end + end end context 'for a forked project with LFS objects' do diff --git a/spec/services/projects/group_links/create_service_spec.rb b/spec/services/projects/group_links/create_service_spec.rb index 65d3085a850..eae898b4f68 100644 --- a/spec/services/projects/group_links/create_service_spec.rb +++ b/spec/services/projects/group_links/create_service_spec.rb @@ -58,6 +58,8 @@ RSpec.describe Projects::GroupLinks::CreateService, '#execute' do end it 'schedules authorization update for users with access to group' do + stub_feature_flags(do_not_run_safety_net_auth_refresh_jobs: false) + expect(AuthorizedProjectsWorker).not_to( receive(:bulk_perform_async) ) diff --git a/spec/services/projects/group_links/destroy_service_spec.rb b/spec/services/projects/group_links/destroy_service_spec.rb index 5d07fd52230..89865d6bc3b 100644 --- a/spec/services/projects/group_links/destroy_service_spec.rb +++ b/spec/services/projects/group_links/destroy_service_spec.rb @@ -28,6 +28,8 @@ RSpec.describe Projects::GroupLinks::DestroyService, '#execute' do end it 'calls AuthorizedProjectUpdate::UserRefreshFromReplicaWorker with a delay to update project authorizations' do + stub_feature_flags(do_not_run_safety_net_auth_refresh_jobs: false) + expect(AuthorizedProjectUpdate::UserRefreshFromReplicaWorker).to( receive(:bulk_perform_in) .with(1.hour, diff --git a/spec/services/projects/group_links/update_service_spec.rb b/spec/services/projects/group_links/update_service_spec.rb index 20616890ebd..1acbb770763 100644 --- a/spec/services/projects/group_links/update_service_spec.rb +++ b/spec/services/projects/group_links/update_service_spec.rb @@ -42,6 +42,8 @@ RSpec.describe Projects::GroupLinks::UpdateService, '#execute' do end it 'calls AuthorizedProjectUpdate::UserRefreshFromReplicaWorker with a delay to update project authorizations' do + stub_feature_flags(do_not_run_safety_net_auth_refresh_jobs: false) + expect(AuthorizedProjectUpdate::UserRefreshFromReplicaWorker).to( receive(:bulk_perform_in) .with(1.hour, diff --git a/spec/services/projects/import_export/export_service_spec.rb b/spec/services/projects/import_export/export_service_spec.rb index 2c1ebe27014..be059aec697 100644 --- a/spec/services/projects/import_export/export_service_spec.rb +++ b/spec/services/projects/import_export/export_service_spec.rb @@ -2,11 +2,12 @@ require 'spec_helper' -RSpec.describe Projects::ImportExport::ExportService do +RSpec.describe Projects::ImportExport::ExportService, feature_category: :importers do describe '#execute' do let_it_be(:user) { create(:user) } + let_it_be(:group) { create(:group) } + let_it_be_with_reload(:project) { create(:project, group: group) } - let(:project) { create(:project) } let(:shared) { project.import_export_shared } let!(:after_export_strategy) { Gitlab::ImportExport::AfterExportStrategies::DownloadNotificationStrategy.new } @@ -220,5 +221,21 @@ RSpec.describe Projects::ImportExport::ExportService do expect { service.execute }.to raise_error(Gitlab::ImportExport::Error).with_message(expected_message) end end + + it "avoids N+1 when exporting project members" do + group.add_owner(user) + group.add_maintainer(create(:user)) + project.add_maintainer(create(:user)) + + # warm up + service.execute + + control = ActiveRecord::QueryRecorder.new { service.execute } + + group.add_maintainer(create(:user)) + project.add_maintainer(create(:user)) + + expect { service.execute }.not_to exceed_query_limit(control) + end end end diff --git a/spec/services/projects/import_service_spec.rb b/spec/services/projects/import_service_spec.rb index 38ab7b6e2ee..97a3b338069 100644 --- a/spec/services/projects/import_service_spec.rb +++ b/spec/services/projects/import_service_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Projects::ImportService do +RSpec.describe Projects::ImportService, feature_category: :importers do let!(:project) { create(:project) } let(:user) { project.creator } diff --git a/spec/services/projects/protect_default_branch_service_spec.rb b/spec/services/projects/protect_default_branch_service_spec.rb index c8aa421cdd4..9f9e89ff8f8 100644 --- a/spec/services/projects/protect_default_branch_service_spec.rb +++ b/spec/services/projects/protect_default_branch_service_spec.rb @@ -233,6 +233,38 @@ RSpec.describe Projects::ProtectDefaultBranchService do end end + describe '#protected_branch_exists?' do + let_it_be(:group) { create(:group) } + let_it_be(:project) { create(:project, group: group) } + + let(:default_branch) { "default-branch" } + + before do + allow(project).to receive(:default_branch).and_return(default_branch) + create(:protected_branch, project: nil, group: group, name: default_branch) + end + + context 'when feature flag `group_protected_branches` disabled' do + before do + stub_feature_flags(group_protected_branches: false) + end + + it 'return false' do + expect(service.protected_branch_exists?).to eq(false) + end + end + + context 'when feature flag `group_protected_branches` enabled' do + before do + stub_feature_flags(group_protected_branches: true) + end + + it 'return true' do + expect(service.protected_branch_exists?).to eq(true) + end + end + end + describe '#default_branch' do it 'returns the default branch of the project' do allow(project) diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb index 5171836f917..32818535146 100644 --- a/spec/services/projects/transfer_service_spec.rb +++ b/spec/services/projects/transfer_service_spec.rb @@ -126,6 +126,12 @@ RSpec.describe Projects::TransferService do expect(project.namespace).to eq(user.namespace) end + it 'invalidates personal_project_count cache of the the owner of the personal namespace' do + expect(user).to receive(:invalidate_personal_projects_count) + + execute_transfer + end + context 'the owner of the namespace does not have a direct membership in the project residing in the group' do it 'creates a project membership record for the owner of the namespace, with OWNER access level, after the transfer' do execute_transfer @@ -161,6 +167,17 @@ RSpec.describe Projects::TransferService do end end + context 'personal namespace -> group', :enable_admin_mode do + let(:executor) { create(:admin) } + + it 'invalidates personal_project_count cache of the the owner of the personal namespace' \ + 'that previously held the project' do + expect(user).to receive(:invalidate_personal_projects_count) + + execute_transfer + end + end + context 'when transfer succeeds' do before do group.add_owner(user) @@ -645,6 +662,8 @@ RSpec.describe Projects::TransferService do end it 'calls AuthorizedProjectUpdate::UserRefreshFromReplicaWorker with a delay to update project authorizations' do + stub_feature_flags(do_not_run_safety_net_auth_refresh_jobs: false) + user_ids = [user.id, member_of_old_group.id, member_of_new_group.id].map { |id| [id] } expect(AuthorizedProjectUpdate::UserRefreshFromReplicaWorker).to( diff --git a/spec/services/protected_branches/create_service_spec.rb b/spec/services/protected_branches/create_service_spec.rb index 9c8fe769ed8..625aa4fa377 100644 --- a/spec/services/protected_branches/create_service_spec.rb +++ b/spec/services/protected_branches/create_service_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe ProtectedBranches::CreateService do +RSpec.describe ProtectedBranches::CreateService, feature_category: :compliance_management do shared_examples 'execute with entity' do let(:params) do { @@ -58,6 +58,7 @@ RSpec.describe ProtectedBranches::CreateService do context 'with entity project' do let_it_be_with_reload(:entity) { create(:project) } + let(:user) { entity.first_owner } it_behaves_like 'execute with entity' diff --git a/spec/services/quick_actions/interpret_service_spec.rb b/spec/services/quick_actions/interpret_service_spec.rb index 8eccb9e41bb..257e7eb972b 100644 --- a/spec/services/quick_actions/interpret_service_spec.rb +++ b/spec/services/quick_actions/interpret_service_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe QuickActions::InterpretService do +RSpec.describe QuickActions::InterpretService, feature_category: :team_planning do include AfterNextHelpers let_it_be(:group) { create(:group, :crm_enabled) } diff --git a/spec/services/quick_actions/target_service_spec.rb b/spec/services/quick_actions/target_service_spec.rb index d960678f809..1b0a5d4ae73 100644 --- a/spec/services/quick_actions/target_service_spec.rb +++ b/spec/services/quick_actions/target_service_spec.rb @@ -12,9 +12,9 @@ RSpec.describe QuickActions::TargetService do end describe '#execute' do - shared_examples 'no target' do |type_id:| + shared_examples 'no target' do |type_iid:| it 'returns nil' do - target = service.execute(type, type_id) + target = service.execute(type, type_iid) expect(target).to be_nil end @@ -22,15 +22,15 @@ RSpec.describe QuickActions::TargetService do shared_examples 'find target' do it 'returns the target' do - found_target = service.execute(type, target_id) + found_target = service.execute(type, target_iid) expect(found_target).to eq(target) end end - shared_examples 'build target' do |type_id:| + shared_examples 'build target' do |type_iid:| it 'builds a new target' do - target = service.execute(type, type_id) + target = service.execute(type, type_iid) expect(target.project).to eq(project) expect(target).to be_new_record @@ -39,36 +39,44 @@ RSpec.describe QuickActions::TargetService do context 'for issue' do let(:target) { create(:issue, project: project) } - let(:target_id) { target.iid } + let(:target_iid) { target.iid } let(:type) { 'Issue' } it_behaves_like 'find target' - it_behaves_like 'build target', type_id: nil - it_behaves_like 'build target', type_id: -1 + it_behaves_like 'build target', type_iid: nil + it_behaves_like 'build target', type_iid: -1 + end + + context 'for work item' do + let(:target) { create(:work_item, :task, project: project) } + let(:target_iid) { target.iid } + let(:type) { 'WorkItem' } + + it_behaves_like 'find target' end context 'for merge request' do let(:target) { create(:merge_request, source_project: project) } - let(:target_id) { target.iid } + let(:target_iid) { target.iid } let(:type) { 'MergeRequest' } it_behaves_like 'find target' - it_behaves_like 'build target', type_id: nil - it_behaves_like 'build target', type_id: -1 + it_behaves_like 'build target', type_iid: nil + it_behaves_like 'build target', type_iid: -1 end context 'for commit' do let(:project) { create(:project, :repository) } let(:target) { project.commit.parent } - let(:target_id) { target.sha } + let(:target_iid) { target.sha } let(:type) { 'Commit' } it_behaves_like 'find target' - it_behaves_like 'no target', type_id: 'invalid_sha' + it_behaves_like 'no target', type_iid: 'invalid_sha' - context 'with nil target_id' do + context 'with nil target_iid' do let(:target) { project.commit } - let(:target_id) { nil } + let(:target_iid) { nil } it_behaves_like 'find target' end @@ -77,7 +85,7 @@ RSpec.describe QuickActions::TargetService do context 'for unknown type' do let(:type) { 'unknown' } - it_behaves_like 'no target', type_id: :unused + it_behaves_like 'no target', type_iid: :unused end end end diff --git a/spec/services/releases/create_service_spec.rb b/spec/services/releases/create_service_spec.rb index 5f49eed3e77..9768ceb12e8 100644 --- a/spec/services/releases/create_service_spec.rb +++ b/spec/services/releases/create_service_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Releases::CreateService do +RSpec.describe Releases::CreateService, feature_category: :continuous_integration do let(:project) { create(:project, :repository) } let(:user) { create(:user) } let(:tag_name) { project.repository.tag_names.first } @@ -132,6 +132,15 @@ RSpec.describe Releases::CreateService do expect(result[:status]).to eq(:error) expect(result[:message]).to eq("Milestone(s) not found: #{inexistent_milestone_tag}") end + + it 'raises an error saying the milestone id is inexistent' do + inexistent_milestone_id = non_existing_record_id + service = described_class.new(project, user, params.merge!({ milestone_ids: [inexistent_milestone_id] })) + result = service.execute + + expect(result[:status]).to eq(:error) + expect(result[:message]).to eq("Milestone id(s) not found: #{inexistent_milestone_id}") + end end context 'when existing milestone is passed in' do @@ -140,15 +149,27 @@ RSpec.describe Releases::CreateService do let(:params_with_milestone) { params.merge!({ milestones: [title] }) } let(:service) { described_class.new(milestone.project, user, params_with_milestone) } - it 'creates a release and ties this milestone to it' do - result = service.execute + shared_examples 'creates release' do + it 'creates a release and ties this milestone to it' do + result = service.execute - expect(project.releases.count).to eq(1) - expect(result[:status]).to eq(:success) + expect(project.releases.count).to eq(1) + expect(result[:status]).to eq(:success) + + release = project.releases.last + + expect(release.milestones).to match_array([milestone]) + end + end - release = project.releases.last + context 'by title' do + it_behaves_like 'creates release' + end + + context 'by ids' do + let(:params_with_milestone) { params.merge!({ milestone_ids: [milestone.id] }) } - expect(release.milestones).to match_array([milestone]) + it_behaves_like 'creates release' end context 'when another release was previously created with that same milestone linked' do @@ -164,18 +185,31 @@ RSpec.describe Releases::CreateService do end end - context 'when multiple existing milestone titles are passed in' do + context 'when multiple existing milestones are passed in' do let(:title_1) { 'v1.0' } let(:title_2) { 'v1.0-rc' } let!(:milestone_1) { create(:milestone, :active, project: project, title: title_1) } let!(:milestone_2) { create(:milestone, :active, project: project, title: title_2) } - let!(:params_with_milestones) { params.merge!({ milestones: [title_1, title_2] }) } - it 'creates a release and ties it to these milestones' do - described_class.new(project, user, params_with_milestones).execute - release = project.releases.last + shared_examples 'creates multiple releases' do + it 'creates a release and ties it to these milestones' do + described_class.new(project, user, params_with_milestones).execute + release = project.releases.last + + expect(release.milestones.map(&:title)).to include(title_1, title_2) + end + end + + context 'by title' do + let!(:params_with_milestones) { params.merge!({ milestones: [title_1, title_2] }) } + + it_behaves_like 'creates multiple releases' + end + + context 'by ids' do + let!(:params_with_milestones) { params.merge!({ milestone_ids: [milestone_1.id, milestone_2.id] }) } - expect(release.milestones.map(&:title)).to include(title_1, title_2) + it_behaves_like 'creates multiple releases' end end @@ -198,6 +232,17 @@ RSpec.describe Releases::CreateService do service.execute end.not_to change(Release, :count) end + + context 'with milestones as ids' do + let!(:params_with_milestones) { params.merge!({ milestone_ids: [milestone.id, non_existing_record_id] }) } + + it 'raises an error' do + result = service.execute + + expect(result[:status]).to eq(:error) + expect(result[:message]).to eq("Milestone id(s) not found: #{non_existing_record_id}") + end + end end context 'no milestone association behavior' do diff --git a/spec/services/releases/update_service_spec.rb b/spec/services/releases/update_service_spec.rb index 7461470a844..6bddea48251 100644 --- a/spec/services/releases/update_service_spec.rb +++ b/spec/services/releases/update_service_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Releases::UpdateService do +RSpec.describe Releases::UpdateService, feature_category: :continuous_integration do let(:project) { create(:project, :repository) } let(:user) { create(:user) } let(:new_name) { 'A new name' } @@ -60,18 +60,22 @@ RSpec.describe Releases::UpdateService do release.milestones << milestone end - context 'a different milestone' do - let(:new_title) { 'v2.0' } - + shared_examples 'updates milestones' do it 'updates the related milestone accordingly' do - result = service.execute release.reload + result = service.execute expect(release.milestones.first.title).to eq(new_title) expect(result[:milestones_updated]).to be_truthy end end + context 'a different milestone' do + let(:new_title) { 'v2.0' } + + it_behaves_like 'updates milestones' + end + context 'an identical milestone' do let(:new_title) { 'v1.0' } @@ -79,11 +83,17 @@ RSpec.describe Releases::UpdateService do expect { service.execute }.to raise_error(ActiveRecord::RecordInvalid) end end + + context 'by ids' do + let(:new_title) { 'v2.0' } + let(:params_with_milestone) { params.merge!({ milestone_ids: [new_milestone.id] }) } + + it_behaves_like 'updates milestones' + end end context "when an 'empty' milestone is passed in" do let(:milestone) { create(:milestone, project: project, title: 'v1.0') } - let(:params_with_empty_milestone) { params.merge!({ milestones: [] }) } before do release.milestones << milestone @@ -91,12 +101,26 @@ RSpec.describe Releases::UpdateService do service.params = params_with_empty_milestone end - it 'removes the old milestone and does not associate any new milestone' do - result = service.execute - release.reload + shared_examples 'removes milestones' do + it 'removes the old milestone and does not associate any new milestone' do + result = service.execute + release.reload + + expect(release.milestones).not_to be_present + expect(result[:milestones_updated]).to be_truthy + end + end - expect(release.milestones).not_to be_present - expect(result[:milestones_updated]).to be_truthy + context 'by title' do + let(:params_with_empty_milestone) { params.merge!({ milestones: [] }) } + + it_behaves_like 'removes milestones' + end + + context 'by id' do + let(:params_with_empty_milestone) { params.merge!({ milestone_ids: [] }) } + + it_behaves_like 'removes milestones' end end @@ -104,22 +128,35 @@ RSpec.describe Releases::UpdateService do let(:new_title_1) { 'v2.0' } let(:new_title_2) { 'v2.0-rc' } let(:milestone) { create(:milestone, project: project, title: 'v1.0') } - let(:params_with_milestones) { params.merge!({ milestones: [new_title_1, new_title_2] }) } let(:service) { described_class.new(project, user, params_with_milestones) } + let!(:new_milestone_1) { create(:milestone, project: project, title: new_title_1) } + let!(:new_milestone_2) { create(:milestone, project: project, title: new_title_2) } before do - create(:milestone, project: project, title: new_title_1) - create(:milestone, project: project, title: new_title_2) release.milestones << milestone end - it 'removes the old milestone and update the release with the new ones' do - result = service.execute - release.reload + shared_examples 'updates multiple milestones' do + it 'removes the old milestone and update the release with the new ones' do + result = service.execute + release.reload + + milestone_titles = release.milestones.map(&:title) + expect(milestone_titles).to match_array([new_title_1, new_title_2]) + expect(result[:milestones_updated]).to be_truthy + end + end + + context 'by title' do + let(:params_with_milestones) { params.merge!({ milestones: [new_title_1, new_title_2] }) } + + it_behaves_like 'updates multiple milestones' + end + + context 'by id' do + let(:params_with_milestones) { params.merge!({ milestone_ids: [new_milestone_1.id, new_milestone_2.id] }) } - milestone_titles = release.milestones.map(&:title) - expect(milestone_titles).to match_array([new_title_1, new_title_2]) - expect(result[:milestones_updated]).to be_truthy + it_behaves_like 'updates multiple milestones' end end end diff --git a/spec/services/resource_events/change_labels_service_spec.rb b/spec/services/resource_events/change_labels_service_spec.rb index 9b0ca54a394..d94b49de9d7 100644 --- a/spec/services/resource_events/change_labels_service_spec.rb +++ b/spec/services/resource_events/change_labels_service_spec.rb @@ -2,7 +2,8 @@ require 'spec_helper' -RSpec.describe ResourceEvents::ChangeLabelsService do +# feature category is shared among plan(issues, epics), monitor(incidents), create(merge request) stages +RSpec.describe ResourceEvents::ChangeLabelsService, feature_category: :shared do let_it_be(:project) { create(:project) } let_it_be(:author) { create(:user) } let_it_be(:issue) { create(:issue, project: project) } @@ -86,12 +87,30 @@ RSpec.describe ResourceEvents::ChangeLabelsService do let(:added) { [labels[0]] } let(:removed) { [labels[1]] } + it_behaves_like 'creating timeline events' + it 'creates all label events in a single query' do expect(ApplicationRecord).to receive(:legacy_bulk_insert).once.and_call_original expect { change_labels }.to change { resource.resource_label_events.count }.from(0).to(2) end - it_behaves_like 'creating timeline events' + context 'when resource is a work item' do + it 'triggers note created subscription' do + expect(GraphqlTriggers).to receive(:work_item_note_created) + + change_labels + end + end + + context 'when resource is an MR' do + let(:resource) { create(:merge_request, source_project: project) } + + it 'does not trigger note created subscription' do + expect(GraphqlTriggers).not_to receive(:work_item_note_created) + + change_labels + end + end end describe 'usage data' do diff --git a/spec/services/security/ci_configuration/sast_create_service_spec.rb b/spec/services/security/ci_configuration/sast_create_service_spec.rb index 1e6dc367146..e80fe1a42fa 100644 --- a/spec/services/security/ci_configuration/sast_create_service_spec.rb +++ b/spec/services/security/ci_configuration/sast_create_service_spec.rb @@ -2,7 +2,8 @@ require 'spec_helper' -RSpec.describe Security::CiConfiguration::SastCreateService, :snowplow, feature_category: :sast do +RSpec.describe Security::CiConfiguration::SastCreateService, :snowplow, + feature_category: :static_application_security_testing do subject(:result) { described_class.new(project, user, params).execute } let(:branch_name) { 'set-sast-config-1' } @@ -24,7 +25,45 @@ RSpec.describe Security::CiConfiguration::SastCreateService, :snowplow, feature_ include_examples 'services security ci configuration create service' - context "when committing to the default branch", :aggregate_failures do + RSpec.shared_examples_for 'commits directly to the default branch' do + it 'commits directly to the default branch' do + expect(project).to receive(:default_branch).twice.and_return('master') + + expect(result.status).to eq(:success) + expect(result.payload[:success_path]).to match(/#{Gitlab::Routing.url_helpers.project_new_merge_request_url(project, {})}(.*)description(.*)source_branch/) + expect(result.payload[:branch]).to eq('master') + end + end + + context 'when the repository is empty' do + let_it_be(:project) { create(:project_empty_repo) } + + context 'when initialize_with_sast is false' do + before do + project.add_developer(user) + end + + let(:params) { { initialize_with_sast: false } } + + it 'raises an error' do + expect { result }.to raise_error(Gitlab::Graphql::Errors::MutationError) + end + end + + context 'when initialize_with_sast is true' do + let(:params) { { initialize_with_sast: true } } + + subject(:result) { described_class.new(project, user, params, commit_on_default: true).execute } + + before do + project.add_maintainer(user) + end + + it_behaves_like 'commits directly to the default branch' + end + end + + context 'when committing to the default branch', :aggregate_failures do subject(:result) { described_class.new(project, user, params, commit_on_default: true).execute } let(:params) { {} } @@ -33,17 +72,13 @@ RSpec.describe Security::CiConfiguration::SastCreateService, :snowplow, feature_ project.add_developer(user) end - it "doesn't try to remove that branch on raised exceptions" do + it 'does not try to remove that branch on raised exceptions' do expect(Files::MultiService).to receive(:new).and_raise(StandardError, '_exception_') expect(project.repository).not_to receive(:rm_branch) expect { result }.to raise_error(StandardError, '_exception_') end - it "commits directly to the default branch" do - expect(result.status).to eq(:success) - expect(result.payload[:success_path]).to match(/#{Gitlab::Routing.url_helpers.project_new_merge_request_url(project, {})}(.*)description(.*)source_branch/) - expect(result.payload[:branch]).to eq('master') - end + it_behaves_like 'commits directly to the default branch' end end diff --git a/spec/services/serverless/associate_domain_service_spec.rb b/spec/services/serverless/associate_domain_service_spec.rb index 3b5231989bc..2f45806589e 100644 --- a/spec/services/serverless/associate_domain_service_spec.rb +++ b/spec/services/serverless/associate_domain_service_spec.rb @@ -3,13 +3,24 @@ require 'spec_helper' RSpec.describe Serverless::AssociateDomainService do - subject { described_class.new(knative, pages_domain_id: pages_domain_id, creator: creator) } + let_it_be(:sdc_pages_domain) { create(:pages_domain, :instance_serverless) } + let_it_be(:sdc_cluster) { create(:cluster, :with_installed_helm, :provided_by_gcp) } + let_it_be(:sdc_knative) { create(:clusters_applications_knative, cluster: sdc_cluster) } + let_it_be(:sdc_creator) { create(:user) } + + let(:sdc) do + create(:serverless_domain_cluster, + knative: sdc_knative, + creator: sdc_creator, + pages_domain: sdc_pages_domain) + end - let(:sdc) { create(:serverless_domain_cluster, pages_domain: create(:pages_domain, :instance_serverless)) } let(:knative) { sdc.knative } let(:creator) { sdc.creator } let(:pages_domain_id) { sdc.pages_domain_id } + subject { described_class.new(knative, pages_domain_id: pages_domain_id, creator: creator) } + context 'when the domain is unchanged' do let(:creator) { create(:user) } @@ -19,8 +30,8 @@ RSpec.describe Serverless::AssociateDomainService do end context 'when domain is changed to nil' do - let(:pages_domain_id) { nil } - let(:creator) { create(:user) } + let_it_be(:creator) { create(:user) } + let_it_be(:pages_domain_id) { nil } it 'removes the association between knative and the domain' do expect { subject.execute }.to change { knative.reload.pages_domain }.from(sdc.pages_domain).to(nil) @@ -32,11 +43,13 @@ RSpec.describe Serverless::AssociateDomainService do end context 'when a new domain is associated' do - let(:pages_domain_id) { create(:pages_domain, :instance_serverless).id } - let(:creator) { create(:user) } + let_it_be(:creator) { create(:user) } + let_it_be(:pages_domain_id) { create(:pages_domain, :instance_serverless).id } it 'creates an association with the domain' do - expect { subject.execute }.to change { knative.pages_domain.id }.from(sdc.pages_domain.id).to(pages_domain_id) + expect { subject.execute }.to change { knative.reload.pages_domain.id } + .from(sdc.pages_domain.id) + .to(pages_domain_id) end it 'updates creator' do @@ -45,7 +58,7 @@ RSpec.describe Serverless::AssociateDomainService do end context 'when knative is not authorized to use the pages domain' do - let(:pages_domain_id) { create(:pages_domain).id } + let_it_be(:pages_domain_id) { create(:pages_domain).id } before do expect(knative).to receive(:available_domains).and_return(PagesDomain.none) @@ -56,19 +69,23 @@ RSpec.describe Serverless::AssociateDomainService do end end - context 'when knative hostname is nil' do - let(:knative) { build(:clusters_applications_knative, hostname: nil) } + describe 'for new knative application' do + let_it_be(:cluster) { create(:cluster, :with_installed_helm, :provided_by_gcp) } - it 'sets hostname to a placeholder value' do - expect { subject.execute }.to change { knative.hostname }.to('example.com') + context 'when knative hostname is nil' do + let(:knative) { build(:clusters_applications_knative, cluster: cluster, hostname: nil) } + + it 'sets hostname to a placeholder value' do + expect { subject.execute }.to change { knative.hostname }.to('example.com') + end end - end - context 'when knative hostname exists' do - let(:knative) { build(:clusters_applications_knative, hostname: 'hostname.com') } + context 'when knative hostname exists' do + let(:knative) { build(:clusters_applications_knative, cluster: cluster, hostname: 'hostname.com') } - it 'does not change hostname' do - expect { subject.execute }.not_to change { knative.hostname } + it 'does not change hostname' do + expect { subject.execute }.not_to change { knative.hostname } + end end end end diff --git a/spec/services/spam/spam_verdict_service_spec.rb b/spec/services/spam/spam_verdict_service_spec.rb index b89c96129c2..dde93aa6b93 100644 --- a/spec/services/spam/spam_verdict_service_spec.rb +++ b/spec/services/spam/spam_verdict_service_spec.rb @@ -28,10 +28,6 @@ RSpec.describe Spam::SpamVerdictService do extra_attributes end - before do - stub_feature_flags(allow_possible_spam: false) - end - shared_examples 'execute spam verdict service' do subject { service.execute } @@ -119,9 +115,9 @@ RSpec.describe Spam::SpamVerdictService do end end - context 'if allow_possible_spam flag is true' do + context 'if allow_possible_spam application setting is true' do before do - stub_feature_flags(allow_possible_spam: true) + stub_application_setting(allow_possible_spam: true) end context 'and a service returns a verdict that should be overridden' do diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index a192fae27db..38b6943b12a 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe SystemNoteService do +RSpec.describe SystemNoteService, feature_category: :shared do include Gitlab::Routing include RepoHelpers include AssetsHelpers diff --git a/spec/services/tasks_to_be_done/base_service_spec.rb b/spec/services/tasks_to_be_done/base_service_spec.rb index bf6be6d46e5..cfeff36cc0d 100644 --- a/spec/services/tasks_to_be_done/base_service_spec.rb +++ b/spec/services/tasks_to_be_done/base_service_spec.rb @@ -18,7 +18,7 @@ RSpec.describe TasksToBeDone::BaseService do subject(:service) do TasksToBeDone::CreateCiTaskService.new( - project: project, + container: project, current_user: current_user, assignee_ids: assignee_ids ) @@ -35,7 +35,7 @@ RSpec.describe TasksToBeDone::BaseService do expect(Issues::BuildService) .to receive(:new) - .with(project: project, current_user: current_user, params: params) + .with(container: project, current_user: current_user, params: params) .and_call_original expect { service.execute }.to change(Issue, :count).by(1) @@ -58,7 +58,7 @@ RSpec.describe TasksToBeDone::BaseService do expect(Issues::UpdateService) .to receive(:new) - .with(project: project, current_user: current_user, params: params) + .with(container: project, current_user: current_user, params: params) .and_call_original expect { service.execute }.not_to change(Issue, :count) diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb index 596ca9495ff..f73eae70d3c 100644 --- a/spec/services/todo_service_spec.rb +++ b/spec/services/todo_service_spec.rb @@ -1224,6 +1224,24 @@ RSpec.describe TodoService do end end + describe '#resolve_access_request_todos' do + let_it_be(:source) { create(:group, :public) } + let_it_be(:requester) { create(:group_member, :access_request, group: source, user: assignee) } + + it 'marks the todos for request handler as done' do + request_handler_todo = create(:todo, + user: member, + state: :pending, + action: Todo::MEMBER_ACCESS_REQUESTED, + author: requester.user, + target: source) + + service.resolve_access_request_todos(member, requester) + + expect(request_handler_todo.reload).to be_done + end + end + describe '#restore_todo' do let!(:todo) { create(:todo, :done, user: john_doe) } diff --git a/spec/services/todos/destroy/entity_leave_service_spec.rb b/spec/services/todos/destroy/entity_leave_service_spec.rb index 9d5ed70e9ef..1ced2eda799 100644 --- a/spec/services/todos/destroy/entity_leave_service_spec.rb +++ b/spec/services/todos/destroy/entity_leave_service_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Todos::Destroy::EntityLeaveService do +RSpec.describe Todos::Destroy::EntityLeaveService, feature_category: :team_planning do let_it_be(:user, reload: true) { create(:user) } let_it_be(:user2, reload: true) { create(:user) } let_it_be_with_refind(:group) { create(:group, :private) } diff --git a/spec/services/todos/destroy/group_private_service_spec.rb b/spec/services/todos/destroy/group_private_service_spec.rb index 30d02cb7400..be470688084 100644 --- a/spec/services/todos/destroy/group_private_service_spec.rb +++ b/spec/services/todos/destroy/group_private_service_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Todos::Destroy::GroupPrivateService do +RSpec.describe Todos::Destroy::GroupPrivateService, feature_category: :team_planning do let(:group) { create(:group, :public) } let(:project) { create(:project, group: group) } let(:user) { create(:user) } diff --git a/spec/services/user_project_access_changed_service_spec.rb b/spec/services/user_project_access_changed_service_spec.rb index be4f205afb5..356675d55f2 100644 --- a/spec/services/user_project_access_changed_service_spec.rb +++ b/spec/services/user_project_access_changed_service_spec.rb @@ -2,33 +2,39 @@ require 'spec_helper' -RSpec.describe UserProjectAccessChangedService do +RSpec.describe UserProjectAccessChangedService, feature_category: :authentication_and_authorization do describe '#execute' do - it 'schedules the user IDs' do - expect(AuthorizedProjectsWorker).to receive(:bulk_perform_and_wait) + it 'permits high-priority operation' do + expect(AuthorizedProjectsWorker).to receive(:bulk_perform_async) .with([[1], [2]]) described_class.new([1, 2]).execute end - it 'permits non-blocking operation' do - expect(AuthorizedProjectsWorker).to receive(:bulk_perform_async) - .with([[1], [2]]) + context 'for low priority operation' do + context 'when the feature flag `do_not_run_safety_net_auth_refresh_jobs` is disabled' do + before do + stub_feature_flags(do_not_run_safety_net_auth_refresh_jobs: false) + end + + it 'permits low-priority operation' do + expect(AuthorizedProjectUpdate::UserRefreshFromReplicaWorker).to( + receive(:bulk_perform_in).with( + described_class::DELAY, + [[1], [2]], + { batch_delay: 30.seconds, batch_size: 100 } + ) + ) + + described_class.new([1, 2]).execute(priority: described_class::LOW_PRIORITY) + end + end - described_class.new([1, 2]).execute(blocking: false) - end + it 'does not perform low-priority operation' do + expect(AuthorizedProjectUpdate::UserRefreshFromReplicaWorker).not_to receive(:bulk_perform_in) - it 'permits low-priority operation' do - expect(AuthorizedProjectUpdate::UserRefreshFromReplicaWorker).to( - receive(:bulk_perform_in).with( - described_class::DELAY, - [[1], [2]], - { batch_delay: 30.seconds, batch_size: 100 } - ) - ) - - described_class.new([1, 2]).execute(blocking: false, - priority: described_class::LOW_PRIORITY) + described_class.new([1, 2]).execute(priority: described_class::LOW_PRIORITY) + end end it 'permits medium-priority operation' do @@ -40,14 +46,12 @@ RSpec.describe UserProjectAccessChangedService do ) ) - described_class.new([1, 2]).execute(blocking: false, - priority: described_class::MEDIUM_PRIORITY) + described_class.new([1, 2]).execute(priority: described_class::MEDIUM_PRIORITY) end it 'sets the current caller_id as related_class in the context of all the enqueued jobs' do Gitlab::ApplicationContext.with_context(caller_id: 'Foo') do - described_class.new([1, 2]).execute(blocking: false, - priority: described_class::LOW_PRIORITY) + described_class.new([1, 2]).execute(priority: described_class::LOW_PRIORITY) end expect(AuthorizedProjectUpdate::UserRefreshFromReplicaWorker.jobs).to all( @@ -60,7 +64,7 @@ RSpec.describe UserProjectAccessChangedService do let(:service) { UserProjectAccessChangedService.new([1, 2]) } before do - expect(AuthorizedProjectsWorker).to receive(:bulk_perform_and_wait) + expect(AuthorizedProjectsWorker).to receive(:bulk_perform_async) .with([[1], [2]]) .and_return(10) end @@ -79,7 +83,7 @@ RSpec.describe UserProjectAccessChangedService do service = UserProjectAccessChangedService.new([1, 2, 3, 4, 5]) - allow(AuthorizedProjectsWorker).to receive(:bulk_perform_and_wait) + allow(AuthorizedProjectsWorker).to receive(:bulk_perform_async) .with([[1], [2], [3], [4], [5]]) .and_return(10) diff --git a/spec/services/users/activity_service_spec.rb b/spec/services/users/activity_service_spec.rb index 47a4b943d83..6c0d93f568a 100644 --- a/spec/services/users/activity_service_spec.rb +++ b/spec/services/users/activity_service_spec.rb @@ -7,9 +7,21 @@ RSpec.describe Users::ActivityService do let(:user) { create(:user, last_activity_on: last_activity_on) } - subject { described_class.new(user) } + subject { described_class.new(author: user) } describe '#execute', :clean_gitlab_redis_shared_state do + shared_examples 'does not update last_activity_on' do + it 'does not update user attribute' do + expect { subject.execute }.not_to change(user, :last_activity_on) + end + + it 'does not track Snowplow event' do + subject.execute + + expect_no_snowplow_event + end + end + context 'when last activity is nil' do let(:last_activity_on) { nil } @@ -41,13 +53,29 @@ RSpec.describe Users::ActivityService do subject.execute end + + it_behaves_like 'Snowplow event tracking with RedisHLL context' do + subject(:record_activity) { described_class.new(author: user, namespace: namespace, project: project).execute } + + let(:feature_flag_name) { :route_hll_to_snowplow_phase3 } + let(:category) { described_class.name } + let(:action) { 'perform_action' } + let(:label) { 'redis_hll_counters.manage.unique_active_users_monthly' } + let(:namespace) { build(:group) } + let(:project) { build(:project) } + let(:context) do + payload = Gitlab::Tracking::ServicePingContext.new(data_source: :redis_hll, + event: 'unique_active_user').to_context + [Gitlab::Json.dump(payload)] + end + end end context 'when a bad object is passed' do let(:fake_object) { double(username: 'hello') } it 'does not record activity' do - service = described_class.new(fake_object) + service = described_class.new(author: fake_object) expect(service).not_to receive(:record_activity) @@ -58,9 +86,7 @@ RSpec.describe Users::ActivityService do context 'when last activity is today' do let(:last_activity_on) { Date.today } - it 'does not update last_activity_on' do - expect { subject.execute }.not_to change(user, :last_activity_on) - end + it_behaves_like 'does not update last_activity_on' it 'does not try to obtain ExclusiveLease' do expect(Gitlab::ExclusiveLease).not_to receive(:new).with("activity_service:#{user.id}", anything) @@ -76,19 +102,17 @@ RSpec.describe Users::ActivityService do allow(Gitlab::Database).to receive(:read_only?).and_return(true) end - it 'does not update last_activity_on' do - expect { subject.execute }.not_to change(user, :last_activity_on) - end + it_behaves_like 'does not update last_activity_on' end context 'when a lease could not be obtained' do let(:last_activity_on) { nil } - it 'does not update last_activity_on' do + before do stub_exclusive_lease_taken("activity_service:#{user.id}", timeout: 1.minute.to_i) - - expect { subject.execute }.not_to change(user, :last_activity_on) end + + it_behaves_like 'does not update last_activity_on' end end @@ -104,7 +128,7 @@ RSpec.describe Users::ActivityService do end let(:service) do - service = described_class.new(user) + service = described_class.new(author: user) ::Gitlab::Database::LoadBalancing::Session.clear_session @@ -123,7 +147,7 @@ RSpec.describe Users::ActivityService do end context 'database load balancing is not configured' do - let(:service) { described_class.new(user) } + let(:service) { described_class.new(author: user) } it 'updates user without error' do service.execute diff --git a/spec/services/users/assigned_issues_count_service_spec.rb b/spec/services/users/assigned_issues_count_service_spec.rb index afa6a0af3dd..2062f68b24b 100644 --- a/spec/services/users/assigned_issues_count_service_spec.rb +++ b/spec/services/users/assigned_issues_count_service_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' RSpec.describe Users::AssignedIssuesCountService, :use_clean_rails_memory_store_caching, - feature_category: :project_management do + feature_category: :team_planning do let_it_be(:user) { create(:user) } let_it_be(:max_limit) { 10 } diff --git a/spec/services/web_hook_service_spec.rb b/spec/services/web_hook_service_spec.rb index 4b925a058e7..5736bf885be 100644 --- a/spec/services/web_hook_service_spec.rb +++ b/spec/services/web_hook_service_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe WebHookService, :request_store, :clean_gitlab_redis_shared_state do +RSpec.describe WebHookService, :request_store, :clean_gitlab_redis_shared_state, feature_category: :integrations do include StubRequests let(:ellipsis) { '…' } @@ -358,6 +358,7 @@ RSpec.describe WebHookService, :request_store, :clean_gitlab_redis_shared_state { trigger: 'push_hooks', url: project_hook.url, + interpolated_url: project_hook.interpolated_url, request_headers: headers, request_data: data, response_body: 'Success', diff --git a/spec/services/work_items/build_service_spec.rb b/spec/services/work_items/build_service_spec.rb index 6b2e2d8819e..405b4414fc2 100644 --- a/spec/services/work_items/build_service_spec.rb +++ b/spec/services/work_items/build_service_spec.rb @@ -13,7 +13,7 @@ RSpec.describe WorkItems::BuildService do end describe '#execute' do - subject { described_class.new(project: project, current_user: user, params: {}).execute } + subject { described_class.new(container: project, current_user: user, params: {}).execute } it { is_expected.to be_a(::WorkItem) } end diff --git a/spec/services/work_items/create_service_spec.rb b/spec/services/work_items/create_service_spec.rb index 049c90f20b0..1b134c308f2 100644 --- a/spec/services/work_items/create_service_spec.rb +++ b/spec/services/work_items/create_service_spec.rb @@ -29,7 +29,7 @@ RSpec.describe WorkItems::CreateService do describe '#execute' do let(:service) do described_class.new( - project: project, + container: project, current_user: current_user, params: opts, spam_params: spam_params, @@ -118,7 +118,7 @@ RSpec.describe WorkItems::CreateService do let(:service) do described_class.new( - project: project, + container: project, current_user: current_user, params: opts, spam_params: spam_params, @@ -188,7 +188,7 @@ RSpec.describe WorkItems::CreateService do { title: 'Awesome work_item', description: 'please fix', - work_item_type: create(:work_item_type, :task) + work_item_type: WorkItems::Type.default_by_type(:task) } end diff --git a/spec/services/work_items/delete_service_spec.rb b/spec/services/work_items/delete_service_spec.rb index 6cca5018852..69ae881a12f 100644 --- a/spec/services/work_items/delete_service_spec.rb +++ b/spec/services/work_items/delete_service_spec.rb @@ -16,7 +16,7 @@ RSpec.describe WorkItems::DeleteService do end describe '#execute' do - subject(:result) { described_class.new(project: project, current_user: user).execute(work_item) } + subject(:result) { described_class.new(container: project, current_user: user).execute(work_item) } context 'when user can delete the work item' do it { is_expected.to be_success } diff --git a/spec/services/work_items/export_csv_service_spec.rb b/spec/services/work_items/export_csv_service_spec.rb new file mode 100644 index 00000000000..0718d3b686a --- /dev/null +++ b/spec/services/work_items/export_csv_service_spec.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe WorkItems::ExportCsvService, :with_license, feature_category: :team_planning do + let_it_be(:user) { create(:user) } + let_it_be(:group) { create(:group) } + let_it_be(:project) { create(:project, :public, group: group) } + let_it_be(:work_item_1) { create(:work_item, project: project) } + let_it_be(:work_item_2) { create(:work_item, :incident, project: project) } + + subject { described_class.new(WorkItem.all, project) } + + def csv + CSV.parse(subject.csv_data, headers: true) + end + + context 'when import_export_work_items_csv flag is not enabled' do + before do + stub_feature_flags(import_export_work_items_csv: false) + end + + it 'renders an error' do + expect { subject.csv_data }.to raise_error(described_class::NotAvailableError) + end + end + + it 'renders csv to string' do + expect(subject.csv_data).to be_a String + end + + describe '#email' do + # TODO - will be implemented as part of https://gitlab.com/gitlab-org/gitlab/-/issues/379082 + xit 'emails csv' do + expect { subject.email(user) }.o change { ActionMailer::Base.deliveries.count }.from(0).to(1) + end + end + + it 'returns two work items' do + expect(csv.count).to eq(2) + end + + specify 'iid' do + expect(csv[0]['Id']).to eq work_item_1.iid.to_s + end + + specify 'title' do + expect(csv[0]['Title']).to eq work_item_1.title + end + + specify 'type' do + expect(csv[0]['Type']).to eq('Issue') + expect(csv[1]['Type']).to eq('Incident') + end + + specify 'author name' do + expect(csv[0]['Author']).to eq(work_item_1.author_name) + end + + specify 'author username' do + expect(csv[0]['Author Username']).to eq(work_item_1.author.username) + end + + specify 'created_at' do + expect(csv[0]['Created At (UTC)']).to eq(work_item_1.created_at.to_s(:csv)) + end + + it 'preloads fields to avoid N+1 queries' do + control = ActiveRecord::QueryRecorder.new { subject.csv_data } + + create(:work_item, :task, project: project) + + expect { subject.csv_data }.not_to exceed_query_limit(control) + end + + it_behaves_like 'a service that returns invalid fields from selection' +end diff --git a/spec/services/work_items/update_service_spec.rb b/spec/services/work_items/update_service_spec.rb index 87665bcad2c..435995c6570 100644 --- a/spec/services/work_items/update_service_spec.rb +++ b/spec/services/work_items/update_service_spec.rb @@ -22,7 +22,7 @@ RSpec.describe WorkItems::UpdateService do describe '#execute' do let(:service) do described_class.new( - project: project, + container: project, current_user: current_user, params: opts, spam_params: spam_params, @@ -146,7 +146,7 @@ RSpec.describe WorkItems::UpdateService do let(:service) do described_class.new( - project: project, + container: project, current_user: current_user, params: opts, spam_params: spam_params, @@ -362,7 +362,7 @@ RSpec.describe WorkItems::UpdateService do def update_issuable(update_params) described_class.new( - project: project, + container: project, current_user: current_user, params: update_params, spam_params: spam_params, |