diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-06-20 11:10:13 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-06-20 11:10:13 +0000 |
commit | 0ea3fcec397b69815975647f5e2aa5fe944a8486 (patch) | |
tree | 7979381b89d26011bcf9bdc989a40fcc2f1ed4ff /spec/services | |
parent | 72123183a20411a36d607d70b12d57c484394c8e (diff) | |
download | gitlab-ce-0ea3fcec397b69815975647f5e2aa5fe944a8486.tar.gz |
Add latest changes from gitlab-org/gitlab@15-1-stable-eev15.1.0-rc42
Diffstat (limited to 'spec/services')
91 files changed, 2330 insertions, 1089 deletions
diff --git a/spec/services/alert_management/alerts/update_service_spec.rb b/spec/services/alert_management/alerts/update_service_spec.rb index f02607b8174..9bdc9970807 100644 --- a/spec/services/alert_management/alerts/update_service_spec.rb +++ b/spec/services/alert_management/alerts/update_service_spec.rb @@ -291,14 +291,6 @@ RSpec.describe AlertManagement::Alerts::UpdateService do it_behaves_like 'does not sync with the incident status' end - - context 'when feature flag is disabled' do - before do - stub_feature_flags(incident_escalations: false) - end - - it_behaves_like 'does not sync with the incident status' - end end end end diff --git a/spec/services/bulk_create_integration_service_spec.rb b/spec/services/bulk_create_integration_service_spec.rb index 68c5af33fd8..22bb1736f9f 100644 --- a/spec/services/bulk_create_integration_service_spec.rb +++ b/spec/services/bulk_create_integration_service_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' RSpec.describe BulkCreateIntegrationService do - include JiraServiceHelper + include JiraIntegrationHelpers before_all do stub_jira_integration_test @@ -21,7 +21,7 @@ RSpec.describe BulkCreateIntegrationService do ] end - shared_examples 'creates integration from batch ids' do + shared_examples 'creates integration successfully' do def attributes(record) record.reload.attributes.except(*excluded_attributes) end @@ -41,15 +41,31 @@ RSpec.describe BulkCreateIntegrationService do expect(attributes(created_integration.data_fields)) .to eq attributes(integration.data_fields) end + + it 'sets created_at and updated_at timestamps', :freeze_time do + described_class.new(integration, batch, association).execute + + expect(created_integration.data_fields.reload).to have_attributes( + created_at: eq(Time.current), + updated_at: eq(Time.current) + ) + end end - end - shared_examples 'updates inherit_from_id' do it 'updates inherit_from_id attributes' do described_class.new(integration, batch, association).execute expect(created_integration.reload.inherit_from_id).to eq(inherit_from_id) end + + it 'sets created_at and updated_at timestamps', :freeze_time do + described_class.new(integration, batch, association).execute + + expect(created_integration.reload).to have_attributes( + created_at: eq(Time.current), + updated_at: eq(Time.current) + ) + end end context 'passing an instance-level integration' do @@ -62,8 +78,7 @@ RSpec.describe BulkCreateIntegrationService do let(:batch) { Project.where(id: project.id) } let(:association) { 'project' } - it_behaves_like 'creates integration from batch ids' - it_behaves_like 'updates inherit_from_id' + it_behaves_like 'creates integration successfully' end context 'with a group association' do @@ -72,8 +87,7 @@ RSpec.describe BulkCreateIntegrationService do let(:batch) { Group.where(id: group.id) } let(:association) { 'group' } - it_behaves_like 'creates integration from batch ids' - it_behaves_like 'updates inherit_from_id' + it_behaves_like 'creates integration successfully' end end @@ -88,15 +102,13 @@ RSpec.describe BulkCreateIntegrationService do let(:association) { 'project' } let(:inherit_from_id) { integration.id } - it_behaves_like 'creates integration from batch ids' - it_behaves_like 'updates inherit_from_id' + it_behaves_like 'creates integration successfully' context 'with different foreign key of data_fields' do let(:integration) { create(:zentao_integration, :group, group: group) } let(:created_integration) { project.zentao_integration } - it_behaves_like 'creates integration from batch ids' - it_behaves_like 'updates inherit_from_id' + it_behaves_like 'creates integration successfully' end end @@ -108,14 +120,12 @@ RSpec.describe BulkCreateIntegrationService do let(:association) { 'group' } let(:inherit_from_id) { instance_integration.id } - it_behaves_like 'creates integration from batch ids' - it_behaves_like 'updates inherit_from_id' + it_behaves_like 'creates integration successfully' context 'with different foreign key of data_fields' do let(:integration) { create(:zentao_integration, :group, group: group, inherit_from_id: instance_integration.id) } - it_behaves_like 'creates integration from batch ids' - it_behaves_like 'updates inherit_from_id' + it_behaves_like 'creates integration successfully' end end end diff --git a/spec/services/bulk_imports/create_pipeline_trackers_service_spec.rb b/spec/services/bulk_imports/create_pipeline_trackers_service_spec.rb new file mode 100644 index 00000000000..d7b00ba04ab --- /dev/null +++ b/spec/services/bulk_imports/create_pipeline_trackers_service_spec.rb @@ -0,0 +1,168 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe BulkImports::CreatePipelineTrackersService do + describe '#execute!' do + context 'when entity is group' do + it 'creates trackers for group entity' do + bulk_import = create(:bulk_import) + entity = create(:bulk_import_entity, :group_entity, bulk_import: bulk_import) + + described_class.new(entity).execute! + + expect(entity.trackers.to_a).to include( + have_attributes( + stage: 0, status_name: :created, relation: BulkImports::Groups::Pipelines::GroupPipeline.to_s + ), + have_attributes( + stage: 1, status_name: :created, relation: BulkImports::Groups::Pipelines::GroupAttributesPipeline.to_s + ) + ) + end + end + + context 'when entity is project' do + it 'creates trackers for project entity' do + bulk_import = create(:bulk_import) + entity = create(:bulk_import_entity, :project_entity, bulk_import: bulk_import) + + described_class.new(entity).execute! + + expect(entity.trackers.to_a).to include( + have_attributes( + stage: 0, status_name: :created, relation: BulkImports::Projects::Pipelines::ProjectPipeline.to_s + ), + have_attributes( + stage: 1, status_name: :created, relation: BulkImports::Projects::Pipelines::RepositoryPipeline.to_s + ) + ) + end + end + + context 'when tracker configuration has a minimum version defined' do + before do + allow_next_instance_of(BulkImports::Groups::Stage) do |stage| + allow(stage).to receive(:config).and_return( + { + pipeline1: { pipeline: 'PipelineClass1', stage: 0 }, + pipeline2: { pipeline: 'PipelineClass2', stage: 1, minimum_source_version: '14.10.0' }, + pipeline3: { pipeline: 'PipelineClass3', stage: 1, minimum_source_version: '15.0.0' }, + pipeline5: { pipeline: 'PipelineClass4', stage: 1, minimum_source_version: '15.1.0' }, + pipeline6: { pipeline: 'PipelineClass5', stage: 1, minimum_source_version: '16.0.0' } + } + ) + end + end + + context 'when the source instance version is older than the tracker mininum version' do + let_it_be(:bulk_import) { create(:bulk_import, source_version: '15.0.0') } + let_it_be(:entity) { create(:bulk_import_entity, :group_entity, bulk_import: bulk_import) } + + it 'creates trackers as skipped if version requirement does not meet' do + described_class.new(entity).execute! + + expect(entity.trackers.collect { |tracker| [tracker.status_name, tracker.relation] }).to contain_exactly( + [:created, 'PipelineClass1'], + [:created, 'PipelineClass2'], + [:created, 'PipelineClass3'], + [:skipped, 'PipelineClass4'], + [:skipped, 'PipelineClass5'] + ) + end + + it 'logs an info message for the skipped pipelines' do + expect_next_instance_of(Gitlab::Import::Logger) do |logger| + expect(logger).to receive(:info).with({ + message: 'Pipeline skipped as source instance version not compatible with pipeline', + entity_id: entity.id, + pipeline_name: 'PipelineClass4', + minimum_source_version: '15.1.0', + maximum_source_version: nil, + source_version: '15.0.0' + }) + + expect(logger).to receive(:info).with({ + message: 'Pipeline skipped as source instance version not compatible with pipeline', + entity_id: entity.id, + pipeline_name: 'PipelineClass5', + minimum_source_version: '16.0.0', + maximum_source_version: nil, + source_version: '15.0.0' + }) + end + + described_class.new(entity).execute! + end + end + + context 'when the source instance version is undefined' do + it 'creates trackers as created' do + bulk_import = create(:bulk_import, source_version: nil) + entity = create(:bulk_import_entity, :group_entity, bulk_import: bulk_import) + + described_class.new(entity).execute! + + expect(entity.trackers.collect { |tracker| [tracker.status_name, tracker.relation] }).to contain_exactly( + [:created, 'PipelineClass1'], + [:created, 'PipelineClass2'], + [:created, 'PipelineClass3'], + [:created, 'PipelineClass4'], + [:created, 'PipelineClass5'] + ) + end + end + end + + context 'when tracker configuration has a maximum version defined' do + before do + allow_next_instance_of(BulkImports::Groups::Stage) do |stage| + allow(stage).to receive(:config).and_return( + { + pipeline1: { pipeline: 'PipelineClass1', stage: 0 }, + pipeline2: { pipeline: 'PipelineClass2', stage: 1, maximum_source_version: '14.10.0' }, + pipeline3: { pipeline: 'PipelineClass3', stage: 1, maximum_source_version: '15.0.0' }, + pipeline5: { pipeline: 'PipelineClass4', stage: 1, maximum_source_version: '15.1.0' }, + pipeline6: { pipeline: 'PipelineClass5', stage: 1, maximum_source_version: '16.0.0' } + } + ) + end + end + + context 'when the source instance version is older than the tracker maximum version' do + it 'creates trackers as skipped if version requirement does not meet' do + bulk_import = create(:bulk_import, source_version: '15.0.0') + entity = create(:bulk_import_entity, :group_entity, bulk_import: bulk_import) + + described_class.new(entity).execute! + + expect(entity.trackers.collect { |tracker| [tracker.status_name, tracker.relation] }).to contain_exactly( + [:created, 'PipelineClass1'], + [:skipped, 'PipelineClass2'], + [:created, 'PipelineClass3'], + [:created, 'PipelineClass4'], + [:created, 'PipelineClass5'] + ) + end + end + + context 'when the source instance version is a patch version' do + it 'creates trackers with the same status as the non-patch source version' do + bulk_import_1 = create(:bulk_import, source_version: '15.0.1') + entity_1 = create(:bulk_import_entity, :group_entity, bulk_import: bulk_import_1) + + bulk_import_2 = create(:bulk_import, source_version: '15.0.0') + entity_2 = create(:bulk_import_entity, :group_entity, bulk_import: bulk_import_2) + + described_class.new(entity_1).execute! + described_class.new(entity_2).execute! + + trackers_1 = entity_1.trackers.collect { |tracker| [tracker.status_name, tracker.relation] } + trackers_2 = entity_2.trackers.collect { |tracker| [tracker.status_name, tracker.relation] } + + expect(trackers_1).to eq(trackers_2) + end + end + end + end +end diff --git a/spec/services/bulk_imports/file_export_service_spec.rb b/spec/services/bulk_imports/file_export_service_spec.rb index 94efceff6c6..453fc1d0c0d 100644 --- a/spec/services/bulk_imports/file_export_service_spec.rb +++ b/spec/services/bulk_imports/file_export_service_spec.rb @@ -4,39 +4,34 @@ require 'spec_helper' RSpec.describe BulkImports::FileExportService do let_it_be(:project) { create(:project) } - let_it_be(:export_path) { Dir.mktmpdir } - let_it_be(:relation) { BulkImports::FileTransfer::BaseConfig::UPLOADS_RELATION } - - subject(:service) { described_class.new(project, export_path, relation) } describe '#execute' do - it 'executes export service and archives exported data' do - expect_next_instance_of(BulkImports::UploadsExportService) do |service| - expect(service).to receive(:execute) - end + it 'executes export service and archives exported data for each file relation' do + relations = { + 'uploads' => BulkImports::UploadsExportService, + 'lfs_objects' => BulkImports::LfsObjectsExportService, + 'repository' => BulkImports::RepositoryBundleExportService, + 'design' => BulkImports::RepositoryBundleExportService + } - expect(subject).to receive(:tar_cf).with(archive: File.join(export_path, 'uploads.tar'), dir: export_path) + relations.each do |relation, klass| + Dir.mktmpdir do |export_path| + service = described_class.new(project, export_path, relation) - subject.execute - end + expect_next_instance_of(klass) do |service| + expect(service).to receive(:execute) + end - context 'when relation is lfs objects' do - let_it_be(:relation) { BulkImports::FileTransfer::ProjectConfig::LFS_OBJECTS_RELATION } + expect(service).to receive(:tar_cf).with(archive: File.join(export_path, "#{relation}.tar"), dir: export_path) - it 'executes lfs objects export service' do - expect_next_instance_of(BulkImports::LfsObjectsExportService) do |service| - expect(service).to receive(:execute) + service.execute end - - expect(subject).to receive(:tar_cf).with(archive: File.join(export_path, 'lfs_objects.tar'), dir: export_path) - - subject.execute end end context 'when unsupported relation is passed' do it 'raises an error' do - service = described_class.new(project, export_path, 'unsupported') + service = described_class.new(project, nil, 'unsupported') expect { service.execute }.to raise_error(BulkImports::Error, 'Unsupported relation export type') end @@ -45,7 +40,9 @@ RSpec.describe BulkImports::FileExportService do describe '#exported_filename' do it 'returns filename of the exported file' do - expect(subject.exported_filename).to eq('uploads.tar') + service = described_class.new(project, nil, 'uploads') + + expect(service.exported_filename).to eq('uploads.tar') end end end diff --git a/spec/services/bulk_imports/lfs_objects_export_service_spec.rb b/spec/services/bulk_imports/lfs_objects_export_service_spec.rb index 5ae54ed309b..894789c7941 100644 --- a/spec/services/bulk_imports/lfs_objects_export_service_spec.rb +++ b/spec/services/bulk_imports/lfs_objects_export_service_spec.rb @@ -53,6 +53,18 @@ RSpec.describe BulkImports::LfsObjectsExportService do ) end + context 'when lfs object has file on disk missing' do + it 'does not attempt to copy non-existent file' do + FileUtils.rm(lfs_object.file.path) + + expect(service).not_to receive(:copy_files) + + service.execute + + expect(File).not_to exist(File.join(export_path, lfs_object.oid)) + end + end + context 'when lfs object is remotely stored' do let(:lfs_object) { create(:lfs_object, :object_storage) } diff --git a/spec/services/bulk_imports/repository_bundle_export_service_spec.rb b/spec/services/bulk_imports/repository_bundle_export_service_spec.rb new file mode 100644 index 00000000000..a7d98a7474a --- /dev/null +++ b/spec/services/bulk_imports/repository_bundle_export_service_spec.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe BulkImports::RepositoryBundleExportService do + let(:project) { create(:project) } + let(:export_path) { Dir.mktmpdir } + + subject(:service) { described_class.new(repository, export_path, export_filename) } + + after do + FileUtils.remove_entry(export_path) if Dir.exist?(export_path) + end + + describe '#execute' do + shared_examples 'repository export' do + context 'when repository exists' do + it 'bundles repository to disk' do + allow(repository).to receive(:exists?).and_return(true) + expect(repository).to receive(:bundle_to_disk).with(File.join(export_path, "#{export_filename}.bundle")) + + service.execute + end + end + + context 'when repository does not exist' do + it 'does not bundle repository to disk' do + allow(repository).to receive(:exists?).and_return(false) + expect(repository).not_to receive(:bundle_to_disk) + + service.execute + end + end + end + + include_examples 'repository export' do + let(:repository) { project.repository } + let(:export_filename) { 'repository' } + end + + include_examples 'repository export' do + let(:repository) { project.design_repository } + let(:export_filename) { 'design' } + end + end +end diff --git a/spec/services/bulk_update_integration_service_spec.rb b/spec/services/bulk_update_integration_service_spec.rb index dcc8d2df36d..e3e38aacaa2 100644 --- a/spec/services/bulk_update_integration_service_spec.rb +++ b/spec/services/bulk_update_integration_service_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' RSpec.describe BulkUpdateIntegrationService do - include JiraServiceHelper + include JiraIntegrationHelpers before_all do stub_jira_integration_test @@ -55,6 +55,20 @@ RSpec.describe BulkUpdateIntegrationService do .not_to eq(subgroup_integration.attributes.except(*excluded_attributes)) end + it 'does not change the created_at timestamp' do + subgroup_integration.update_column(:created_at, Time.utc('2022-01-01')) + + expect do + described_class.new(subgroup_integration, batch).execute + end.not_to change { integration.reload.created_at } + end + + it 'sets the updated_at timestamp to the current time', time_travel_to: Time.utc('2022-01-01') do + expect do + described_class.new(subgroup_integration, batch).execute + end.to change { integration.reload.updated_at }.to(Time.current) + end + context 'with integration with data fields' do let(:excluded_attributes) do %w[id service_id created_at updated_at encrypted_properties encrypted_properties_iv] @@ -69,6 +83,20 @@ RSpec.describe BulkUpdateIntegrationService do expect(integration.data_fields.attributes.except(*excluded_attributes)) .not_to eq(excluded_integration.data_fields.attributes.except(*excluded_attributes)) end + + it 'does not change the created_at timestamp' do + subgroup_integration.data_fields.update_column(:created_at, Time.utc('2022-01-02')) + + expect do + described_class.new(subgroup_integration, batch).execute + end.not_to change { integration.data_fields.reload.created_at } + end + + it 'sets the updated_at timestamp to the current time', time_travel_to: Time.utc('2022-01-01') do + expect do + described_class.new(subgroup_integration, batch).execute + end.to change { integration.data_fields.reload.updated_at }.to(Time.current) + end end end diff --git a/spec/services/ci/abort_pipelines_service_spec.rb b/spec/services/ci/abort_pipelines_service_spec.rb index db25faff70f..9f9519d6829 100644 --- a/spec/services/ci/abort_pipelines_service_spec.rb +++ b/spec/services/ci/abort_pipelines_service_spec.rb @@ -94,28 +94,5 @@ RSpec.describe Ci::AbortPipelinesService do end end end - - context 'with user pipelines' do - def abort_user_pipelines - described_class.new.execute(user.pipelines, :user_blocked) - end - - it 'fails all running pipelines and related jobs' do - expect(abort_user_pipelines).to be_success - - expect_correct_cancellations - - expect(other_users_pipeline.status).not_to eq('failed') - end - - it 'avoids N+1 queries' do - control_count = ActiveRecord::QueryRecorder.new { abort_user_pipelines }.count - - pipelines = create_list(:ci_pipeline, 5, :running, project: project, user: user) - create_list(:ci_build, 5, :running, pipeline: pipelines.first) - - expect { abort_user_pipelines }.not_to exceed_query_limit(control_count) - end - end end end diff --git a/spec/services/ci/after_requeue_job_service_spec.rb b/spec/services/ci/after_requeue_job_service_spec.rb index c9bd44f78e2..fb67ee18fb2 100644 --- a/spec/services/ci/after_requeue_job_service_spec.rb +++ b/spec/services/ci/after_requeue_job_service_spec.rb @@ -26,6 +26,11 @@ RSpec.describe Ci::AfterRequeueJobService, :sidekiq_inline do script: exit 0 needs: [a1] + a3: + stage: a + script: exit 0 + needs: [a2] + b1: stage: b script: exit 0 @@ -59,6 +64,7 @@ RSpec.describe Ci::AfterRequeueJobService, :sidekiq_inline do check_jobs_statuses( a1: 'pending', a2: 'created', + a3: 'created', b1: 'pending', b2: 'created', c1: 'created', @@ -69,6 +75,7 @@ RSpec.describe Ci::AfterRequeueJobService, :sidekiq_inline do check_jobs_statuses( a1: 'pending', a2: 'created', + a3: 'created', b1: 'success', b2: 'created', c1: 'created', @@ -79,6 +86,7 @@ RSpec.describe Ci::AfterRequeueJobService, :sidekiq_inline do check_jobs_statuses( a1: 'failed', a2: 'skipped', + a3: 'skipped', b1: 'success', b2: 'skipped', c1: 'skipped', @@ -90,6 +98,7 @@ RSpec.describe Ci::AfterRequeueJobService, :sidekiq_inline do check_jobs_statuses( a1: 'pending', a2: 'skipped', + a3: 'skipped', b1: 'success', b2: 'skipped', c1: 'skipped', @@ -103,12 +112,42 @@ RSpec.describe Ci::AfterRequeueJobService, :sidekiq_inline do check_jobs_statuses( a1: 'pending', a2: 'created', + a3: 'skipped', b1: 'success', b2: 'created', c1: 'created', c2: 'created' ) end + + context 'when executed by a different user than the original owner' do + let(:retryer) { create(:user).tap { |u| project.add_maintainer(u) } } + let(:service) { described_class.new(project, retryer) } + + it 'reassigns jobs with updated statuses to the retryer' do + expect(jobs_name_status_owner_needs).to contain_exactly( + { 'name' => 'a1', 'status' => 'pending', 'user_id' => user.id, 'needs' => [] }, + { 'name' => 'a2', 'status' => 'skipped', 'user_id' => user.id, 'needs' => ['a1'] }, + { 'name' => 'a3', 'status' => 'skipped', 'user_id' => user.id, 'needs' => ['a2'] }, + { 'name' => 'b1', 'status' => 'success', 'user_id' => user.id, 'needs' => [] }, + { 'name' => 'b2', 'status' => 'skipped', 'user_id' => user.id, 'needs' => ['a2'] }, + { 'name' => 'c1', 'status' => 'skipped', 'user_id' => user.id, 'needs' => ['b2'] }, + { 'name' => 'c2', 'status' => 'skipped', 'user_id' => user.id, 'needs' => [] } + ) + + execute_after_requeue_service(a1) + + expect(jobs_name_status_owner_needs).to contain_exactly( + { 'name' => 'a1', 'status' => 'pending', 'user_id' => user.id, 'needs' => [] }, + { 'name' => 'a2', 'status' => 'created', 'user_id' => retryer.id, 'needs' => ['a1'] }, + { 'name' => 'a3', 'status' => 'skipped', 'user_id' => user.id, 'needs' => ['a2'] }, + { 'name' => 'b1', 'status' => 'success', 'user_id' => user.id, 'needs' => [] }, + { 'name' => 'b2', 'status' => 'created', 'user_id' => retryer.id, 'needs' => ['a2'] }, + { 'name' => 'c1', 'status' => 'created', 'user_id' => retryer.id, 'needs' => ['b2'] }, + { 'name' => 'c2', 'status' => 'created', 'user_id' => retryer.id, 'needs' => [] } + ) + end + end end context 'stage-dag mixed pipeline with some same-stage needs' do @@ -212,6 +251,12 @@ RSpec.describe Ci::AfterRequeueJobService, :sidekiq_inline do pipeline.processables.latest end + def jobs_name_status_owner_needs + processables.reload.map do |job| + job.attributes.slice('name', 'status', 'user_id').merge('needs' => job.needs.map(&:name)) + end + end + def execute_after_requeue_service(processable) service.execute(processable) end diff --git a/spec/services/ci/create_pipeline_service/rate_limit_spec.rb b/spec/services/ci/create_pipeline_service/rate_limit_spec.rb index caea165cc6c..0000296230f 100644 --- a/spec/services/ci/create_pipeline_service/rate_limit_spec.rb +++ b/spec/services/ci/create_pipeline_service/rate_limit_spec.rb @@ -10,10 +10,8 @@ RSpec.describe Ci::CreatePipelineService, :freeze_time, :clean_gitlab_redis_rate before do stub_ci_pipeline_yaml_file(gitlab_ci_yaml) - stub_feature_flags(ci_throttle_pipelines_creation_dry_run: false) - - allow(Gitlab::ApplicationRateLimiter).to receive(:rate_limits) - .and_return(pipelines_create: { threshold: 1, interval: 1.minute }) + stub_application_setting(pipeline_limit_per_project_user_sha: 1) + stub_feature_flags(ci_enforce_throttle_pipelines_creation_override: false) end context 'when user is under the limit' do diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb index c39a76ad2fc..aac059f2104 100644 --- a/spec/services/ci/create_pipeline_service_spec.rb +++ b/spec/services/ci/create_pipeline_service_spec.rb @@ -12,10 +12,6 @@ RSpec.describe Ci::CreatePipelineService do before do stub_ci_pipeline_to_return_yaml_file - - # Disable rate limiting for pipeline creation - allow(Gitlab::ApplicationRateLimiter).to receive(:rate_limits) - .and_return(pipelines_create: { threshold: 0, interval: 1.minute }) end describe '#execute' do @@ -1541,11 +1537,12 @@ RSpec.describe Ci::CreatePipelineService do expect(pipeline.target_sha).to be_nil end - it 'schedules update for the head pipeline of the merge request', :sidekiq_inline do - expect(UpdateHeadPipelineForMergeRequestWorker) - .to receive(:perform_async).with(merge_request.id) + it 'schedules update for the head pipeline of the merge request' do + allow(MergeRequests::UpdateHeadPipelineWorker).to receive(:perform_async) pipeline + + expect(MergeRequests::UpdateHeadPipelineWorker).to have_received(:perform_async).with('Ci::PipelineCreatedEvent', { 'pipeline_id' => pipeline.id }) end it 'schedules a namespace onboarding create action worker' do 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 1c6963e4a31..4f7663d7996 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 @@ -99,6 +99,16 @@ RSpec.describe Ci::JobArtifacts::DestroyAllExpiredService, :clean_gitlab_redis_s expect { subject }.not_to change { artifact.file.exists? } end end + + context 'when the project in which the arfifact belongs to is undergoing stats refresh' do + before do + create(:project_build_artifacts_size_refresh, :pending, project: artifact.project) + end + + it 'does not destroy job artifact' do + expect { subject }.not_to change { Ci::JobArtifact.count } + end + end end context 'when artifact is locked' do diff --git a/spec/services/ci/job_artifacts/destroy_batch_service_spec.rb b/spec/services/ci/job_artifacts/destroy_batch_service_spec.rb index 5e77041a632..3a04a3af03e 100644 --- a/spec/services/ci/job_artifacts/destroy_batch_service_spec.rb +++ b/spec/services/ci/job_artifacts/destroy_batch_service_spec.rb @@ -4,7 +4,14 @@ require 'spec_helper' RSpec.describe Ci::JobArtifacts::DestroyBatchService do let(:artifacts) { Ci::JobArtifact.where(id: [artifact_with_file.id, artifact_without_file.id, trace_artifact.id]) } - let(:service) { described_class.new(artifacts, pick_up_at: Time.current) } + let(:skip_projects_on_refresh) { false } + let(:service) do + described_class.new( + artifacts, + pick_up_at: Time.current, + skip_projects_on_refresh: skip_projects_on_refresh + ) + end let_it_be(:artifact_with_file, refind: true) do create(:ci_job_artifact, :zip) @@ -52,6 +59,128 @@ RSpec.describe Ci::JobArtifacts::DestroyBatchService do .and not_change { Ci::JobArtifact.exists?(trace_artifact.id) } end + context 'when artifact belongs to a project that is undergoing stats refresh' do + let!(:artifact_under_refresh_1) do + create(:ci_job_artifact, :zip) + end + + let!(:artifact_under_refresh_2) do + create(:ci_job_artifact, :zip) + end + + let!(:artifact_under_refresh_3) do + create(:ci_job_artifact, :zip, project: artifact_under_refresh_2.project) + end + + let(:artifacts) do + Ci::JobArtifact.where(id: [artifact_with_file.id, artifact_under_refresh_1.id, artifact_under_refresh_2.id, + artifact_under_refresh_3.id]) + end + + before do + create(:project_build_artifacts_size_refresh, :created, project: artifact_with_file.project) + create(:project_build_artifacts_size_refresh, :pending, project: artifact_under_refresh_1.project) + create(:project_build_artifacts_size_refresh, :running, project: artifact_under_refresh_2.project) + end + + shared_examples 'avoiding N+1 queries' do + let!(:control_artifact_on_refresh) do + create(:ci_job_artifact, :zip) + end + + let!(:control_artifact_non_refresh) do + create(:ci_job_artifact, :zip) + end + + let!(:other_artifact_on_refresh) do + create(:ci_job_artifact, :zip) + end + + let!(:other_artifact_on_refresh_2) do + create(:ci_job_artifact, :zip) + end + + let!(:other_artifact_non_refresh) do + create(:ci_job_artifact, :zip) + end + + let!(:control_artifacts) do + Ci::JobArtifact.where( + id: [ + control_artifact_on_refresh.id, + control_artifact_non_refresh.id + ] + ) + end + + let!(:artifacts) do + Ci::JobArtifact.where( + id: [ + other_artifact_on_refresh.id, + other_artifact_on_refresh_2.id, + other_artifact_non_refresh.id + ] + ) + end + + let(:control_service) do + described_class.new( + control_artifacts, + pick_up_at: Time.current, + skip_projects_on_refresh: skip_projects_on_refresh + ) + end + + before do + create(:project_build_artifacts_size_refresh, :pending, project: control_artifact_on_refresh.project) + create(:project_build_artifacts_size_refresh, :pending, project: other_artifact_on_refresh.project) + create(:project_build_artifacts_size_refresh, :pending, project: other_artifact_on_refresh_2.project) + end + + it 'does not make multiple queries when fetching multiple project refresh records' do + control = ActiveRecord::QueryRecorder.new { control_service.execute } + + expect { subject }.not_to exceed_query_limit(control) + end + end + + context 'and skip_projects_on_refresh is set to false (default)' do + it 'logs the projects undergoing refresh and continues with the delete', :aggregate_failures do + expect(Gitlab::ProjectStatsRefreshConflictsLogger).to receive(:warn_artifact_deletion_during_stats_refresh).with( + method: 'Ci::JobArtifacts::DestroyBatchService#execute', + project_id: artifact_under_refresh_1.project.id + ).once + + expect(Gitlab::ProjectStatsRefreshConflictsLogger).to receive(:warn_artifact_deletion_during_stats_refresh).with( + method: 'Ci::JobArtifacts::DestroyBatchService#execute', + project_id: artifact_under_refresh_2.project.id + ).once + + expect { subject }.to change { Ci::JobArtifact.count }.by(-4) + end + + it_behaves_like 'avoiding N+1 queries' + end + + context 'and skip_projects_on_refresh is set to true' do + let(:skip_projects_on_refresh) { true } + + it 'logs the projects undergoing refresh and excludes the artifacts from deletion', :aggregate_failures do + expect(Gitlab::ProjectStatsRefreshConflictsLogger).to receive(:warn_skipped_artifact_deletion_during_stats_refresh).with( + method: 'Ci::JobArtifacts::DestroyBatchService#execute', + project_ids: match_array([artifact_under_refresh_1.project.id, artifact_under_refresh_2.project.id]) + ) + + expect { subject }.to change { Ci::JobArtifact.count }.by(-1) + expect(Ci::JobArtifact.where(id: artifact_under_refresh_1.id)).to exist + expect(Ci::JobArtifact.where(id: artifact_under_refresh_2.id)).to exist + expect(Ci::JobArtifact.where(id: artifact_under_refresh_3.id)).to exist + end + + it_behaves_like 'avoiding N+1 queries' + end + end + context 'ProjectStatistics' do it 'resets project statistics' do expect(ProjectStatistics).to receive(:increment_statistic).once diff --git a/spec/services/ci/pipeline_artifacts/coverage_report_service_spec.rb b/spec/services/ci/pipeline_artifacts/coverage_report_service_spec.rb index 98b01e2b303..403afde5da3 100644 --- a/spec/services/ci/pipeline_artifacts/coverage_report_service_spec.rb +++ b/spec/services/ci/pipeline_artifacts/coverage_report_service_spec.rb @@ -4,17 +4,14 @@ require 'spec_helper' RSpec.describe ::Ci::PipelineArtifacts::CoverageReportService do describe '#execute' do - subject { described_class.new.execute(pipeline) } + let_it_be(:project) { create(:project, :repository) } - context 'when pipeline has coverage reports' do - let(:project) { create(:project, :repository) } - let(:pipeline) { create(:ci_pipeline, :with_coverage_reports, project: project) } + subject { described_class.new(pipeline).execute } + shared_examples 'creating a pipeline coverage report' do context 'when pipeline is finished' do it 'creates a pipeline artifact' do - subject - - expect(Ci::PipelineArtifact.count).to eq(1) + expect { subject }.to change { Ci::PipelineArtifact.count }.from(0).to(1) end it 'persists the default file name' do @@ -37,21 +34,32 @@ RSpec.describe ::Ci::PipelineArtifacts::CoverageReportService do end context 'when pipeline artifact has already been created' do - it 'do not raise an error and do not persist the same artifact twice' do - expect { 2.times { described_class.new.execute(pipeline) } }.not_to raise_error + it 'does not raise an error and does not persist the same artifact twice' do + expect { 2.times { described_class.new(pipeline).execute } }.not_to raise_error expect(Ci::PipelineArtifact.count).to eq(1) end end end + context 'when pipeline has coverage report' do + let!(:pipeline) { create(:ci_pipeline, :with_coverage_reports, project: project) } + + it_behaves_like 'creating a pipeline coverage report' + end + + context 'when pipeline has coverage report from child pipeline' do + let!(:pipeline) { create(:ci_pipeline, :success, project: project) } + let!(:child_pipeline) { create(:ci_pipeline, :with_coverage_reports, project: project, child_of: pipeline) } + + it_behaves_like 'creating a pipeline coverage report' + end + context 'when pipeline is running and coverage report does not exist' do let(:pipeline) { create(:ci_pipeline, :running) } it 'does not persist data' do - subject - - expect(Ci::PipelineArtifact.count).to eq(0) + expect { subject }.not_to change { Ci::PipelineArtifact.count } end end end diff --git a/spec/services/ci/process_sync_events_service_spec.rb b/spec/services/ci/process_sync_events_service_spec.rb index 6b9717fe57d..241ac4995ff 100644 --- a/spec/services/ci/process_sync_events_service_spec.rb +++ b/spec/services/ci/process_sync_events_service_spec.rb @@ -137,10 +137,9 @@ RSpec.describe Ci::ProcessSyncEventsService do end end - context 'when the FFs sync_traversal_ids, use_traversal_ids and use_traversal_ids_for_ancestors are disabled' do + context 'when the FFs use_traversal_ids and use_traversal_ids_for_ancestors are disabled' do before do - stub_feature_flags(sync_traversal_ids: false, - use_traversal_ids: false, + stub_feature_flags(use_traversal_ids: false, use_traversal_ids_for_ancestors: false) end diff --git a/spec/services/clusters/applications/schedule_update_service_spec.rb b/spec/services/clusters/applications/schedule_update_service_spec.rb deleted file mode 100644 index 2cbcb861938..00000000000 --- a/spec/services/clusters/applications/schedule_update_service_spec.rb +++ /dev/null @@ -1,63 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Clusters::Applications::ScheduleUpdateService do - describe '#execute' do - let(:project) { create(:project) } - - around do |example| - freeze_time { example.run } - end - - context 'when the application is a Clusters::Integrations::Prometheus' do - let(:application) { create(:clusters_integrations_prometheus) } - - it 'does nothing' do - service = described_class.new(application, project) - - expect(::ClusterUpdateAppWorker).not_to receive(:perform_in) - expect(::ClusterUpdateAppWorker).not_to receive(:perform_async) - - service.execute - end - end - - context 'when the application is externally installed' do - let(:application) { create(:clusters_applications_prometheus, :externally_installed) } - - it 'does nothing' do - service = described_class.new(application, project) - - expect(::ClusterUpdateAppWorker).not_to receive(:perform_in) - expect(::ClusterUpdateAppWorker).not_to receive(:perform_async) - - service.execute - end - end - - context 'when application is able to be updated' do - context 'when the application was recently scheduled' do - it 'schedules worker with a backoff delay' do - application = create(:clusters_applications_prometheus, :installed, last_update_started_at: Time.current + 5.minutes) - service = described_class.new(application, project) - - expect(::ClusterUpdateAppWorker).to receive(:perform_in).with(described_class::BACKOFF_DELAY, application.name, application.id, project.id, Time.current).once - - service.execute - end - end - - context 'when the application has not been recently updated' do - it 'schedules worker' do - application = create(:clusters_applications_prometheus, :installed) - service = described_class.new(application, project) - - expect(::ClusterUpdateAppWorker).to receive(:perform_async).with(application.name, application.id, project.id, Time.current).once - - service.execute - end - end - end - end -end diff --git a/spec/services/deployments/create_service_spec.rb b/spec/services/deployments/create_service_spec.rb index 0f2a6ce32e1..f6f4c68a6f1 100644 --- a/spec/services/deployments/create_service_spec.rb +++ b/spec/services/deployments/create_service_spec.rb @@ -21,11 +21,34 @@ RSpec.describe Deployments::CreateService do expect(Deployments::UpdateEnvironmentWorker).to receive(:perform_async) expect(Deployments::LinkMergeRequestWorker).to receive(:perform_async) - expect(Deployments::HooksWorker).to receive(:perform_async) + expect_next_instance_of(Deployment) do |deployment| + expect(deployment).to receive(:execute_hooks) + end expect(service.execute).to be_persisted end + context 'when `deployment_hooks_skip_worker` flag is disabled' do + before do + stub_feature_flags(deployment_hooks_skip_worker: false) + end + + it 'executes Deployments::HooksWorker asynchronously' do + service = described_class.new( + environment, + user, + sha: 'b83d6e391c22777fca1ed3012fce84f633d7fed0', + ref: 'master', + tag: false, + status: 'success' + ) + + expect(Deployments::HooksWorker).to receive(:perform_async) + + service.execute + end + end + it 'does not change the status if no status is given' do service = described_class.new( environment, @@ -37,7 +60,9 @@ RSpec.describe Deployments::CreateService do expect(Deployments::UpdateEnvironmentWorker).not_to receive(:perform_async) expect(Deployments::LinkMergeRequestWorker).not_to receive(:perform_async) - expect(Deployments::HooksWorker).not_to receive(:perform_async) + expect_next_instance_of(Deployment) do |deployment| + expect(deployment).not_to receive(:execute_hooks) + end expect(service.execute).to be_persisted end @@ -55,11 +80,9 @@ RSpec.describe Deployments::CreateService do it 'does not create a new deployment' do described_class.new(environment, user, params).execute - expect(Deployments::UpdateEnvironmentWorker).not_to receive(:perform_async) - expect(Deployments::LinkMergeRequestWorker).not_to receive(:perform_async) - expect(Deployments::HooksWorker).not_to receive(:perform_async) - - described_class.new(environment.reload, user, params).execute + expect do + described_class.new(environment.reload, user, params).execute + end.not_to change { Deployment.count } end end end diff --git a/spec/services/deployments/update_environment_service_spec.rb b/spec/services/deployments/update_environment_service_spec.rb index 0859aa2c9d1..e2d7a80fde3 100644 --- a/spec/services/deployments/update_environment_service_spec.rb +++ b/spec/services/deployments/update_environment_service_spec.rb @@ -33,7 +33,7 @@ RSpec.describe Deployments::UpdateEnvironmentService do before do allow(Deployments::LinkMergeRequestWorker).to receive(:perform_async) - allow(Deployments::HooksWorker).to receive(:perform_async) + allow(deployment).to receive(:execute_hooks) job.success! # Create/Succeed deployment end @@ -84,7 +84,7 @@ RSpec.describe Deployments::UpdateEnvironmentService do context 'and environment is stopped' do before do - environment.stop + environment.stop_complete end it 'makes environment available' do diff --git a/spec/services/emails/confirm_service_spec.rb b/spec/services/emails/confirm_service_spec.rb index d3a745bc744..e8d3c0d673b 100644 --- a/spec/services/emails/confirm_service_spec.rb +++ b/spec/services/emails/confirm_service_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' RSpec.describe Emails::ConfirmService do - let(:user) { create(:user) } + let_it_be(:user) { create(:user) } subject(:service) { described_class.new(user) } @@ -11,7 +11,9 @@ RSpec.describe Emails::ConfirmService do it 'enqueues a background job to send confirmation email again' do email = user.emails.create!(email: 'new@email.com') - expect { service.execute(email) }.to have_enqueued_job.on_queue('mailers') + travel_to(10.minutes.from_now) do + expect { service.execute(email) }.to have_enqueued_job.on_queue('mailers') + end end end end diff --git a/spec/services/environments/auto_stop_service_spec.rb b/spec/services/environments/auto_stop_service_spec.rb index 8dad59cbefd..d688690c376 100644 --- a/spec/services/environments/auto_stop_service_spec.rb +++ b/spec/services/environments/auto_stop_service_spec.rb @@ -37,7 +37,7 @@ RSpec.describe Environments::AutoStopService, :clean_gitlab_redis_shared_state, it 'stops environments and play stop jobs' do expect { subject } .to change { Environment.all.map(&:state).uniq } - .from(['available']).to(['stopped']) + .from(['available']).to(['stopping']) expect(Ci::Build.where(name: 'stop_review_app').map(&:status).uniq).to eq(['pending']) end diff --git a/spec/services/environments/stop_service_spec.rb b/spec/services/environments/stop_service_spec.rb index afbc0ba70f9..3ed8a0b1da0 100644 --- a/spec/services/environments/stop_service_spec.rb +++ b/spec/services/environments/stop_service_spec.rb @@ -29,14 +29,27 @@ RSpec.describe Environments::StopService do review_job.success! end - it 'stops the environment' do - expect { subject }.to change { environment.reload.state }.from('available').to('stopped') + context 'without stop action' do + let!(:environment) { create(:environment, :available, project: project) } + + it 'stops the environment' do + expect { subject }.to change { environment.reload.state }.from('available').to('stopped') + end end it 'plays the stop action' do expect { subject }.to change { stop_review_job.reload.status }.from('manual').to('pending') end + context 'force option' do + let(:service) { described_class.new(project, user, { force: true }) } + + it 'does not play the stop action when forced' do + expect { subject }.to change { environment.reload.state }.from('available').to('stopped') + expect(stop_review_job.reload.status).to eq('manual') + end + end + context 'when an environment has already been stopped' do let!(:environment) { create(:environment, :stopped, project: project) } @@ -65,11 +78,6 @@ RSpec.describe Environments::StopService do describe '#execute_for_branch' do context 'when environment with review app exists' do - before do - create(:environment, :with_review_app, project: project, - ref: 'feature') - end - context 'when user has permission to stop environment' do before do project.add_developer(user) @@ -77,25 +85,25 @@ RSpec.describe Environments::StopService do context 'when environment is associated with removed branch' do it 'stops environment' do - expect_environment_stopped_on('feature') + expect_environment_stopping_on('feature', feature_environment) end end context 'when environment is associated with different branch' do it 'does not stop environment' do - expect_environment_not_stopped_on('master') + expect_environment_not_stopped_on('master', feature_environment) end end context 'when specified branch does not exist' do it 'does not stop environment' do - expect_environment_not_stopped_on('non/existent/branch') + expect_environment_not_stopped_on('non/existent/branch', feature_environment) end end context 'when no branch not specified' do it 'does not stop environment' do - expect_environment_not_stopped_on(nil) + expect_environment_not_stopped_on(nil, feature_environment) end end @@ -107,7 +115,7 @@ RSpec.describe Environments::StopService do end it 'does not stop environment' do - expect_environment_not_stopped_on('feature') + expect_environment_not_stopped_on('feature', feature_environment) end end end @@ -119,7 +127,7 @@ RSpec.describe Environments::StopService do end it 'does not stop environment' do - expect_environment_not_stopped_on('master') + expect_environment_not_stopped_on('master', feature_environment) end end end @@ -132,7 +140,7 @@ RSpec.describe Environments::StopService do end it 'does not stop environment' do - expect_environment_not_stopped_on('master') + expect_environment_not_stopped_on('master', feature_environment) end end end @@ -148,7 +156,7 @@ RSpec.describe Environments::StopService do end it 'does not stop environment' do - expect_environment_not_stopped_on('master') + expect_environment_not_stopped_on('master', feature_environment) end end end @@ -177,7 +185,7 @@ RSpec.describe Environments::StopService do merge_requests_as_head_pipeline: [merge_request]) end - let!(:review_job) { create(:ci_build, :with_deployment, :start_review_app, pipeline: pipeline, project: project) } + let!(:review_job) { create(:ci_build, :with_deployment, :start_review_app, :success, pipeline: pipeline, project: project) } let!(:stop_review_job) { create(:ci_build, :with_deployment, :stop_review_app, :manual, pipeline: pipeline, project: project) } before do @@ -195,8 +203,7 @@ RSpec.describe Environments::StopService do it 'stops the active environment' do subject - - expect(pipeline.environments_in_self_and_descendants.first).to be_stopped + expect(pipeline.environments_in_self_and_descendants.first).to be_stopping end context 'when pipeline is a branch pipeline for merge request' do @@ -263,13 +270,22 @@ RSpec.describe Environments::StopService do end end - def expect_environment_stopped_on(branch) + def expect_environment_stopped_on(branch, environment) + expect { service.execute_for_branch(branch) } + .to change { environment.reload.state }.from('available').to('stopped') + end + + def expect_environment_stopping_on(branch, environment) expect { service.execute_for_branch(branch) } - .to change { Environment.last.state }.from('available').to('stopped') + .to change { environment.reload.state }.from('available').to('stopping') end - def expect_environment_not_stopped_on(branch) + def expect_environment_not_stopped_on(branch, environment) expect { service.execute_for_branch(branch) } - .not_to change { Environment.last.state } + .not_to change { environment.reload.state }.from('available') + end + + def feature_environment + create(:environment, :with_review_app, project: project, ref: 'feature') end end diff --git a/spec/services/event_create_service_spec.rb b/spec/services/event_create_service_spec.rb index c22099fe410..56da85cc4a0 100644 --- a/spec/services/event_create_service_spec.rb +++ b/spec/services/event_create_service_spec.rb @@ -21,8 +21,10 @@ RSpec.describe EventCreateService, :clean_gitlab_redis_cache, :clean_gitlab_redi end shared_examples 'Snowplow event' do + let(:label) { nil } + it 'is not emitted if FF is disabled' do - stub_feature_flags(route_hll_to_snowplow: false) + stub_feature_flags(feature_flag_name => false) subject @@ -30,15 +32,18 @@ RSpec.describe EventCreateService, :clean_gitlab_redis_cache, :clean_gitlab_redi end it 'is emitted' do + params = { + category: category, + action: action, + namespace: namespace, + user: user, + project: project, + label: label + }.compact + subject - expect_snowplow_event( - category: described_class.to_s, - action: 'action_active_users_project_repo', - namespace: project.namespace, - user: user, - project: project - ) + expect_snowplow_event(**params) end end @@ -74,7 +79,7 @@ RSpec.describe EventCreateService, :clean_gitlab_redis_cache, :clean_gitlab_redi end end - describe 'Merge Requests' do + describe 'Merge Requests', :snowplow do describe '#open_mr' do subject(:open_mr) { service.open_mr(merge_request, merge_request.author) } @@ -89,6 +94,16 @@ RSpec.describe EventCreateService, :clean_gitlab_redis_cache, :clean_gitlab_redi it_behaves_like "it records the event in the event counter" do let(:event_action) { Gitlab::UsageDataCounters::TrackUniqueEvents::MERGE_REQUEST_ACTION } end + + it_behaves_like 'Snowplow event' do + let(:category) { Gitlab::UsageDataCounters::TrackUniqueEvents::MERGE_REQUEST_ACTION.to_s } + let(:label) { 'merge_requests_users' } + let(:action) { 'create' } + let(:namespace) { project.namespace } + let(:project) { merge_request.project } + let(:user) { merge_request.author } + let(:feature_flag_name) { :route_hll_to_snowplow_phase2 } + end end describe '#close_mr' do @@ -105,6 +120,16 @@ RSpec.describe EventCreateService, :clean_gitlab_redis_cache, :clean_gitlab_redi it_behaves_like "it records the event in the event counter" do let(:event_action) { Gitlab::UsageDataCounters::TrackUniqueEvents::MERGE_REQUEST_ACTION } end + + it_behaves_like 'Snowplow event' do + let(:category) { Gitlab::UsageDataCounters::TrackUniqueEvents::MERGE_REQUEST_ACTION.to_s } + let(:label) { 'merge_requests_users' } + let(:action) { 'close' } + let(:namespace) { project.namespace } + let(:project) { merge_request.project } + let(:user) { merge_request.author } + let(:feature_flag_name) { :route_hll_to_snowplow_phase2 } + end end describe '#merge_mr' do @@ -121,6 +146,16 @@ RSpec.describe EventCreateService, :clean_gitlab_redis_cache, :clean_gitlab_redi it_behaves_like "it records the event in the event counter" do let(:event_action) { Gitlab::UsageDataCounters::TrackUniqueEvents::MERGE_REQUEST_ACTION } end + + it_behaves_like 'Snowplow event' do + let(:category) { Gitlab::UsageDataCounters::TrackUniqueEvents::MERGE_REQUEST_ACTION.to_s } + let(:label) { 'merge_requests_users' } + let(:action) { 'merge' } + let(:namespace) { project.namespace } + let(:project) { merge_request.project } + let(:user) { merge_request.author } + let(:feature_flag_name) { :route_hll_to_snowplow_phase2 } + end end describe '#reopen_mr' do @@ -295,7 +330,12 @@ RSpec.describe EventCreateService, :clean_gitlab_redis_cache, :clean_gitlab_redi let(:event_action) { Gitlab::UsageDataCounters::TrackUniqueEvents::PUSH_ACTION } end - it_behaves_like 'Snowplow event' + it_behaves_like 'Snowplow event' do + let(:category) { described_class.to_s } + let(:action) { 'action_active_users_project_repo' } + let(:namespace) { project.namespace } + let(:feature_flag_name) { :route_hll_to_snowplow } + end end describe '#bulk_push', :snowplow do @@ -315,7 +355,12 @@ RSpec.describe EventCreateService, :clean_gitlab_redis_cache, :clean_gitlab_redi let(:event_action) { Gitlab::UsageDataCounters::TrackUniqueEvents::PUSH_ACTION } end - it_behaves_like 'Snowplow event' + it_behaves_like 'Snowplow event' do + let(:category) { described_class.to_s } + let(:action) { 'action_active_users_project_repo' } + let(:namespace) { project.namespace } + let(:feature_flag_name) { :route_hll_to_snowplow } + end end describe 'Project' do @@ -392,7 +437,7 @@ RSpec.describe EventCreateService, :clean_gitlab_redis_cache, :clean_gitlab_redi end end - describe '#leave_note' do + describe '#leave_note', :snowplow do subject(:leave_note) { service.leave_note(note, author) } let(:note) { create(:note) } @@ -409,6 +454,17 @@ RSpec.describe EventCreateService, :clean_gitlab_redis_cache, :clean_gitlab_redi it_behaves_like "it records the event in the event counter" do let(:note) { create(:diff_note_on_merge_request) } end + + it_behaves_like 'Snowplow event' do + let(:note) { create(:diff_note_on_merge_request) } + let(:category) { Gitlab::UsageDataCounters::TrackUniqueEvents::MERGE_REQUEST_ACTION.to_s } + let(:label) { 'merge_requests_users' } + let(:action) { 'comment' } + let(:project) { note.project } + let(:namespace) { project.namespace } + let(:feature_flag_name) { :route_hll_to_snowplow_phase2 } + let(:user) { author } + end end context 'when it is not a diff note' do diff --git a/spec/services/git/branch_push_service_spec.rb b/spec/services/git/branch_push_service_spec.rb index 57c130f76a4..befa9598964 100644 --- a/spec/services/git/branch_push_service_spec.rb +++ b/spec/services/git/branch_push_service_spec.rb @@ -410,7 +410,7 @@ RSpec.describe Git::BranchPushService, services: true do end context "for jira issue tracker" do - include JiraServiceHelper + include JiraIntegrationHelpers let(:jira_tracker) { project.create_jira_integration if project.jira_integration.nil? } diff --git a/spec/services/import/fogbugz_service_spec.rb b/spec/services/import/fogbugz_service_spec.rb new file mode 100644 index 00000000000..c9477dba7a5 --- /dev/null +++ b/spec/services/import/fogbugz_service_spec.rb @@ -0,0 +1,150 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Import::FogbugzService do + let_it_be(:user) { create(:user) } + + let(:base_uri) { "https://test:7990" } + let(:token) { "asdasd12345" } + let(:repo_id) { "fogbugz_id" } + let(:repo) { instance_double(Gitlab::FogbugzImport::Repository, name: 'test', raw_data: nil) } + + let(:client) { instance_double(Gitlab::FogbugzImport::Client) } + let(:credentials) { { uri: base_uri, token: token } } + let(:params) { { repo_id: repo_id } } + + subject { described_class.new(client, user, params) } + + before do + allow(subject).to receive(:authorized?).and_return(true) + end + + context 'when no repo is found' do + before do + allow(client).to receive(:repo).with(repo_id).and_return(nil) + end + + it 'returns an error' do + result = subject.execute(credentials) + + expect(result).to include( + message: "Project #{repo_id} could not be found", + status: :error, + http_status: :unprocessable_entity + ) + end + end + + context 'when import source is disabled' do + before do + stub_application_setting(import_sources: nil) + allow(client).to receive(:repo).with(repo_id).and_return(repo) + end + + it 'returns forbidden' do + result = subject.execute(credentials) + + expect(result).to include( + status: :error, + http_status: :forbidden + ) + end + end + + context 'when user is unauthorized' do + before do + allow(subject).to receive(:authorized?).and_return(false) + end + + it 'returns an error' do + result = subject.execute(credentials) + + expect(result).to include( + message: "You don't have permissions to create this project", + status: :error, + http_status: :unauthorized + ) + end + end + + context 'verify url' do + shared_examples 'denies local request' do + before do + allow(client).to receive(:repo).with(repo_id).and_return(repo) + end + + it 'does not allow requests' do + result = subject.execute(credentials) + expect(result[:status]).to eq(:error) + expect(result[:message]).to include("Invalid URL:") + end + end + + context 'when host is localhost' do + let(:base_uri) { 'http://localhost:3000' } + + include_examples 'denies local request' + end + + context 'when host is on local network' do + let(:base_uri) { 'https://192.168.0.191' } + + include_examples 'denies local request' + end + + context 'when host is ftp protocol' do + let(:base_uri) { 'ftp://testing' } + + include_examples 'denies local request' + end + end + + context 'when import starts succesfully' do + before do + allow(client).to receive(:repo).with(repo_id).and_return( + instance_double(Gitlab::FogbugzImport::Repository, name: 'test', raw_data: nil) + ) + end + + it 'returns success' do + result = subject.execute(credentials) + + expect(result[:status]).to eq(:success) + expect(result[:project].name).to eq('test') + end + end + + context 'when import fails to start' do + let(:error_messages_array) { instance_double(Array, join: "something went wrong") } + let(:errors_double) { instance_double(ActiveModel::Errors, full_messages: error_messages_array, :[] => nil) } + let(:project_double) { instance_double(Project, persisted?: false, errors: errors_double) } + let(:project_creator) { instance_double(Gitlab::FogbugzImport::ProjectCreator, execute: project_double )} + + before do + allow(Gitlab::FogbugzImport::ProjectCreator).to receive(:new).and_return(project_creator) + allow(client).to receive(:repo).with(repo_id).and_return( + instance_double(Gitlab::FogbugzImport::Repository, name: 'test', raw_data: nil) + ) + end + + it 'returns error' do + result = subject.execute(credentials) + + expect(result[:status]).to eq(:error) + expect(result[:message]).to eq("something went wrong") + end + end + + it 'returns error for unknown error causes' do + message = 'Not Implemented' + exception = StandardError.new(message) + + allow(client).to receive(:repo).and_raise(exception) + + expect(subject.execute(credentials)).to include({ + status: :error, + message: "Fogbugz import failed due to an error: #{message}" + }) + end +end diff --git a/spec/services/incident_management/issuable_escalation_statuses/prepare_update_service_spec.rb b/spec/services/incident_management/issuable_escalation_statuses/prepare_update_service_spec.rb index 25164df40ca..6c99631fcb0 100644 --- a/spec/services/incident_management/issuable_escalation_statuses/prepare_update_service_spec.rb +++ b/spec/services/incident_management/issuable_escalation_statuses/prepare_update_service_spec.rb @@ -42,14 +42,6 @@ RSpec.describe IncidentManagement::IssuableEscalationStatuses::PrepareUpdateServ it_behaves_like 'successful response', { status_event: :acknowledge } - context 'when feature flag is disabled' do - before do - stub_feature_flags(incident_escalations: false) - end - - it_behaves_like 'availability error response' - end - context 'when user is anonymous' do let(:current_user) { nil } 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 38ce15e74f1..133a644f243 100644 --- a/spec/services/incident_management/timeline_events/create_service_spec.rb +++ b/spec/services/incident_management/timeline_events/create_service_spec.rb @@ -18,6 +18,7 @@ RSpec.describe IncidentManagement::TimelineEvents::CreateService do } end + let(:editable) { false } let(:current_user) { user_with_permissions } let(:service) { described_class.new(incident, current_user, args) } @@ -32,6 +33,8 @@ RSpec.describe IncidentManagement::TimelineEvents::CreateService do expect(execute).to be_error expect(execute.message).to eq(message) end + + it_behaves_like 'does not track incident management event', :incident_management_timeline_event_created end shared_examples 'success response' do @@ -45,7 +48,10 @@ RSpec.describe IncidentManagement::TimelineEvents::CreateService do expect(result.project).to eq(project) expect(result.note).to eq(args[:note]) expect(result.promoted_from_note).to eq(comment) + expect(result.editable).to eq(editable) end + + it_behaves_like 'an incident management tracked event', :incident_management_timeline_event_created end subject(:execute) { service.execute } @@ -90,6 +96,30 @@ RSpec.describe IncidentManagement::TimelineEvents::CreateService do end end + context 'with editable param' do + let(:args) do + { + note: 'note', + occurred_at: Time.current, + action: 'new comment', + promoted_from_note: comment, + editable: editable + } + end + + context 'when editable is true' do + let(:editable) { true } + + it_behaves_like 'success response' + end + + context 'when editable is false' do + let(:editable) { false } + + it_behaves_like 'success response' + end + end + it 'successfully creates a database record', :aggregate_failures do expect { execute }.to change { ::IncidentManagement::TimelineEvent.count }.by(1) end diff --git a/spec/services/incident_management/timeline_events/destroy_service_spec.rb b/spec/services/incident_management/timeline_events/destroy_service_spec.rb index 01daee2b749..09026f87116 100644 --- a/spec/services/incident_management/timeline_events/destroy_service_spec.rb +++ b/spec/services/incident_management/timeline_events/destroy_service_spec.rb @@ -24,6 +24,8 @@ RSpec.describe IncidentManagement::TimelineEvents::DestroyService do expect(execute).to be_error expect(execute.message).to eq(message) end + + it_behaves_like 'does not track incident management event', :incident_management_timeline_event_deleted end subject(:execute) { service.execute } @@ -49,12 +51,16 @@ RSpec.describe IncidentManagement::TimelineEvents::DestroyService do it_behaves_like 'error response', 'Note cannot be removed' end - it 'successfully returns the timeline event', :aggregate_failures do - expect(execute).to be_success + context 'success response' do + it 'successfully returns the timeline event', :aggregate_failures do + expect(execute).to be_success + + result = execute.payload[:timeline_event] + expect(result).to be_a(::IncidentManagement::TimelineEvent) + expect(result.id).to eq(timeline_event.id) + end - result = execute.payload[:timeline_event] - expect(result).to be_a(::IncidentManagement::TimelineEvent) - expect(result.id).to eq(timeline_event.id) + it_behaves_like 'an incident management tracked event', :incident_management_timeline_event_deleted end context 'when incident_timeline feature flag is enabled' do 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 8bc0e5ce0ed..3da533fb2a6 100644 --- a/spec/services/incident_management/timeline_events/update_service_spec.rb +++ b/spec/services/incident_management/timeline_events/update_service_spec.rb @@ -10,6 +10,7 @@ RSpec.describe IncidentManagement::TimelineEvents::UpdateService do let!(:timeline_event) { create(:incident_management_timeline_event, project: project, incident: incident) } let(:occurred_at) { 1.minute.ago } let(:params) { { note: 'Updated note', occurred_at: occurred_at } } + let(:current_user) { user } before do stub_feature_flags(incident_timeline: project) @@ -21,6 +22,8 @@ RSpec.describe IncidentManagement::TimelineEvents::UpdateService do expect(execute).to be_success expect(execute.payload).to eq(timeline_event: timeline_event.reload) end + + it_behaves_like 'an incident management tracked event', :incident_management_timeline_event_edited end shared_examples 'error response' do |message| @@ -28,6 +31,8 @@ RSpec.describe IncidentManagement::TimelineEvents::UpdateService do expect(execute).to be_error expect(execute.message).to eq(message) end + + it_behaves_like 'does not track incident management event', :incident_management_timeline_event_edited end shared_examples 'passing the correct was_changed value' do |was_changed| @@ -135,6 +140,14 @@ RSpec.describe IncidentManagement::TimelineEvents::UpdateService do execute end end + + context 'when timeline event is non-editable' do + let!(:timeline_event) do + create(:incident_management_timeline_event, :non_editable, project: project, incident: incident) + end + + it_behaves_like 'error response', 'You cannot edit this timeline event.' + end end context 'when user does not have permissions' do diff --git a/spec/services/integrations/propagate_service_spec.rb b/spec/services/integrations/propagate_service_spec.rb index 7ae843f6aeb..c971c4a0ad0 100644 --- a/spec/services/integrations/propagate_service_spec.rb +++ b/spec/services/integrations/propagate_service_spec.rb @@ -4,7 +4,7 @@ require 'spec_helper' RSpec.describe Integrations::PropagateService do describe '.propagate' do - include JiraServiceHelper + include JiraIntegrationHelpers before do stub_jira_integration_test diff --git a/spec/services/issues/create_service_spec.rb b/spec/services/issues/create_service_spec.rb index 3934ca04a00..5c1544d8ebc 100644 --- a/spec/services/issues/create_service_spec.rb +++ b/spec/services/issues/create_service_spec.rb @@ -66,6 +66,7 @@ RSpec.describe Issues::CreateService do expect(issue.milestone).to eq(milestone) expect(issue.due_date).to eq(Date.tomorrow) expect(issue.work_item_type.base_type).to eq('issue') + expect(issue.issue_customer_relations_contacts).to be_empty end context 'when a build_service is provided' do @@ -444,6 +445,50 @@ RSpec.describe Issues::CreateService do expect(issue.issue_customer_relations_contacts.last.contact).to eq(contact) end end + + context 'with external_author' do + let_it_be(:contact) { create(:contact, group: group) } + + context 'when CRM contact exists with matching e-mail' do + let(:opts) do + { + title: 'Title', + external_author: contact.email + } + end + + context 'with permission' do + it 'assigns contact to issue' do + group.add_reporter(user) + expect(issue).to be_persisted + expect(issue.issue_customer_relations_contacts.last.contact).to eq(contact) + end + end + + context 'without permission' do + it 'does not assign contact to issue' do + group.add_guest(user) + expect(issue).to be_persisted + expect(issue.issue_customer_relations_contacts).to be_empty + end + end + end + + context 'when no CRM contact exists with matching e-mail' do + let(:opts) do + { + title: 'Title', + external_author: 'example@gitlab.com' + } + end + + it 'does not assign contact to issue' do + group.add_reporter(user) + expect(issue).to be_persisted + expect(issue.issue_customer_relations_contacts).to be_empty + end + end + end end context 'resolving discussions' do diff --git a/spec/services/issues/move_service_spec.rb b/spec/services/issues/move_service_spec.rb index 35a380e01d0..56a3c22cd7f 100644 --- a/spec/services/issues/move_service_spec.rb +++ b/spec/services/issues/move_service_spec.rb @@ -194,20 +194,6 @@ RSpec.describe Issues::MoveService do expect(new_issue.customer_relations_contacts).to be_empty end end - - context 'when customer_relations feature is disabled' do - let(:another_project) { create(:project, namespace: create(:group)) } - - before do - stub_feature_flags(customer_relations: false) - end - - it 'does not preserve contacts' do - new_issue = move_service.execute(old_issue, new_project) - - expect(new_issue.customer_relations_contacts).to be_empty - end - end end context 'moving to same project' do diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb index d496857bb25..d11fe772023 100644 --- a/spec/services/issues/update_service_spec.rb +++ b/spec/services/issues/update_service_spec.rb @@ -1230,14 +1230,6 @@ RSpec.describe Issues::UpdateService, :mailer do it_behaves_like 'updates the escalation status record', :acknowledged end - - context 'with :incident_escalations feature flag disabled' do - before do - stub_feature_flags(incident_escalations: false) - end - - it_behaves_like 'does not change the status record' - end end context 'when issue type is not incident' do diff --git a/spec/services/jira_connect_subscriptions/create_service_spec.rb b/spec/services/jira_connect_subscriptions/create_service_spec.rb index cde4753cde7..85208a30c30 100644 --- a/spec/services/jira_connect_subscriptions/create_service_spec.rb +++ b/spec/services/jira_connect_subscriptions/create_service_spec.rb @@ -3,9 +3,10 @@ require 'spec_helper' RSpec.describe JiraConnectSubscriptions::CreateService do - let(:installation) { create(:jira_connect_installation) } - let(:current_user) { create(:user) } - let(:group) { create(:group) } + let_it_be(:installation) { create(:jira_connect_installation) } + let_it_be(:current_user) { create(:user) } + let_it_be(:group) { create(:group) } + let(:path) { group.full_path } let(:params) { { namespace_path: path, jira_user: jira_user } } let(:jira_user) { double(:JiraUser, site_admin?: true) } @@ -16,38 +17,31 @@ RSpec.describe JiraConnectSubscriptions::CreateService do group.add_maintainer(current_user) end - shared_examples 'a failed execution' do + shared_examples 'a failed execution' do |**status_attributes| it 'does not create a subscription' do expect { subject }.not_to change { installation.subscriptions.count } end it 'returns an error status' do expect(subject[:status]).to eq(:error) + expect(subject).to include(status_attributes) end end context 'remote user does not have access' do let(:jira_user) { double(site_admin?: false) } - it 'does not create a subscription' do - expect { subject }.not_to change { installation.subscriptions.count } - end - - it 'returns error' do - expect(subject[:status]).to eq(:error) - end + it_behaves_like 'a failed execution', + http_status: 403, + message: 'The Jira user is not a site administrator. Check the permissions in Jira and try again.' end context 'remote user cannot be retrieved' do let(:jira_user) { nil } - it 'does not create a subscription' do - expect { subject }.not_to change { installation.subscriptions.count } - end - - it 'returns error' do - expect(subject[:status]).to eq(:error) - end + it_behaves_like 'a failed execution', + http_status: 403, + message: 'Could not fetch user information from Jira. Check the permissions in Jira and try again.' end context 'when user does have access' do @@ -60,8 +54,8 @@ RSpec.describe JiraConnectSubscriptions::CreateService do end context 'namespace has projects' do - let!(:project_1) { create(:project, group: group) } - let!(:project_2) { create(:project, group: group) } + let_it_be(:project_1) { create(:project, group: group) } + let_it_be(:project_2) { create(:project, group: group) } before do stub_const("#{described_class}::MERGE_REQUEST_SYNC_BATCH_SIZE", 1) @@ -81,12 +75,18 @@ RSpec.describe JiraConnectSubscriptions::CreateService do context 'when path is invalid' do let(:path) { 'some_invalid_namespace_path' } - it_behaves_like 'a failed execution' + it_behaves_like 'a failed execution', + http_status: 401, + message: 'Cannot find namespace. Make sure you have sufficient permissions.' end context 'when user does not have access' do - subject { described_class.new(installation, create(:user), namespace_path: path).execute } + let_it_be(:other_group) { create(:group) } + + let(:path) { other_group.full_path } - it_behaves_like 'a failed execution' + it_behaves_like 'a failed execution', + http_status: 401, + message: 'Cannot find namespace. Make sure you have sufficient permissions.' end end diff --git a/spec/services/jira_import/start_import_service_spec.rb b/spec/services/jira_import/start_import_service_spec.rb index e04e3314158..510f58f0e75 100644 --- a/spec/services/jira_import/start_import_service_spec.rb +++ b/spec/services/jira_import/start_import_service_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' RSpec.describe JiraImport::StartImportService do - include JiraServiceHelper + include JiraIntegrationHelpers let_it_be(:user) { create(:user) } let_it_be(:project, reload: true) { create(:project) } diff --git a/spec/services/jira_import/users_importer_spec.rb b/spec/services/jira_import/users_importer_spec.rb index af408847260..ace9e0d5779 100644 --- a/spec/services/jira_import/users_importer_spec.rb +++ b/spec/services/jira_import/users_importer_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' RSpec.describe JiraImport::UsersImporter do - include JiraServiceHelper + include JiraIntegrationHelpers let_it_be(:user) { create(:user) } let_it_be(:group) { create(:group) } diff --git a/spec/services/markdown_content_rewriter_service_spec.rb b/spec/services/markdown_content_rewriter_service_spec.rb index 37c8a210ba5..91a117536ca 100644 --- a/spec/services/markdown_content_rewriter_service_spec.rb +++ b/spec/services/markdown_content_rewriter_service_spec.rb @@ -8,38 +8,63 @@ RSpec.describe MarkdownContentRewriterService do let_it_be(:target_parent) { create(:project, :public) } let(:content) { 'My content' } + let(:issue) { create(:issue, project: source_parent, description: content)} describe '#initialize' do it 'raises an error if source_parent is not a Project' do expect do - described_class.new(user, content, create(:group), target_parent) + described_class.new(user, issue, :description, create(:group), target_parent) end.to raise_error(ArgumentError, 'The rewriter classes require that `source_parent` is a `Project`') end + + it 'raises an error if field does not have cached markdown' do + expect do + described_class.new(user, issue, :author, source_parent, target_parent) + end.to raise_error(ArgumentError, 'The `field` attribute does not contain cached markdown') + end end describe '#execute' do - subject { described_class.new(user, content, source_parent, target_parent).execute } + subject { described_class.new(user, issue, :description, source_parent, target_parent).execute } - it 'calls the rewriter classes successfully', :aggregate_failures do - [Gitlab::Gfm::ReferenceRewriter, Gitlab::Gfm::UploadsRewriter].each do |rewriter_class| - service = double - - expect(service).to receive(:rewrite).with(target_parent) - expect(rewriter_class).to receive(:new).and_return(service) + context 'when content does not need a rewrite' do + it 'returns original content and cached html' do + expect(subject).to eq({ + 'description' => issue.description, + 'description_html' => issue.description_html, + 'skip_markdown_cache_validation' => true + }) end + end + + context 'when content needs a rewrite' do + it 'calls the rewriter classes successfully', :aggregate_failures do + described_class::REWRITERS.each do |rewriter_class| + service = double - subject + allow(service).to receive(:needs_rewrite?).and_return(true) + + expect(service).to receive(:rewrite).with(target_parent) + expect(rewriter_class).to receive(:new).and_return(service) + end + + subject + end end # Perform simple integration-style tests for each rewriter class. # to prove they run correctly. - context 'when content contains a reference' do - let_it_be(:issue) { create(:issue, project: source_parent) } + context 'when content has references' do + let_it_be(:issue_to_reference) { create(:issue, project: source_parent) } - let(:content) { "See ##{issue.iid}" } + let(:content) { "See ##{issue_to_reference.iid}" } it 'rewrites content' do - expect(subject).to eq("See #{source_parent.full_path}##{issue.iid}") + expect(subject).to eq({ + 'description' => "See #{source_parent.full_path}##{issue_to_reference.iid}", + 'description_html' => nil, + 'skip_markdown_cache_validation' => false + }) end end @@ -50,9 +75,37 @@ RSpec.describe MarkdownContentRewriterService do it 'rewrites content' do new_content = subject - expect(new_content).not_to eq(content) - expect(new_content.length).to eq(content.length) + expect(new_content[:description]).not_to eq(content) + expect(new_content[:description].length).to eq(content.length) + expect(new_content[1]).to eq(nil) end end end + + describe '#safe_to_copy_markdown?' do + subject do + rewriter = described_class.new(user, issue, :description, source_parent, target_parent) + rewriter.safe_to_copy_markdown? + end + + context 'when content has references' do + let(:milestone) { create(:milestone, project: source_parent) } + let(:content) { "Description that references #{milestone.to_reference}" } + + it { is_expected.to eq(false) } + end + + context 'when content has uploaded file references' do + let(:image_uploader) { build(:file_uploader, project: source_parent) } + let(:content) { "Text and #{image_uploader.markdown_link}" } + + it { is_expected.to eq(false) } + end + + context 'when content does not have references or uploads' do + let(:content) { "simples text with ```code```" } + + it { is_expected.to eq(true) } + end + end end diff --git a/spec/services/members/approve_access_request_service_spec.rb b/spec/services/members/approve_access_request_service_spec.rb index f7fbac612ee..d26bab7bb0a 100644 --- a/spec/services/members/approve_access_request_service_spec.rb +++ b/spec/services/members/approve_access_request_service_spec.rb @@ -9,36 +9,34 @@ RSpec.describe Members::ApproveAccessRequestService do let(:access_requester_user) { create(:user) } let(:access_requester) { source.requesters.find_by!(user_id: access_requester_user.id) } let(:opts) { {} } - - shared_examples 'a service raising ActiveRecord::RecordNotFound' do - it 'raises ActiveRecord::RecordNotFound' do - expect { described_class.new(current_user).execute(access_requester, **opts) }.to raise_error(ActiveRecord::RecordNotFound) - end - end + let(:params) { {} } + let(:custom_access_level) { Gitlab::Access::MAINTAINER } shared_examples 'a service raising Gitlab::Access::AccessDeniedError' do it 'raises Gitlab::Access::AccessDeniedError' do - expect { described_class.new(current_user).execute(access_requester, **opts) }.to raise_error(Gitlab::Access::AccessDeniedError) + expect { described_class.new(current_user, params).execute(access_requester, **opts) }.to raise_error(Gitlab::Access::AccessDeniedError) end end shared_examples 'a service approving an access request' do it 'succeeds' do - expect { described_class.new(current_user).execute(access_requester, **opts) }.to change { source.requesters.count }.by(-1) + expect { described_class.new(current_user, params).execute(access_requester, **opts) }.to change { source.requesters.count }.by(-1) end it 'returns a <Source>Member' do - member = described_class.new(current_user).execute(access_requester, **opts) + member = described_class.new(current_user, params).execute(access_requester, **opts) expect(member).to be_a "#{source.class}Member".constantize expect(member.requested_at).to be_nil end context 'with a custom access level' do + let(:params) { { access_level: custom_access_level } } + it 'returns a ProjectMember with the custom access level' do - member = described_class.new(current_user, access_level: Gitlab::Access::MAINTAINER).execute(access_requester, **opts) + member = described_class.new(current_user, params).execute(access_requester, **opts) - expect(member.access_level).to eq(Gitlab::Access::MAINTAINER) + expect(member.access_level).to eq(custom_access_level) end end end @@ -111,5 +109,38 @@ RSpec.describe Members::ApproveAccessRequestService do let(:source) { group } end end + + context 'in a project' do + let_it_be(:group_project) { create(:project, :public, group: create(:group, :public)) } + + let(:source) { group_project } + let(:custom_access_level) { Gitlab::Access::OWNER } + let(:params) { { access_level: custom_access_level } } + + before do + group_project.request_access(access_requester_user) + end + + context 'maintainers' do + before do + group_project.add_maintainer(current_user) + end + + context 'cannot approve the access request of a requester to give them OWNER permissions' do + it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' + end + end + + context 'owners' do + before do + # so that `current_user` is considered an `OWNER` in the project via inheritance. + group_project.group.add_owner(current_user) + end + + context 'can approve the access request of a requester to give them OWNER permissions' do + it_behaves_like 'a service approving an access request' + end + end + end end end diff --git a/spec/services/members/create_service_spec.rb b/spec/services/members/create_service_spec.rb index 730175af0bb..e79e13af769 100644 --- a/spec/services/members/create_service_spec.rb +++ b/spec/services/members/create_service_spec.rb @@ -33,6 +33,18 @@ RSpec.describe Members::CreateService, :aggregate_failures, :clean_gitlab_redis_ it 'raises a Gitlab::Access::AccessDeniedError' do expect { execute_service }.to raise_error(Gitlab::Access::AccessDeniedError) end + + context 'when a project maintainer attempts to add owners' do + let(:access_level) { Gitlab::Access::OWNER } + + before do + source.add_maintainer(current_user) + end + + it 'raises a Gitlab::Access::AccessDeniedError' do + expect { execute_service }.to raise_error(Gitlab::Access::AccessDeniedError) + end + end end context 'when passing an invalid source' do diff --git a/spec/services/members/creator_service_spec.rb b/spec/services/members/creator_service_spec.rb index ff5bf705b6c..8b1df2ab86d 100644 --- a/spec/services/members/creator_service_spec.rb +++ b/spec/services/members/creator_service_spec.rb @@ -11,7 +11,7 @@ RSpec.describe Members::CreatorService do describe '#execute' do it 'raises error for new member on authorization check implementation' do expect do - described_class.new(source, user, :maintainer, current_user: current_user).execute + described_class.add_user(source, user, :maintainer, current_user: current_user) end.to raise_error(NotImplementedError) end @@ -19,7 +19,7 @@ RSpec.describe Members::CreatorService do source.add_developer(user) expect do - described_class.new(source, user, :maintainer, current_user: current_user).execute + described_class.add_user(source, user, :maintainer, current_user: current_user) end.to raise_error(NotImplementedError) end end diff --git a/spec/services/members/destroy_service_spec.rb b/spec/services/members/destroy_service_spec.rb index 1a1283b1078..9f0daba3327 100644 --- a/spec/services/members/destroy_service_spec.rb +++ b/spec/services/members/destroy_service_spec.rb @@ -105,26 +105,46 @@ RSpec.describe Members::DestroyService do context 'with a project member' do let(:member) { group_project.members.find_by(user_id: member_user.id) } - before do - group_project.add_developer(member_user) + context 'when current user does not have any membership management permissions' do + before do + group_project.add_developer(member_user) + end + + it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' + + context 'when skipping authorisation' do + it_behaves_like 'a service destroying a member with access' do + let(:opts) { { skip_authorization: true, unassign_issuables: true } } + end + end end - it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' + context 'when a project maintainer tries to destroy a project owner' do + before do + group_project.add_owner(member_user) + end - it_behaves_like 'a service destroying a member with access' do - let(:opts) { { skip_authorization: true, unassign_issuables: true } } + it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' + + context 'when skipping authorisation' do + it_behaves_like 'a service destroying a member with access' do + let(:opts) { { skip_authorization: true, unassign_issuables: true } } + end + end end end + end - context 'with a group member' do - let(:member) { group.members.find_by(user_id: member_user.id) } + context 'with a group member' do + let(:member) { group.members.find_by(user_id: member_user.id) } - before do - group.add_developer(member_user) - end + before do + group.add_developer(member_user) + end - it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' + it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' + context 'when skipping authorisation' do it_behaves_like 'a service destroying a member with access' do let(:opts) { { skip_authorization: true, unassign_issuables: true } } end diff --git a/spec/services/members/groups/bulk_creator_service_spec.rb b/spec/services/members/groups/bulk_creator_service_spec.rb deleted file mode 100644 index 0623ae00080..00000000000 --- a/spec/services/members/groups/bulk_creator_service_spec.rb +++ /dev/null @@ -1,10 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Members::Groups::BulkCreatorService do - it_behaves_like 'bulk member creation' do - let_it_be(:source, reload: true) { create(:group, :public) } - let_it_be(:member_type) { GroupMember } - end -end diff --git a/spec/services/members/groups/creator_service_spec.rb b/spec/services/members/groups/creator_service_spec.rb index c3ba7c0374d..b80b7998eac 100644 --- a/spec/services/members/groups/creator_service_spec.rb +++ b/spec/services/members/groups/creator_service_spec.rb @@ -3,16 +3,24 @@ require 'spec_helper' RSpec.describe Members::Groups::CreatorService do + let_it_be(:source, reload: true) { create(:group, :public) } + let_it_be(:user) { create(:user) } + describe '.access_levels' do it 'returns Gitlab::Access.options_with_owner' do expect(described_class.access_levels).to eq(Gitlab::Access.sym_options_with_owner) end end - describe '#execute' do - let_it_be(:source, reload: true) { create(:group, :public) } - let_it_be(:user) { create(:user) } + it_behaves_like 'owner management' + + describe '.add_users' do + it_behaves_like 'bulk member creation' do + let_it_be(:member_type) { GroupMember } + end + end + describe '.add_user' do it_behaves_like 'member creation' do let_it_be(:member_type) { GroupMember } end @@ -22,7 +30,7 @@ RSpec.describe Members::Groups::CreatorService do expect(AuthorizedProjectsWorker).to receive(:bulk_perform_and_wait).once 1.upto(3) do - described_class.new(source, user, :maintainer).execute + described_class.add_user(source, user, :maintainer) end end end diff --git a/spec/services/members/invite_service_spec.rb b/spec/services/members/invite_service_spec.rb index 8213e8baae0..a948041479b 100644 --- a/spec/services/members/invite_service_spec.rb +++ b/spec/services/members/invite_service_spec.rb @@ -367,20 +367,21 @@ RSpec.describe Members::InviteService, :aggregate_failures, :clean_gitlab_redis_ context 'when email is already a member with a user on the project' do let!(:existing_member) { create(:project_member, :guest, project: project) } - let(:params) { { email: "#{existing_member.user.email}" } } + let(:params) { { email: "#{existing_member.user.email}", access_level: ProjectMember::MAINTAINER } } - it 'returns an error for the already invited email' do - expect_not_to_create_members - expect(result[:message][existing_member.user.email]).to eq("User already exists in source") + it 'allows re-invite of an already invited email and updates the access_level' do + expect { result }.not_to change(ProjectMember, :count) + expect(result[:status]).to eq(:success) + expect(existing_member.reset.access_level).to eq ProjectMember::MAINTAINER end context 'when email belongs to an existing user as a secondary email' do let(:secondary_email) { create(:email, email: 'secondary@example.com', user: existing_member.user) } let(:params) { { email: "#{secondary_email.email}" } } - it 'returns an error for the already invited email' do - expect_not_to_create_members - expect(result[:message][secondary_email.email]).to eq("User already exists in source") + it 'allows re-invite to an already invited email' do + expect_to_create_members(count: 0) + expect(result[:status]).to eq(:success) end end end diff --git a/spec/services/members/mailgun/process_webhook_service_spec.rb b/spec/services/members/mailgun/process_webhook_service_spec.rb deleted file mode 100644 index d6a21183395..00000000000 --- a/spec/services/members/mailgun/process_webhook_service_spec.rb +++ /dev/null @@ -1,42 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Members::Mailgun::ProcessWebhookService do - describe '#execute', :aggregate_failures do - let_it_be(:member) { create(:project_member, :invited) } - - let(:raw_invite_token) { member.raw_invite_token } - let(:payload) { { 'user-variables' => { ::Members::Mailgun::INVITE_EMAIL_TOKEN_KEY => raw_invite_token } } } - - subject(:service) { described_class.new(payload).execute } - - it 'marks the member invite email success as false' do - expect(Gitlab::AppLogger).to receive(:info).with(/^UPDATED MEMBER INVITE_EMAIL_SUCCESS/).and_call_original - - expect { service }.to change { member.reload.invite_email_success }.from(true).to(false) - end - - context 'when member can not be found' do - let(:raw_invite_token) { '_foobar_' } - - it 'does not change member status' do - expect(Gitlab::AppLogger).not_to receive(:info).with(/^UPDATED MEMBER INVITE_EMAIL_SUCCESS/) - - expect { service }.not_to change { member.reload.invite_email_success } - end - end - - context 'when invite token is not found in payload' do - let(:payload) { {} } - - it 'does not change member status and logs an error' do - expect(Gitlab::AppLogger).not_to receive(:info).with(/^UPDATED MEMBER INVITE_EMAIL_SUCCESS/) - expect(Gitlab::ErrorTracking).to receive(:track_exception).with( - an_instance_of(described_class::ProcessWebhookServiceError)) - - expect { service }.not_to change { member.reload.invite_email_success } - end - end - end -end diff --git a/spec/services/members/projects/bulk_creator_service_spec.rb b/spec/services/members/projects/bulk_creator_service_spec.rb deleted file mode 100644 index 7acb7d79fe7..00000000000 --- a/spec/services/members/projects/bulk_creator_service_spec.rb +++ /dev/null @@ -1,10 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Members::Projects::BulkCreatorService do - it_behaves_like 'bulk member creation' do - let_it_be(:source, reload: true) { create(:project, :public) } - let_it_be(:member_type) { ProjectMember } - end -end diff --git a/spec/services/members/projects/creator_service_spec.rb b/spec/services/members/projects/creator_service_spec.rb index 7605238c3c5..38955122ab0 100644 --- a/spec/services/members/projects/creator_service_spec.rb +++ b/spec/services/members/projects/creator_service_spec.rb @@ -3,16 +3,24 @@ require 'spec_helper' RSpec.describe Members::Projects::CreatorService do + let_it_be(:source, reload: true) { create(:project, :public) } + let_it_be(:user) { create(:user) } + describe '.access_levels' do it 'returns Gitlab::Access.sym_options_with_owner' do expect(described_class.access_levels).to eq(Gitlab::Access.sym_options_with_owner) end end - describe '#execute' do - let_it_be(:source, reload: true) { create(:project, :public) } - let_it_be(:user) { create(:user) } + it_behaves_like 'owner management' + + describe '.add_users' do + it_behaves_like 'bulk member creation' do + let_it_be(:member_type) { ProjectMember } + end + end + describe '.add_user' do it_behaves_like 'member creation' do let_it_be(:member_type) { ProjectMember } end @@ -22,7 +30,7 @@ RSpec.describe Members::Projects::CreatorService do expect(AuthorizedProjectUpdate::UserRefreshFromReplicaWorker).to receive(:bulk_perform_in).once 1.upto(3) do - described_class.new(source, user, :maintainer).execute + described_class.add_user(source, user, :maintainer) end end end diff --git a/spec/services/members/update_service_spec.rb b/spec/services/members/update_service_spec.rb index a1b1397d444..f919d6d1516 100644 --- a/spec/services/members/update_service_spec.rb +++ b/spec/services/members/update_service_spec.rb @@ -9,8 +9,9 @@ RSpec.describe Members::UpdateService do let(:member_user) { create(:user) } let(:permission) { :update } let(:member) { source.members_and_requesters.find_by!(user_id: member_user.id) } + let(:access_level) { Gitlab::Access::MAINTAINER } let(:params) do - { access_level: Gitlab::Access::MAINTAINER } + { access_level: access_level } end subject { described_class.new(current_user, params).execute(member, permission: permission) } @@ -29,7 +30,7 @@ RSpec.describe Members::UpdateService do updated_member = subject.fetch(:member) expect(updated_member).to be_valid - expect(updated_member.access_level).to eq(Gitlab::Access::MAINTAINER) + expect(updated_member.access_level).to eq(access_level) end it 'returns success status' do @@ -111,4 +112,75 @@ RSpec.describe Members::UpdateService do let(:source) { group } end end + + context 'in a project' do + let_it_be(:group_project) { create(:project, group: create(:group)) } + + let(:source) { group_project } + + context 'a project maintainer' do + before do + group_project.add_maintainer(current_user) + end + + context 'cannot update a member to OWNER' do + before do + group_project.add_developer(member_user) + end + + it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do + let(:access_level) { Gitlab::Access::OWNER } + end + end + + context 'cannot update themselves to OWNER' do + let(:member) { source.members_and_requesters.find_by!(user_id: current_user.id) } + + before do + group_project.add_developer(member_user) + end + + it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do + let(:access_level) { Gitlab::Access::OWNER } + end + end + + context 'cannot downgrade a member from OWNER' do + before do + group_project.add_owner(member_user) + end + + it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do + let(:access_level) { Gitlab::Access::MAINTAINER } + end + end + end + + context 'owners' do + before do + # so that `current_user` is considered an `OWNER` in the project via inheritance. + group_project.group.add_owner(current_user) + end + + context 'can update a member to OWNER' do + before do + group_project.add_developer(member_user) + end + + it_behaves_like 'a service updating a member' do + let(:access_level) { Gitlab::Access::OWNER } + end + end + + context 'can downgrade a member from OWNER' do + before do + group_project.add_owner(member_user) + end + + it_behaves_like 'a service updating a member' do + let(:access_level) { Gitlab::Access::MAINTAINER } + end + end + end + end end diff --git a/spec/services/merge_requests/build_service_spec.rb b/spec/services/merge_requests/build_service_spec.rb index ab3d9880d29..3c9d2271ddc 100644 --- a/spec/services/merge_requests/build_service_spec.rb +++ b/spec/services/merge_requests/build_service_spec.rb @@ -79,6 +79,15 @@ RSpec.describe MergeRequests::BuildService do end end + shared_examples 'with a Default.md template' do + let(:files) { { '.gitlab/merge_request_templates/Default.md' => 'Default template contents' } } + let(:project) { create(:project, :custom_repo, files: files ) } + + it 'the template description is preferred' do + expect(merge_request.description).to eq('Default template contents') + end + end + describe '#execute' do it 'calls the compare service with the correct arguments' do allow_any_instance_of(described_class).to receive(:projects_and_branches_valid?).and_return(true) @@ -221,6 +230,7 @@ RSpec.describe MergeRequests::BuildService do end it_behaves_like 'allows the merge request to be created' + it_behaves_like 'with a Default.md template' it 'uses the title of the commit as the title of the merge request' do expect(merge_request.title).to eq(commit_2.safe_message.split("\n").first) @@ -241,6 +251,8 @@ RSpec.describe MergeRequests::BuildService do context 'commit has no description' do let(:commits) { Commit.decorate([commit_3], project) } + it_behaves_like 'with a Default.md template' + it 'uses the title of the commit as the title of the merge request' do expect(merge_request.title).to eq(commit_3.safe_message) end @@ -279,6 +291,17 @@ RSpec.describe MergeRequests::BuildService do expect(merge_request.description).to eq(expected_description) end + + context 'a Default.md template is defined' do + let(:files) { { '.gitlab/merge_request_templates/Default.md' => 'Default template contents' } } + let(:project) { create(:project, :custom_repo, files: files ) } + + it 'appends the closing description to a Default.md template' do + expected_description = ['Default template contents', closing_message].compact.join("\n\n") + + expect(merge_request.description).to eq(expected_description) + end + end end context 'when the source branch matches an internal issue' do @@ -332,6 +355,7 @@ RSpec.describe MergeRequests::BuildService do end it_behaves_like 'allows the merge request to be created' + it_behaves_like 'with a Default.md template' it 'uses the title of the branch as the merge request title' do expect(merge_request.title).to eq('Feature branch') @@ -347,6 +371,15 @@ RSpec.describe MergeRequests::BuildService do it 'keeps the description from the initial params' do expect(merge_request.description).to eq(description) end + + context 'a Default.md template is defined' do + let(:files) { { '.gitlab/merge_request_templates/Default.md' => 'Default template contents' } } + let(:project) { create(:project, :custom_repo, files: files ) } + + it 'keeps the description from the initial params' do + expect(merge_request.description).to eq(description) + end + end end context 'when the source branch matches an issue' do @@ -377,6 +410,17 @@ RSpec.describe MergeRequests::BuildService do it 'sets the closing description' do expect(merge_request.description).to eq(closing_message) end + + context 'a Default.md template is defined' do + let(:files) { { '.gitlab/merge_request_templates/Default.md' => 'Default template contents' } } + let(:project) { create(:project, :custom_repo, files: files ) } + + it 'appends the closing description to a Default.md template' do + expected_description = ['Default template contents', closing_message].compact.join("\n\n") + + expect(merge_request.description).to eq(expected_description) + end + end end end end @@ -389,6 +433,7 @@ RSpec.describe MergeRequests::BuildService do end it_behaves_like 'allows the merge request to be created' + it_behaves_like 'with a Default.md template' it 'uses the first line of the first multi-line commit message as the title' do expect(merge_request.title).to eq('Closes #1234 Second commit') @@ -426,6 +471,17 @@ RSpec.describe MergeRequests::BuildService do it 'sets the closing description' do expect(merge_request.description).to eq("Create the app#{closing_message ? "\n\n" + closing_message : ''}") end + + context 'a Default.md template is defined' do + let(:files) { { '.gitlab/merge_request_templates/Default.md' => 'Default template contents' } } + let(:project) { create(:project, :custom_repo, files: files ) } + + it 'appends the closing description to a Default.md template' do + expected_description = ['Default template contents', closing_message].compact.join("\n\n") + + expect(merge_request.description).to eq(expected_description) + end + end end end @@ -626,4 +682,52 @@ RSpec.describe MergeRequests::BuildService do end end end + + describe '#assign_description_from_repository_template' do + subject { service.send(:assign_description_from_repository_template) } + + it 'performs no action if the merge request description is not blank' do + merge_request.description = 'foo' + subject + expect(merge_request.description).to eq 'foo' + end + + context 'when a Default template is not found' do + it 'does not modify the merge request description' do + merge_request.description = nil + subject + expect(merge_request.description).to be_nil + end + end + + context 'when a Default template is found' do + context 'when its contents cannot be retrieved' do + let(:files) { { '.gitlab/merge_request_templates/OtherTemplate.md' => 'Other template contents' } } + let(:project) { create(:project, :custom_repo, files: files ) } + + it 'does not modify the merge request description' do + allow(TemplateFinder).to receive(:all_template_names).and_return({ + merge_requests: [ + { name: 'Default', id: 'default', key: 'default', project_id: project.id } + ] + }) + + merge_request.description = nil + subject + expect(merge_request.description).to be_nil + end + end + + context 'when its contents can be retrieved' do + let(:files) { { '.gitlab/merge_request_templates/Default.md' => 'Default template contents' } } + let(:project) { create(:project, :custom_repo, files: files ) } + + it 'modifies the merge request description' do + merge_request.description = nil + subject + expect(merge_request.description).to eq 'Default template contents' + end + end + end + end end diff --git a/spec/services/merge_requests/create_pipeline_service_spec.rb b/spec/services/merge_requests/create_pipeline_service_spec.rb index d84ce8d15b4..08ad05b54da 100644 --- a/spec/services/merge_requests/create_pipeline_service_spec.rb +++ b/spec/services/merge_requests/create_pipeline_service_spec.rb @@ -50,6 +50,19 @@ RSpec.describe MergeRequests::CreatePipelineService do expect(response.payload.source).to eq('merge_request_event') end + context 'when push options contain ci.skip' do + let(:params) { { push_options: { ci: { skip: true } } } } + + it 'creates a skipped pipeline' do + expect { response }.to change { Ci::Pipeline.count }.by(1) + + expect(response).to be_success + expect(response.payload).to be_persisted + expect(response.payload.builds).to be_empty + expect(response.payload).to be_skipped + end + end + context 'with fork merge request' do let_it_be(:forked_project) { fork_project(project, nil, repository: true, target_project: create(:project, :private, :repository)) } diff --git a/spec/services/merge_requests/create_service_spec.rb b/spec/services/merge_requests/create_service_spec.rb index 49f691e97e2..c0c56a72192 100644 --- a/spec/services/merge_requests/create_service_spec.rb +++ b/spec/services/merge_requests/create_service_spec.rb @@ -32,7 +32,7 @@ RSpec.describe MergeRequests::CreateService, :clean_gitlab_redis_shared_state do it 'creates an MR' do expect(merge_request).to be_valid - expect(merge_request.work_in_progress?).to be(false) + expect(merge_request.draft?).to be(false) expect(merge_request.title).to eq('Awesome merge_request') expect(merge_request.assignees).to be_empty expect(merge_request.merge_params['force_remove_source_branch']).to eq('1') @@ -74,7 +74,7 @@ RSpec.describe MergeRequests::CreateService, :clean_gitlab_redis_shared_state do end it 'sets MR to draft' do - expect(merge_request.work_in_progress?).to be(true) + expect(merge_request.draft?).to be(true) end end @@ -90,7 +90,7 @@ RSpec.describe MergeRequests::CreateService, :clean_gitlab_redis_shared_state do end it 'sets MR to draft' do - expect(merge_request.work_in_progress?).to be(true) + expect(merge_request.draft?).to be(true) end end end diff --git a/spec/services/merge_requests/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb index 78deab64b1c..a2d73d8c9b1 100644 --- a/spec/services/merge_requests/merge_service_spec.rb +++ b/spec/services/merge_requests/merge_service_spec.rb @@ -161,7 +161,7 @@ RSpec.describe MergeRequests::MergeService do end context 'with Jira integration' do - include JiraServiceHelper + include JiraIntegrationHelpers let(:jira_tracker) { project.create_jira_integration } let(:jira_issue) { ExternalIssue.new('JIRA-123', project) } @@ -263,10 +263,13 @@ RSpec.describe MergeRequests::MergeService do merge_request.update_attribute(:merge_params, { 'force_remove_source_branch' => '1' }) end + # Not a real use case. When a merger merges a MR , merge param 'should_remove_source_branch' is defined it 'removes the source branch using the author user' do expect(::MergeRequests::DeleteSourceBranchWorker).to receive(:perform_async).with(merge_request.id, merge_request.source_branch_sha, merge_request.author.id) service.execute(merge_request) + + expect(merge_request.reload.should_remove_source_branch?).to be nil end context 'when the merger set the source branch not to be removed' do @@ -276,6 +279,8 @@ RSpec.describe MergeRequests::MergeService do expect(::MergeRequests::DeleteSourceBranchWorker).not_to receive(:perform_async) service.execute(merge_request) + + expect(merge_request.reload.should_remove_source_branch?).to be false end end end @@ -289,6 +294,8 @@ RSpec.describe MergeRequests::MergeService do expect(::MergeRequests::DeleteSourceBranchWorker).to receive(:perform_async).with(merge_request.id, merge_request.source_branch_sha, user.id) service.execute(merge_request) + + expect(merge_request.reload.should_remove_source_branch?).to be true end end end diff --git a/spec/services/merge_requests/mergeability/run_checks_service_spec.rb b/spec/services/merge_requests/mergeability/run_checks_service_spec.rb index d4ee4afd71d..2bb7dc3eef7 100644 --- a/spec/services/merge_requests/mergeability/run_checks_service_spec.rb +++ b/spec/services/merge_requests/mergeability/run_checks_service_spec.rb @@ -7,12 +7,6 @@ RSpec.describe MergeRequests::Mergeability::RunChecksService do let_it_be(:merge_request) { create(:merge_request) } - describe '#CHECKS' do - it 'contains every subclass of the base checks service', :eager_load do - expect(described_class::CHECKS).to contain_exactly(*MergeRequests::Mergeability::CheckBaseService.subclasses) - end - end - describe '#execute' do subject(:execute) { run_checks.execute } @@ -22,8 +16,8 @@ RSpec.describe MergeRequests::Mergeability::RunChecksService do context 'when every check is skipped', :eager_load do before do MergeRequests::Mergeability::CheckBaseService.subclasses.each do |subclass| - expect_next_instance_of(subclass) do |service| - expect(service).to receive(:skip?).and_return(true) + allow_next_instance_of(subclass) do |service| + allow(service).to receive(:skip?).and_return(true) end end end @@ -35,7 +29,7 @@ RSpec.describe MergeRequests::Mergeability::RunChecksService do context 'when a check is skipped' do it 'does not execute the check' do - described_class::CHECKS.each do |check| + merge_request.mergeability_checks.each do |check| allow_next_instance_of(check) do |service| allow(service).to receive(:skip?).and_return(false) allow(service).to receive(:execute).and_return(success_result) @@ -47,7 +41,13 @@ RSpec.describe MergeRequests::Mergeability::RunChecksService do expect(service).not_to receive(:execute) end - expect(execute).to match_array([success_result, success_result, success_result, success_result]) + # Since we're only marking one check to be skipped, we expect to receive + # `# of checks - 1` success result objects in return + # + check_count = merge_request.mergeability_checks.count - 1 + success_array = (1..check_count).each_with_object([]) { |_, array| array << success_result } + + expect(execute).to match_array(success_array) end end @@ -56,7 +56,7 @@ RSpec.describe MergeRequests::Mergeability::RunChecksService do let(:merge_check) { instance_double(MergeRequests::Mergeability::CheckCiStatusService) } before do - described_class::CHECKS.each do |check| + merge_request.mergeability_checks.each do |check| allow_next_instance_of(check) do |service| allow(service).to receive(:skip?).and_return(true) end diff --git a/spec/services/merge_requests/post_merge_service_spec.rb b/spec/services/merge_requests/post_merge_service_spec.rb index f0885365f96..e486daae15e 100644 --- a/spec/services/merge_requests/post_merge_service_spec.rb +++ b/spec/services/merge_requests/post_merge_service_spec.rb @@ -106,32 +106,6 @@ RSpec.describe MergeRequests::PostMergeService do expect(merge_request.reload).to be_merged end end - - context 'when async_mr_close_issue feature flag is disabled' do - before do - stub_feature_flags(async_mr_close_issue: false) - end - - it 'executes Issues::CloseService' do - expect_next_instance_of(Issues::CloseService) do |close_service| - expect(close_service).to receive(:execute).with(issue, commit: merge_request) - end - - subject - - expect(merge_request.reload).to be_merged - end - - it 'marks MR as merged regardless of errors when closing issues' do - expect_next_instance_of(Issues::CloseService) do |close_service| - allow(close_service).to receive(:execute).with(issue, commit: merge_request).and_raise(RuntimeError) - end - - expect { subject }.to raise_error(RuntimeError) - - expect(merge_request.reload).to be_merged - end - end end context 'when the merge request has review apps' do diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb index 6e6b4a91e0d..eecf7c21cba 100644 --- a/spec/services/merge_requests/refresh_service_spec.rb +++ b/spec/services/merge_requests/refresh_service_spec.rb @@ -228,6 +228,21 @@ RSpec.describe MergeRequests::RefreshService do expect(@another_merge_request.has_commits?).to be_falsy end + context 'when "push_options: nil" is passed' do + let(:service_instance) { service.new(project: project, current_user: @user, params: { push_options: nil }) } + + subject { service_instance.execute(@oldrev, @newrev, ref) } + + it 'creates a detached merge request pipeline with commits' do + expect { subject } + .to change { @merge_request.pipelines_for_merge_request.count }.by(1) + .and change { @another_merge_request.pipelines_for_merge_request.count }.by(0) + + expect(@merge_request.has_commits?).to be_truthy + expect(@another_merge_request.has_commits?).to be_falsy + end + end + it 'does not create detached merge request pipeline for forked project' do expect { subject } .not_to change { @fork_merge_request.pipelines_for_merge_request.count } @@ -741,47 +756,48 @@ RSpec.describe MergeRequests::RefreshService do refresh_service.execute(oldrev, newrev, 'refs/heads/wip') fixup_merge_request.reload - expect(fixup_merge_request.work_in_progress?).to eq(true) + expect(fixup_merge_request.draft?).to eq(true) expect(fixup_merge_request.notes.last.note).to match( /marked this merge request as \*\*draft\*\* from #{Commit.reference_pattern}/ ) end it 'references the commit that caused the draft status' do - wip_merge_request = create(:merge_request, + draft_merge_request = create(:merge_request, source_project: @project, source_branch: 'wip', target_branch: 'master', target_project: @project) - commits = wip_merge_request.commits + commits = draft_merge_request.commits oldrev = commits.last.id newrev = commits.first.id - wip_commit = wip_merge_request.commits.find(&:work_in_progress?) + draft_commit = draft_merge_request.commits.find(&:draft?) refresh_service.execute(oldrev, newrev, 'refs/heads/wip') - expect(wip_merge_request.reload.notes.last.note).to eq( - "marked this merge request as **draft** from #{wip_commit.id}" + expect(draft_merge_request.reload.notes.last.note).to eq( + "marked this merge request as **draft** from #{draft_commit.id}" ) end it 'does not mark as draft based on commits that do not belong to an MR' do allow(refresh_service).to receive(:find_new_commits) + refresh_service.instance_variable_set("@commits", [ double( id: 'aaaaaaa', sha: 'aaaaaaa', short_id: 'aaaaaaa', title: 'Fix issue', - work_in_progress?: false + draft?: false ), double( id: 'bbbbbbb', sha: 'bbbbbbbb', short_id: 'bbbbbbb', title: 'fixup! Fix issue', - work_in_progress?: true, + draft?: true, to_reference: 'bbbbbbb' ) ]) @@ -789,7 +805,7 @@ RSpec.describe MergeRequests::RefreshService do refresh_service.execute(@oldrev, @newrev, 'refs/heads/master') reload_mrs - expect(@merge_request.work_in_progress?).to be_falsey + expect(@merge_request.draft?).to be_falsey end end diff --git a/spec/services/metrics/dashboard/panel_preview_service_spec.rb b/spec/services/metrics/dashboard/panel_preview_service_spec.rb index 2877d22d1f3..787c61cc918 100644 --- a/spec/services/metrics/dashboard/panel_preview_service_spec.rb +++ b/spec/services/metrics/dashboard/panel_preview_service_spec.rb @@ -45,7 +45,6 @@ RSpec.describe Metrics::Dashboard::PanelPreviewService do ::Gitlab::Metrics::Dashboard::Stages::CommonMetricsInserter, ::Gitlab::Metrics::Dashboard::Stages::MetricEndpointInserter, ::Gitlab::Metrics::Dashboard::Stages::PanelIdsInserter, - ::Gitlab::Metrics::Dashboard::Stages::AlertsInserter, ::Gitlab::Metrics::Dashboard::Stages::UrlValidator ] processor_params = [project, dashboard, sequence, environment: environment] diff --git a/spec/services/notes/copy_service_spec.rb b/spec/services/notes/copy_service_spec.rb index dd11fa15ea8..fd8802e6640 100644 --- a/spec/services/notes/copy_service_spec.rb +++ b/spec/services/notes/copy_service_spec.rb @@ -16,9 +16,8 @@ RSpec.describe Notes::CopyService do let_it_be(:group) { create(:group) } let_it_be(:from_project) { create(:project, :public, group: group) } let_it_be(:to_project) { create(:project, :public, group: group) } - - let(:from_noteable) { create(:issue, project: from_project) } - let(:to_noteable) { create(:issue, project: to_project) } + let_it_be(:from_noteable) { create(:issue, project: from_project) } + let_it_be(:to_noteable) { create(:issue, project: to_project) } subject(:execute_service) { described_class.new(user, from_noteable, to_noteable).execute } @@ -85,6 +84,15 @@ RSpec.describe Notes::CopyService do expect(execute_service).to be_success end end + + it 'copies rendered markdown from note_html' do + expect(Banzai::Renderer).not_to receive(:cacheless_render_field) + + execute_service + + new_note = to_noteable.notes.first + expect(new_note.note_html).to eq(notes.first.note_html) + end end context 'notes with mentions' do @@ -119,6 +127,13 @@ RSpec.describe Notes::CopyService do expect(new_note.author).to eq(note.author) end end + + it 'does not copy rendered markdown from note_html' do + execute_service + + new_note = to_noteable.notes.first + expect(new_note.note_html).not_to eq(note.note_html) + end end context 'notes with upload' do @@ -137,6 +152,13 @@ RSpec.describe Notes::CopyService do expect(note.note_html).not_to eq(new_note.note_html) end end + + it 'does not copy rendered markdown from note_html' do + execute_service + + new_note = to_noteable.notes.first + expect(new_note.note_html).not_to eq(note.note_html) + end end context 'discussion notes' do diff --git a/spec/services/notes/create_service_spec.rb b/spec/services/notes/create_service_spec.rb index c72a9465f20..53b75a3c991 100644 --- a/spec/services/notes/create_service_spec.rb +++ b/spec/services/notes/create_service_spec.rb @@ -359,7 +359,7 @@ RSpec.describe Notes::CreateService do issuable.reload.update!(title: "title") }, expectation: ->(issuable, can_use_quick_action) { - expect(issuable.work_in_progress?).to eq(can_use_quick_action) + expect(issuable.draft?).to eq(can_use_quick_action) } ), # Remove draft status @@ -369,7 +369,7 @@ RSpec.describe Notes::CreateService do issuable.reload.update!(title: "Draft: title") }, expectation: ->(noteable, can_use_quick_action) { - expect(noteable.work_in_progress?).not_to eq(can_use_quick_action) + expect(noteable.draft?).not_to eq(can_use_quick_action) } ) ] diff --git a/spec/services/notification_recipients/build_service_spec.rb b/spec/services/notification_recipients/build_service_spec.rb index ff54d6ccd2f..899d23ec641 100644 --- a/spec/services/notification_recipients/build_service_spec.rb +++ b/spec/services/notification_recipients/build_service_spec.rb @@ -14,6 +14,9 @@ RSpec.describe NotificationRecipients::BuildService do shared_examples 'no N+1 queries' do it 'avoids N+1 queries', :request_store do + # existing N+1 due to multiple users having to be looked up in the project_authorizations table + threshold = project.private? ? 1 : 0 + create_user service.build_new_note_recipients(note) @@ -24,7 +27,7 @@ RSpec.describe NotificationRecipients::BuildService do create_user - expect { service.build_new_note_recipients(note) }.not_to exceed_query_limit(control_count) + expect { service.build_new_note_recipients(note) }.not_to exceed_query_limit(control_count).with_threshold(threshold) end end @@ -66,6 +69,9 @@ RSpec.describe NotificationRecipients::BuildService do shared_examples 'no N+1 queries' do it 'avoids N+1 queries', :request_store do + # existing N+1 due to multiple users having to be looked up in the project_authorizations table + threshold = project.private? ? 1 : 0 + create_user service.build_new_review_recipients(review) @@ -76,7 +82,7 @@ RSpec.describe NotificationRecipients::BuildService do create_user - expect { service.build_new_review_recipients(review) }.not_to exceed_query_limit(control_count) + expect { service.build_new_review_recipients(review) }.not_to exceed_query_limit(control_count).with_threshold(threshold) end end diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index 743a04eabe6..032f35cfc29 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -975,10 +975,17 @@ RSpec.describe NotificationService, :mailer do end describe '#send_new_release_notifications', :deliver_mails_inline do - let(:release) { create(:release, author: current_user) } + let(:release) { create(:release, project: project, author: current_user) } let(:object) { release } let(:action) { notification.send_new_release_notifications(release) } + before_all do + build_team(project) + + update_custom_notification(:new_release, @u_guest_custom, resource: project) + update_custom_notification(:new_release, @u_custom_global) + end + context 'when release author is blocked' do let(:current_user) { create(:user, :blocked) } @@ -994,19 +1001,15 @@ RSpec.describe NotificationService, :mailer do context 'when recipients for a new release exist' do let(:current_user) { create(:user) } - it 'calls new_release_email for each relevant recipient' do - user_1 = create(:user) - user_2 = create(:user) - user_3 = create(:user) - recipient_1 = NotificationRecipient.new(user_1, :custom, custom_action: :new_release) - recipient_2 = NotificationRecipient.new(user_2, :custom, custom_action: :new_release) - allow(NotificationRecipients::BuildService).to receive(:build_new_release_recipients).and_return([recipient_1, recipient_2]) - + it 'notifies the expected users' do notification.send_new_release_notifications(release) - should_email(user_1) - should_email(user_2) - should_not_email(user_3) + should_only_email( + @u_watcher, + @u_guest_watcher, + @u_custom_global, + @u_guest_custom + ) end end end diff --git a/spec/services/packages/cleanup/update_policy_service_spec.rb b/spec/services/packages/cleanup/update_policy_service_spec.rb new file mode 100644 index 00000000000..a11fbb766f5 --- /dev/null +++ b/spec/services/packages/cleanup/update_policy_service_spec.rb @@ -0,0 +1,105 @@ +# frozen_string_literal: true +require 'spec_helper' + +RSpec.describe Packages::Cleanup::UpdatePolicyService do + using RSpec::Parameterized::TableSyntax + + let_it_be_with_reload(:project) { create(:project) } + let_it_be(:current_user) { create(:user) } + + let(:params) { { keep_n_duplicated_package_files: 50 } } + + describe '#execute' do + subject { described_class.new(project: project, current_user: current_user, params: params).execute } + + shared_examples 'creating the policy' do + it 'creates a new one' do + expect { subject }.to change { ::Packages::Cleanup::Policy.count }.from(0).to(1) + + expect(subject.payload[:packages_cleanup_policy]).to be_present + expect(subject.success?).to be_truthy + expect(project.packages_cleanup_policy).to be_persisted + expect(project.packages_cleanup_policy.keep_n_duplicated_package_files).to eq('50') + end + + context 'with invalid parameters' do + let(:params) { { keep_n_duplicated_package_files: 100 } } + + it 'does not create one' do + expect { subject }.not_to change { ::Packages::Cleanup::Policy.count } + + expect(subject.status).to eq(:error) + expect(subject.message).to eq('Keep n duplicated package files is invalid') + end + end + end + + shared_examples 'updating the policy' do + it 'updates the existing one' do + expect { subject }.not_to change { ::Packages::Cleanup::Policy.count } + + expect(subject.payload[:packages_cleanup_policy]).to be_present + expect(subject.success?).to be_truthy + expect(project.packages_cleanup_policy.keep_n_duplicated_package_files).to eq('50') + end + + context 'with invalid parameters' do + let(:params) { { keep_n_duplicated_package_files: 100 } } + + it 'does not update one' do + expect { subject }.not_to change { policy.keep_n_duplicated_package_files } + + expect(subject.status).to eq(:error) + expect(subject.message).to eq('Keep n duplicated package files is invalid') + end + end + end + + shared_examples 'denying access' do + it 'returns an error' do + subject + + expect(subject.message).to eq('Access denied') + expect(subject.status).to eq(:error) + end + end + + context 'with existing container expiration policy' do + let_it_be(:policy) { create(:packages_cleanup_policy, project: project) } + + where(:user_role, :shared_examples_name) do + :maintainer | 'updating the policy' + :developer | 'denying access' + :reporter | 'denying access' + :guest | 'denying access' + :anonymous | 'denying access' + end + + with_them do + before do + project.send("add_#{user_role}", current_user) unless user_role == :anonymous + end + + it_behaves_like params[:shared_examples_name] + end + end + + context 'without existing container expiration policy' do + where(:user_role, :shared_examples_name) do + :maintainer | 'creating the policy' + :developer | 'denying access' + :reporter | 'denying access' + :guest | 'denying access' + :anonymous | 'denying access' + end + + with_them do + before do + project.send("add_#{user_role}", current_user) unless user_role == :anonymous + end + + it_behaves_like params[:shared_examples_name] + end + end + end +end diff --git a/spec/services/packages/go/create_package_service_spec.rb b/spec/services/packages/go/create_package_service_spec.rb index 5c5fec0aa3a..4ca1119fbaa 100644 --- a/spec/services/packages/go/create_package_service_spec.rb +++ b/spec/services/packages/go/create_package_service_spec.rb @@ -35,6 +35,22 @@ RSpec.describe Packages::Go::CreatePackageService do expect(file.file_sha1).not_to be_nil expect(file.file_sha256).not_to be_nil end + + context 'with FIPS mode', :fips_mode do + it 'does not generate file_md5' do + file_name = "#{version.name}.#{type}" + expect(subject.package_files.map { |f| f.file_name }).to include(file_name) + + file = subject.package_files.with_file_name(file_name).first + expect(file).not_to be_nil + expect(file.file).not_to be_nil + expect(file.size).to eq(file.file.size) + expect(file.file_name).to eq(file_name) + expect(file.file_md5).to be_nil + expect(file.file_sha1).not_to be_nil + expect(file.file_sha256).not_to be_nil + end + end end describe '#execute' do diff --git a/spec/services/packages/maven/metadata/append_package_file_service_spec.rb b/spec/services/packages/maven/metadata/append_package_file_service_spec.rb index c406ab93630..f3a90d31158 100644 --- a/spec/services/packages/maven/metadata/append_package_file_service_spec.rb +++ b/spec/services/packages/maven/metadata/append_package_file_service_spec.rb @@ -22,6 +22,18 @@ RSpec.describe ::Packages::Maven::Metadata::AppendPackageFileService do expect_file("#{metadata_file_name}.sha256") expect_file("#{metadata_file_name}.sha512") end + + context 'with FIPS mode', :fips_mode do + it 'does not generate file_md5' do + expect { subject }.to change { package.package_files.count }.by(4) + expect(subject).to be_success + + expect_file(metadata_file_name, with_content: content, with_content_type: 'application/xml', fips: true) + expect_file("#{metadata_file_name}.sha1", fips: true) + expect_file("#{metadata_file_name}.sha256", fips: true) + expect_file("#{metadata_file_name}.sha512", fips: true) + end + end end context 'with nil content' do @@ -36,17 +48,22 @@ RSpec.describe ::Packages::Maven::Metadata::AppendPackageFileService do it_behaves_like 'returning an error service response', message: 'package is not set' end - def expect_file(file_name, with_content: nil, with_content_type: '') + def expect_file(file_name, fips: false, with_content: nil, with_content_type: '') package_file = package.package_files.recent.with_file_name(file_name).first expect(package_file.file).to be_present expect(package_file.file_name).to eq(file_name) expect(package_file.size).to be > 0 - expect(package_file.file_md5).to be_present expect(package_file.file_sha1).to be_present expect(package_file.file_sha256).to be_present expect(package_file.file.content_type).to eq(with_content_type) + if fips + expect(package_file.file_md5).not_to be_present + else + expect(package_file.file_md5).to be_present + end + if with_content expect(package_file.file.read).to eq(with_content) end diff --git a/spec/services/packages/rubygems/create_gemspec_service_spec.rb b/spec/services/packages/rubygems/create_gemspec_service_spec.rb index 198e978a47e..839fb4d955a 100644 --- a/spec/services/packages/rubygems/create_gemspec_service_spec.rb +++ b/spec/services/packages/rubygems/create_gemspec_service_spec.rb @@ -24,5 +24,18 @@ RSpec.describe Packages::Rubygems::CreateGemspecService do expect(gemspec_file.file_sha1).not_to be_nil expect(gemspec_file.file_sha256).not_to be_nil end + + context 'with FIPS mode', :fips_mode do + it 'does not generate file_md5' do + expect { subject }.to change { package.package_files.count }.by(1) + + gemspec_file = package.package_files.find_by(file_name: "#{gemspec.name}.gemspec") + expect(gemspec_file.file).not_to be_nil + expect(gemspec_file.size).not_to be_nil + expect(gemspec_file.file_md5).to be_nil + expect(gemspec_file.file_sha1).not_to be_nil + expect(gemspec_file.file_sha256).not_to be_nil + end + end end end diff --git a/spec/services/pages/delete_service_spec.rb b/spec/services/pages/delete_service_spec.rb index e02e8e72e0b..0c0b2c0431b 100644 --- a/spec/services/pages/delete_service_spec.rb +++ b/spec/services/pages/delete_service_spec.rb @@ -43,4 +43,10 @@ RSpec.describe Pages::DeleteService do service.execute end.to change { PagesDeployment.count }.by(-1) end + + it 'publishes a ProjectDeleted event with project id and namespace id' do + expected_data = { project_id: project.id, namespace_id: project.namespace_id } + + expect { service.execute }.to publish_event(Pages::PageDeletedEvent).with(expected_data) + end end diff --git a/spec/services/pages_domains/create_acme_order_service_spec.rb b/spec/services/pages_domains/create_acme_order_service_spec.rb index 35b2cc56973..b882c253613 100644 --- a/spec/services/pages_domains/create_acme_order_service_spec.rb +++ b/spec/services/pages_domains/create_acme_order_service_spec.rb @@ -38,13 +38,21 @@ RSpec.describe PagesDomains::CreateAcmeOrderService do expect(challenge).to have_received(:request_validation).ordered end - it 'generates and saves private key' do + it 'generates and saves private key: rsa' do + stub_feature_flags(pages_lets_encrypt_ecdsa: false) service.execute saved_order = PagesDomainAcmeOrder.last expect { OpenSSL::PKey::RSA.new(saved_order.private_key) }.not_to raise_error end + it 'generates and saves private key: ec' do + service.execute + + saved_order = PagesDomainAcmeOrder.last + expect { OpenSSL::PKey::EC.new(saved_order.private_key) }.not_to raise_error + end + it 'properly saves order attributes' do service.execute diff --git a/spec/services/projects/after_rename_service_spec.rb b/spec/services/projects/after_rename_service_spec.rb index a8db87e48d0..a9329f092fa 100644 --- a/spec/services/projects/after_rename_service_spec.rb +++ b/spec/services/projects/after_rename_service_spec.rb @@ -64,22 +64,11 @@ RSpec.describe Projects::AfterRenameService do allow(project_storage).to receive(:rename_repo) { true } end - context 'when the project has pages deployed' do - it 'schedules a move of the pages directory' do - allow(project).to receive(:pages_deployed?).and_return(true) - - expect(PagesTransferWorker).to receive(:perform_async).with('rename_project', anything) - - service_execute - end - end - context 'when the project does not have pages deployed' do it 'does nothing with the pages directory' do allow(project).to receive(:pages_deployed?).and_return(false) expect(PagesTransferWorker).not_to receive(:perform_async) - expect(Gitlab::PagesTransfer).not_to receive(:new) service_execute end @@ -172,29 +161,6 @@ RSpec.describe Projects::AfterRenameService do end end - context 'gitlab pages' do - context 'when the project has pages deployed' do - it 'schedules a move of the pages directory' do - allow(project).to receive(:pages_deployed?).and_return(true) - - expect(PagesTransferWorker).to receive(:perform_async).with('rename_project', anything) - - service_execute - end - end - - context 'when the project does not have pages deployed' do - it 'does nothing with the pages directory' do - allow(project).to receive(:pages_deployed?).and_return(false) - - expect(PagesTransferWorker).not_to receive(:perform_async) - expect(Gitlab::PagesTransfer).not_to receive(:new) - - service_execute - end - end - end - context 'attachments' do let(:uploader) { create(:upload, :issuable_upload, :with_file, model: project) } let(:file_uploader) { build(:file_uploader, project: project) } diff --git a/spec/services/projects/autocomplete_service_spec.rb b/spec/services/projects/autocomplete_service_spec.rb index ed043bacf31..54a21d2f22b 100644 --- a/spec/services/projects/autocomplete_service_spec.rb +++ b/spec/services/projects/autocomplete_service_spec.rb @@ -158,7 +158,6 @@ RSpec.describe Projects::AutocompleteService do subject { described_class.new(project, user).contacts.as_json } before do - stub_feature_flags(customer_relations: true) group.add_developer(user) end diff --git a/spec/services/projects/destroy_rollback_service_spec.rb b/spec/services/projects/destroy_rollback_service_spec.rb deleted file mode 100644 index 3eaacc8c1e7..00000000000 --- a/spec/services/projects/destroy_rollback_service_spec.rb +++ /dev/null @@ -1,46 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Projects::DestroyRollbackService do - let_it_be(:user) { create(:user) } - let_it_be(:project) { create(:project, :repository, namespace: user.namespace) } - - let(:repository) { project.repository } - let(:repository_storage) { project.repository_storage } - - subject { described_class.new(project, user, {}).execute } - - describe '#execute' do - let(:path) { repository.disk_path + '.git' } - let(:removal_path) { "#{repository.disk_path}+#{project.id}#{Repositories::DestroyService::DELETED_FLAG}.git" } - - before do - aggregate_failures do - expect(TestEnv.storage_dir_exists?(repository_storage, path)).to be_truthy - expect(TestEnv.storage_dir_exists?(repository_storage, removal_path)).to be_falsey - end - - # Don't run sidekiq to check if renamed repository exists - Sidekiq::Testing.fake! { destroy_project(project, user, {}) } - - aggregate_failures do - expect(TestEnv.storage_dir_exists?(repository_storage, path)).to be_falsey - expect(TestEnv.storage_dir_exists?(repository_storage, removal_path)).to be_truthy - end - end - - it 'restores the repositories' do - Sidekiq::Testing.fake! { subject } - - aggregate_failures do - expect(TestEnv.storage_dir_exists?(repository_storage, path)).to be_truthy - expect(TestEnv.storage_dir_exists?(repository_storage, removal_path)).to be_falsey - end - end - end - - def destroy_project(project, user, params = {}) - Projects::DestroyService.new(project, user, params).execute - end -end diff --git a/spec/services/projects/destroy_service_spec.rb b/spec/services/projects/destroy_service_spec.rb index cd923720631..c00438199fd 100644 --- a/spec/services/projects/destroy_service_spec.rb +++ b/spec/services/projects/destroy_service_spec.rb @@ -9,7 +9,6 @@ RSpec.describe Projects::DestroyService, :aggregate_failures, :event_store_publi let!(:project) { create(:project, :repository, namespace: user.namespace) } let(:path) { project.repository.disk_path } - let(:remove_path) { removal_path(path) } let(:async) { false } # execute or async_execute before do @@ -24,7 +23,6 @@ RSpec.describe Projects::DestroyService, :aggregate_failures, :event_store_publi expect(Project.unscoped.all).not_to include(project) expect(project.gitlab_shell.repository_exists?(project.repository_storage, path + '.git')).to be_falsey - expect(project.gitlab_shell.repository_exists?(project.repository_storage, remove_path + '.git')).to be_falsey end it 'publishes a ProjectDeleted event with project id and namespace id' do @@ -73,6 +71,18 @@ RSpec.describe Projects::DestroyService, :aggregate_failures, :event_store_publi end it_behaves_like 'deleting the project' + + context 'when project is undergoing refresh' do + let!(:build_artifacts_size_refresh) { create(:project_build_artifacts_size_refresh, :pending, project: project) } + + it 'does not log about artifact deletion but continues to delete artifacts' do + expect(Gitlab::ProjectStatsRefreshConflictsLogger).not_to receive(:warn_artifact_deletion_during_stats_refresh) + + expect { destroy_project(project, user, {}) } + .to change { Ci::JobArtifact.count }.by(-2) + .and change { Projects::BuildArtifactsSizeRefresh.count }.by(-1) + end + end end end @@ -192,10 +202,6 @@ RSpec.describe Projects::DestroyService, :aggregate_failures, :event_store_publi it do expect(project.gitlab_shell.repository_exists?(project.repository_storage, path + '.git')).to be_falsey end - - it do - expect(project.gitlab_shell.repository_exists?(project.repository_storage, remove_path + '.git')).to be_truthy - end end context 'when flushing caches fail due to Git errors' do @@ -392,36 +398,13 @@ RSpec.describe Projects::DestroyService, :aggregate_failures, :event_store_publi end end - context 'repository +deleted path removal' do - context 'regular phase' do - it 'schedules +deleted removal of existing repos' do - service = described_class.new(project, user, {}) - allow(service).to receive(:schedule_stale_repos_removal) - - expect(Repositories::ShellDestroyService).to receive(:new).and_call_original - expect(GitlabShellWorker).to receive(:perform_in) - .with(5.minutes, :remove_repository, project.repository_storage, removal_path(project.disk_path)) - - service.execute + context 'repository removal' do + it 'removal of existing repos' do + expect_next_instances_of(Repositories::DestroyService, 2) do |instance| + expect(instance).to receive(:execute).and_return(status: :success) end - end - - context 'stale cleanup' do - let(:async) { true } - - it 'schedules +deleted wiki and repo removal' do - allow(ProjectDestroyWorker).to receive(:perform_async) - - expect(Repositories::ShellDestroyService).to receive(:new).with(project.repository).and_call_original - expect(GitlabShellWorker).to receive(:perform_in) - .with(10.minutes, :remove_repository, project.repository_storage, removal_path(project.disk_path)) - - expect(Repositories::ShellDestroyService).to receive(:new).with(project.wiki.repository).and_call_original - expect(GitlabShellWorker).to receive(:perform_in) - .with(10.minutes, :remove_repository, project.repository_storage, removal_path(project.wiki.disk_path)) - destroy_project(project, user, {}) - end + described_class.new(project, user, {}).execute end end @@ -480,7 +463,6 @@ RSpec.describe Projects::DestroyService, :aggregate_failures, :event_store_publi expect do destroy_project(project, user) end.to change(WebHook, :count).by(-2) - .and change(WebHookLog, :count).by(-1) end context 'when an error is raised deleting webhooks' do @@ -541,7 +523,6 @@ RSpec.describe Projects::DestroyService, :aggregate_failures, :event_store_publi expect(Project.unscoped.all).not_to include(project) expect(project.gitlab_shell.repository_exists?(project.repository_storage, path + '.git')).to be_falsey - expect(project.gitlab_shell.repository_exists?(project.repository_storage, remove_path + '.git')).to be_falsey expect(project.all_pipelines).to be_empty expect(project.builds).to be_empty end @@ -550,8 +531,4 @@ RSpec.describe Projects::DestroyService, :aggregate_failures, :event_store_publi def destroy_project(project, user, params = {}) described_class.new(project, user, params).public_send(async ? :async_execute : :execute) end - - def removal_path(path) - "#{path}+#{project.id}#{Repositories::DestroyService::DELETED_FLAG}" - end end diff --git a/spec/services/projects/refresh_build_artifacts_size_statistics_service_spec.rb b/spec/services/projects/refresh_build_artifacts_size_statistics_service_spec.rb index 41487e9ea48..6a715312097 100644 --- a/spec/services/projects/refresh_build_artifacts_size_statistics_service_spec.rb +++ b/spec/services/projects/refresh_build_artifacts_size_statistics_service_spec.rb @@ -52,6 +52,12 @@ RSpec.describe Projects::RefreshBuildArtifactsSizeStatisticsService, :clean_gitl expect { service.execute }.to change { refresh.reload.last_job_artifact_id.to_i }.to(artifact_3.id) end + it 'updates the last_job_artifact_id to the ID of the last artifact from the project' do + expect { service.execute } + .to change { refresh.reload.last_job_artifact_id_on_refresh_start.to_i } + .to(project.job_artifacts.last.id) + end + it 'requeues the refresh job' do service.execute expect(refresh.reload).to be_pending @@ -63,7 +69,8 @@ RSpec.describe Projects::RefreshBuildArtifactsSizeStatisticsService, :clean_gitl :project_build_artifacts_size_refresh, :pending, project: project, - last_job_artifact_id: artifact_3.id + last_job_artifact_id: artifact_3.id, + last_job_artifact_id_on_refresh_start: artifact_4.id ) end @@ -77,6 +84,10 @@ RSpec.describe Projects::RefreshBuildArtifactsSizeStatisticsService, :clean_gitl expect(refresh.reload.last_job_artifact_id).to eq(artifact_3.id) end + it 'keeps the last_job_artifact_id_on_refresh_start unchanged' do + expect(refresh.reload.last_job_artifact_id_on_refresh_start).to eq(artifact_4.id) + end + it 'keeps the state of the refresh record at running' do expect(refresh.reload).to be_running end diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb index e547ace1d9f..bebe80b710b 100644 --- a/spec/services/projects/transfer_service_spec.rb +++ b/spec/services/projects/transfer_service_spec.rb @@ -53,9 +53,6 @@ RSpec.describe Projects::TransferService do allow_next_instance_of(Gitlab::UploadsTransfer) do |service| allow(service).to receive(:move_project).and_return(true) end - allow_next_instance_of(Gitlab::PagesTransfer) do |service| - allow(service).to receive(:move_project).and_return(true) - end group.add_owner(user) end @@ -725,15 +722,6 @@ RSpec.describe Projects::TransferService do group.add_owner(user) end - it 'schedules a job when pages are deployed' do - project.mark_pages_as_deployed - - expect(PagesTransferWorker).to receive(:perform_async) - .with("move_project", [project.path, user.namespace.full_path, group.full_path]) - - execute_transfer - end - it 'does not schedule a job when no pages are deployed' do expect(PagesTransferWorker).not_to receive(:perform_async) diff --git a/spec/services/projects/update_pages_service_spec.rb b/spec/services/projects/update_pages_service_spec.rb index 777162b6196..cbbed82aa0b 100644 --- a/spec/services/projects/update_pages_service_spec.rb +++ b/spec/services/projects/update_pages_service_spec.rb @@ -205,6 +205,25 @@ RSpec.describe Projects::UpdatePagesService do include_examples 'fails with outdated reference message' end end + + context 'when uploaded deployment size is wrong' do + it 'raises an error' do + allow_next_instance_of(PagesDeployment) do |deployment| + allow(deployment) + .to receive(:size) + .and_return(file.size + 1) + end + + expect do + expect(execute).not_to eq(:success) + + expect(GenericCommitStatus.last.description).to eq("Error: The uploaded artifact size does not match the expected value.") + project.pages_metadatum.reload + expect(project.pages_metadatum).not_to be_deployed + expect(project.pages_metadatum.pages_deployment).to be_ni + end.to raise_error(Projects::UpdatePagesService::WrongUploadedDeploymentSizeError) + end + end end end diff --git a/spec/services/quick_actions/interpret_service_spec.rb b/spec/services/quick_actions/interpret_service_spec.rb index f7a22b1b92f..f7ed6006099 100644 --- a/spec/services/quick_actions/interpret_service_spec.rb +++ b/spec/services/quick_actions/interpret_service_spec.rb @@ -122,7 +122,7 @@ RSpec.describe QuickActions::InterpretService do inprogress # populate the label _, updates, _ = service.execute(content, issuable) - expect(updates).to eq(add_label_ids: [bug.id, inprogress.id]) + expect(updates).to match(add_label_ids: contain_exactly(bug.id, inprogress.id)) end it 'returns the label message' do @@ -130,7 +130,10 @@ RSpec.describe QuickActions::InterpretService do inprogress # populate the label _, _, message = service.execute(content, issuable) - expect(message).to eq("Added #{bug.to_reference(format: :name)} #{inprogress.to_reference(format: :name)} labels.") + # Compare message without making assumptions about ordering. + expect(message).to match %r{Added ~".*" ~".*" labels.} + expect(message).to include(bug.to_reference(format: :name)) + expect(message).to include(inprogress.to_reference(format: :name)) end end @@ -318,32 +321,40 @@ RSpec.describe QuickActions::InterpretService do end shared_examples 'draft command' do - it 'returns wip_event: "wip" if content contains /draft' do + it 'returns wip_event: "draft"' do _, updates, _ = service.execute(content, issuable) - expect(updates).to eq(wip_event: 'wip') + expect(updates).to eq(wip_event: 'draft') end - it 'returns the wip message' do + it 'returns the draft message' do _, _, message = service.execute(content, issuable) expect(message).to eq("Marked this #{issuable.to_ability_name.humanize(capitalize: false)} as a draft.") end end - shared_examples 'undraft command' do - it 'returns wip_event: "unwip" if content contains /draft' do - issuable.update!(title: issuable.wip_title) + shared_examples 'draft/ready command no action' do + it 'returns the no action message if there is no change to the status' do + _, _, message = service.execute(content, issuable) + + expect(message).to eq("No change to this #{issuable.to_ability_name.humanize(capitalize: false)}'s draft status.") + end + end + + shared_examples 'ready command' do + it 'returns wip_event: "ready"' do + issuable.update!(title: issuable.draft_title) _, updates, _ = service.execute(content, issuable) - expect(updates).to eq(wip_event: 'unwip') + expect(updates).to eq(wip_event: 'ready') end - it 'returns the unwip message' do - issuable.update!(title: issuable.wip_title) + it 'returns the ready message' do + issuable.update!(title: issuable.draft_title) _, _, message = service.execute(content, issuable) - expect(message).to eq("Unmarked this #{issuable.to_ability_name.humanize(capitalize: false)} as a draft.") + expect(message).to eq("Marked this #{issuable.to_ability_name.humanize(capitalize: false)} as ready.") end end @@ -1196,6 +1207,64 @@ RSpec.describe QuickActions::InterpretService do let(:issuable) { merge_request } end + context 'with a colon label' do + let(:bug) { create(:label, project: project, title: 'Category:Bug') } + let(:inprogress) { create(:label, project: project, title: 'status:in:progress') } + + context 'when quoted' do + let(:content) { %(/label ~"#{inprogress.title}" ~"#{bug.title}" ~unknown) } + + it_behaves_like 'label command' do + let(:issuable) { merge_request } + end + + it_behaves_like 'label command' do + let(:issuable) { issue } + end + end + + context 'when unquoted' do + let(:content) { %(/label ~#{inprogress.title} ~#{bug.title} ~unknown) } + + it_behaves_like 'label command' do + let(:issuable) { merge_request } + end + + it_behaves_like 'label command' do + let(:issuable) { issue } + end + end + end + + context 'with a scoped label' do + let(:bug) { create(:label, :scoped, project: project) } + let(:inprogress) { create(:label, project: project, title: 'three::part::label') } + + context 'when quoted' do + let(:content) { %(/label ~"#{inprogress.title}" ~"#{bug.title}" ~unknown) } + + it_behaves_like 'label command' do + let(:issuable) { merge_request } + end + + it_behaves_like 'label command' do + let(:issuable) { issue } + end + end + + context 'when unquoted' do + let(:content) { %(/label ~#{inprogress.title} ~#{bug.title} ~unknown) } + + it_behaves_like 'label command' do + let(:issuable) { merge_request } + end + + it_behaves_like 'label command' do + let(:issuable) { issue } + end + end + end + it_behaves_like 'multiple label command' do let(:content) { %(/label ~"#{inprogress.title}" \n/label ~#{bug.title}) } let(:issuable) { issue } @@ -1306,11 +1375,21 @@ RSpec.describe QuickActions::InterpretService do let(:issuable) { merge_request } end - it_behaves_like 'undraft command' do + it_behaves_like 'ready command' do let(:content) { '/draft' } let(:issuable) { merge_request } end + it_behaves_like 'draft/ready command no action' do + let(:content) { '/ready' } + let(:issuable) { merge_request } + end + + it_behaves_like 'ready command' do + let(:content) { '/ready' } + let(:issuable) { merge_request } + end + it_behaves_like 'failed command', 'Could not apply remove_due_date command.' do let(:content) { '/remove_due_date' } let(:issuable) { merge_request } @@ -2333,24 +2412,6 @@ RSpec.describe QuickActions::InterpretService do create(:issue_customer_relations_contact, issue: issue, contact: existing_contact) end - context 'with feature flag disabled' do - before do - stub_feature_flags(customer_relations: false) - end - - it 'add_contacts command does not add the contact' do - _, updates, _ = add_command - - expect(updates).to be_empty - end - - it 'remove_contacts command does not remove the contact' do - _, updates, _ = remove_command - - expect(updates).to be_empty - end - end - it 'add_contacts command adds the contact' do _, updates, message = add_command @@ -2644,7 +2705,24 @@ RSpec.describe QuickActions::InterpretService do it 'includes the new status' do _, explanations = service.explain(content, merge_request) - expect(explanations).to eq(['Marks this merge request as a draft.']) + expect(explanations).to match_array(['Marks this merge request as a draft.']) + end + end + + describe 'ready command' do + let(:content) { '/ready' } + + it 'includes the new status' do + merge_request.update!(title: merge_request.draft_title) + _, explanations = service.explain(content, merge_request) + + expect(explanations).to match_array(['Marks this merge request as ready.']) + end + + it 'includes the no change message when status unchanged' do + _, explanations = service.explain(content, merge_request) + + expect(explanations).to match_array(["No change to this merge request's draft status."]) end end @@ -2805,12 +2883,6 @@ RSpec.describe QuickActions::InterpretService do expect(explanations).to be_empty end - - it '/remove_contacts is not available' do - _, explanations = service.explain(remove_contacts, issue) - - expect(explanations).to be_empty - end end context 'when group has contacts' do @@ -2822,10 +2894,22 @@ RSpec.describe QuickActions::InterpretService do expect(explanations).to contain_exactly("Add customer relation contact(s).") end - it '/remove_contacts is available' do - _, explanations = service.explain(remove_contacts, issue) + context 'when issue has no contacts' do + it '/remove_contacts is not available' do + _, explanations = service.explain(remove_contacts, issue) - expect(explanations).to contain_exactly("Remove customer relation contact(s).") + expect(explanations).to be_empty + end + end + + context 'when issue has contacts' do + let!(:issue_contact) { create(:issue_customer_relations_contact, issue: issue, contact: contact) } + + it '/remove_contacts is available' do + _, explanations = service.explain(remove_contacts, issue) + + expect(explanations).to contain_exactly("Remove customer relation contact(s).") + end end end end diff --git a/spec/services/releases/create_service_spec.rb b/spec/services/releases/create_service_spec.rb index d53fc968e2a..566d73a3b75 100644 --- a/spec/services/releases/create_service_spec.rb +++ b/spec/services/releases/create_service_spec.rb @@ -6,10 +6,11 @@ RSpec.describe Releases::CreateService do let(:project) { create(:project, :repository) } let(:user) { create(:user) } let(:tag_name) { project.repository.tag_names.first } + let(:tag_message) { nil } let(:tag_sha) { project.repository.find_tag(tag_name).dereferenced_target.sha } let(:name) { 'Bionic Beaver' } let(:description) { 'Awesome release!' } - let(:params) { { tag: tag_name, name: name, description: description, ref: ref } } + let(:params) { { tag: tag_name, name: name, description: description, ref: ref, tag_message: tag_message } } let(:ref) { nil } let(:service) { described_class.new(project, user, params) } @@ -68,6 +69,24 @@ RSpec.describe Releases::CreateService do expect(result[:tag]).not_to be_nil expect(result[:release]).not_to be_nil end + + context 'and tag_message is provided' do + let(:ref) { 'master' } + let(:tag_name) { 'foobar' } + let(:tag_message) { 'Annotated tag message' } + + it_behaves_like 'a successful release creation' + + it 'creates a tag if the tag does not exist' do + expect(project.repository.ref_exists?("refs/tags/#{tag_name}")).to be_falsey + + result = service.execute + expect(result[:status]).to eq(:success) + expect(result[:tag]).not_to be_nil + expect(result[:release]).not_to be_nil + expect(project.repository.find_tag(tag_name).message).to eq(tag_message) + end + end end context 'there already exists a release on a tag' do diff --git a/spec/services/repositories/changelog_service_spec.rb b/spec/services/repositories/changelog_service_spec.rb index ddb8e7e1182..82546ae810b 100644 --- a/spec/services/repositories/changelog_service_spec.rb +++ b/spec/services/repositories/changelog_service_spec.rb @@ -78,7 +78,7 @@ RSpec.describe Repositories::ChangelogService do recorder = ActiveRecord::QueryRecorder.new { service.execute(commit_to_changelog: commit_to_changelog) } changelog = project.repository.blob_at('master', 'CHANGELOG.md')&.data - expect(recorder.count).to eq(9) + expect(recorder.count).to eq(10) expect(changelog).to include('Title 1', 'Title 2') end @@ -148,6 +148,52 @@ RSpec.describe Repositories::ChangelogService do expect(changelog).to include('Title 1', 'Title 2') end end + + it 'avoids N+1 queries', :request_store do + RequestStore.clear! + + request = ->(to) do + described_class + .new(project, creator, version: '1.0.0', from: sha1, to: to) + .execute(commit_to_changelog: false) + end + + control = ActiveRecord::QueryRecorder.new { request.call(sha2) } + + RequestStore.clear! + + expect { request.call(sha3) }.not_to exceed_query_limit(control.count) + end + + context 'when one of commits does not exist' do + let(:service) { described_class.new(project, creator, version: '1.0.0', from: 'master', to: '54321') } + + it 'raises an exception' do + expect { service.execute(commit_to_changelog: false) }.to raise_error(Gitlab::Changelog::Error) + end + end + + context 'when commit range exceeds the limit' do + let(:service) { described_class.new(project, creator, version: '1.0.0', from: sha1) } + + before do + stub_const("#{described_class.name}::COMMITS_LIMIT", 2) + end + + it 'raises an exception' do + expect { service.execute(commit_to_changelog: false) }.to raise_error(Gitlab::Changelog::Error) + end + + context 'when feature flag is off' do + before do + stub_feature_flags(changelog_commits_limitation: false) + end + + it 'returns the changelog' do + expect(service.execute(commit_to_changelog: false)).to include('Title 1', 'Title 2', 'Title 3') + end + end + end end describe '#start_of_commit_range' do diff --git a/spec/services/repositories/destroy_rollback_service_spec.rb b/spec/services/repositories/destroy_rollback_service_spec.rb deleted file mode 100644 index a52dff62760..00000000000 --- a/spec/services/repositories/destroy_rollback_service_spec.rb +++ /dev/null @@ -1,85 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Repositories::DestroyRollbackService do - let_it_be(:user) { create(:user) } - - let!(:project) { create(:project, :repository, namespace: user.namespace) } - let(:repository) { project.repository } - let(:path) { repository.disk_path } - let(:remove_path) { "#{path}+#{project.id}#{described_class::DELETED_FLAG}" } - - subject { described_class.new(repository).execute } - - before do - # Dont run sidekiq to check if renamed repository exists - Sidekiq::Testing.fake! { destroy_project(project, user) } - end - - it 'moves the repository from the +deleted folder' do - expect(project.gitlab_shell.repository_exists?(project.repository_storage, remove_path + '.git')).to be_truthy - expect(project.gitlab_shell.repository_exists?(project.repository_storage, path + '.git')).to be_falsey - - subject - - expect(project.gitlab_shell.repository_exists?(project.repository_storage, remove_path + '.git')).to be_falsey - expect(project.gitlab_shell.repository_exists?(project.repository_storage, path + '.git')).to be_truthy - end - - it 'logs the successful action' do - expect(Gitlab::AppLogger).to receive(:info) - - subject - end - - it 'flushes the repository cache' do - expect(repository).to receive(:before_delete) - - subject - end - - it 'returns success and does not perform any action if repository path does not exist' do - expect(repository).to receive(:disk_path).and_return('foo') - expect(repository).not_to receive(:before_delete) - - expect(subject[:status]).to eq :success - end - - it 'gracefully handles exception if the repository does not exist on disk' do - expect(repository).to receive(:before_delete).and_raise(Gitlab::Git::Repository::NoRepository) - expect(subject[:status]).to eq :success - end - - context 'when move operation cannot be performed' do - let(:service) { described_class.new(repository) } - - before do - expect(service).to receive(:mv_repository).and_return(false) - end - - it 'returns error' do - result = service.execute - - expect(result[:status]).to eq :error - end - - it 'logs the error' do - expect(Gitlab::AppLogger).to receive(:error) - - service.execute - end - - context 'when repository does not exist' do - it 'returns success' do - allow(service).to receive(:repo_exists?).and_return(true, false) - - expect(service.execute[:status]).to eq :success - end - end - end - - def destroy_project(project, user) - Projects::DestroyService.new(project, user, {}).execute - end -end diff --git a/spec/services/repositories/destroy_service_spec.rb b/spec/services/repositories/destroy_service_spec.rb index 3766467d708..565a18d501a 100644 --- a/spec/services/repositories/destroy_service_spec.rb +++ b/spec/services/repositories/destroy_service_spec.rb @@ -8,31 +8,19 @@ RSpec.describe Repositories::DestroyService do let!(:project) { create(:project, :repository, namespace: user.namespace) } let(:repository) { project.repository } let(:path) { repository.disk_path } - let(:remove_path) { "#{path}+#{project.id}#{described_class::DELETED_FLAG}" } subject { described_class.new(repository).execute } - it 'moves the repository to a +deleted folder' do + it 'removes the repository' do expect(project.gitlab_shell.repository_exists?(project.repository_storage, path + '.git')).to be_truthy - expect(project.gitlab_shell.repository_exists?(project.repository_storage, remove_path + '.git')).to be_falsey subject - expect(project.gitlab_shell.repository_exists?(project.repository_storage, path + '.git')).to be_falsey - expect(project.gitlab_shell.repository_exists?(project.repository_storage, remove_path + '.git')).to be_truthy - end - - it 'schedules the repository deletion' do - subject - - expect(Repositories::ShellDestroyService).to receive(:new).with(repository).and_call_original - - expect(GitlabShellWorker).to receive(:perform_in) - .with(Repositories::ShellDestroyService::REPO_REMOVAL_DELAY, :remove_repository, project.repository_storage, remove_path) - - # Because GitlabShellWorker is inside a run_after_commit callback we need to + # Because the removal happens inside a run_after_commit callback we need to # trigger the callback project.touch + + expect(project.gitlab_shell.repository_exists?(project.repository_storage, path + '.git')).to be_falsey end context 'on a read-only instance' do @@ -41,22 +29,12 @@ RSpec.describe Repositories::DestroyService do end it 'schedules the repository deletion' do - expect(Repositories::ShellDestroyService).to receive(:new).with(repository).and_call_original - - expect(GitlabShellWorker).to receive(:perform_in) - .with(Repositories::ShellDestroyService::REPO_REMOVAL_DELAY, :remove_repository, project.repository_storage, remove_path) + expect(project.gitlab_shell.repository_exists?(project.repository_storage, path + '.git')).to be_truthy subject - end - end - - it 'removes the repository', :sidekiq_inline do - subject - project.touch - - expect(project.gitlab_shell.repository_exists?(project.repository_storage, path + '.git')).to be_falsey - expect(project.gitlab_shell.repository_exists?(project.repository_storage, remove_path + '.git')).to be_falsey + expect(project.gitlab_shell.repository_exists?(project.repository_storage, path + '.git')).to be_falsey + end end it 'flushes the repository cache' do @@ -77,48 +55,20 @@ RSpec.describe Repositories::DestroyService do expect(subject[:status]).to eq :success end - context 'when move operation cannot be performed' do - let(:service) { described_class.new(repository) } - - before do - expect(service).to receive(:mv_repository).and_return(false) - end - - it 'returns error' do - expect(service.execute[:status]).to eq :error - end - - it 'logs the error' do - expect(Gitlab::AppLogger).to receive(:error) - - service.execute - end - - context 'when repository does not exist' do - it 'returns success' do - allow(service).to receive(:repo_exists?).and_return(true, false) - - expect(Repositories::ShellDestroyService).not_to receive(:new) - expect(service.execute[:status]).to eq :success - end - end - end - context 'with a project wiki repository' do let(:project) { create(:project, :wiki_repo) } let(:repository) { project.wiki.repository } it 'schedules the repository deletion' do - subject - - expect(Repositories::ShellDestroyService).to receive(:new).with(repository).and_call_original + expect(project.gitlab_shell.repository_exists?(project.repository_storage, path + '.git')).to be_truthy - expect(GitlabShellWorker).to receive(:perform_in) - .with(Repositories::ShellDestroyService::REPO_REMOVAL_DELAY, :remove_repository, project.repository_storage, remove_path) + subject - # Because GitlabShellWorker is inside a run_after_commit callback we need to + # Because the removal happens inside a run_after_commit callback we need to # trigger the callback project.touch + + expect(project.gitlab_shell.repository_exists?(project.repository_storage, path + '.git')).to be_falsey end end end diff --git a/spec/services/repositories/shell_destroy_service_spec.rb b/spec/services/repositories/shell_destroy_service_spec.rb deleted file mode 100644 index 65168a1784a..00000000000 --- a/spec/services/repositories/shell_destroy_service_spec.rb +++ /dev/null @@ -1,26 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Repositories::ShellDestroyService do - let_it_be(:user) { create(:user) } - - let!(:project) { create(:project, :repository, namespace: user.namespace) } - let(:path) { project.repository.disk_path } - let(:remove_path) { "#{path}+#{project.id}#{described_class::DELETED_FLAG}" } - - it 'returns success if the repository is nil' do - expect(GitlabShellWorker).not_to receive(:perform_in) - - result = described_class.new(nil).execute - - expect(result[:status]).to eq :success - end - - it 'schedules the repository deletion' do - expect(GitlabShellWorker).to receive(:perform_in) - .with(described_class::REPO_REMOVAL_DELAY, :remove_repository, project.repository_storage, remove_path) - - described_class.new(project.repository).execute - end -end diff --git a/spec/services/resource_access_tokens/create_service_spec.rb b/spec/services/resource_access_tokens/create_service_spec.rb index 5a88929334b..127948549b0 100644 --- a/spec/services/resource_access_tokens/create_service_spec.rb +++ b/spec/services/resource_access_tokens/create_service_spec.rb @@ -268,10 +268,36 @@ RSpec.describe ResourceAccessTokens::CreateService do end it_behaves_like 'allows creation of bot with valid params' + + context 'when user specifies an access level of OWNER for the bot' do + let_it_be(:params) { { access_level: Gitlab::Access::OWNER } } + + context 'when the executor is a MAINTAINER' do + it 'does not add the bot user with the specified access level in the resource' do + response = subject + + expect(response.error?).to be true + expect(response.errors).to include('Could not provision owner access to project access token') + end + end + + context 'when the executor is an OWNER' do + let_it_be(:user) { project.first_owner } + + it 'adds the bot user with the specified access level in the resource' do + response = subject + + access_token = response.payload[:access_token] + bot_user = access_token.user + + expect(resource.members.owners.map(&:user_id)).to include(bot_user.id) + end + end + end end end - context 'when resource is a project' do + context 'when resource is a group' do let_it_be(:resource_type) { 'group' } let_it_be(:resource) { group } @@ -283,6 +309,18 @@ RSpec.describe ResourceAccessTokens::CreateService do end it_behaves_like 'allows creation of bot with valid params' + + context 'when user specifies an access level of OWNER for the bot' do + let_it_be(:params) { { access_level: Gitlab::Access::OWNER } } + + it 'adds the bot user with the specified access level in the resource' do + response = subject + access_token = response.payload[:access_token] + bot_user = access_token.user + + expect(resource.members.owners.map(&:user_id)).to include(bot_user.id) + end + end end end end diff --git a/spec/services/service_response_spec.rb b/spec/services/service_response_spec.rb index 082ee4ddc67..3ede90fbc44 100644 --- a/spec/services/service_response_spec.rb +++ b/spec/services/service_response_spec.rb @@ -2,7 +2,10 @@ require 'fast_spec_helper' +require 're2' + require_relative '../../app/services/service_response' +require_relative '../../lib/gitlab/error_tracking' RSpec.describe ServiceResponse do describe '.success' do @@ -94,4 +97,76 @@ RSpec.describe ServiceResponse do expect(described_class.error(message: 'error message').errors).to eq(['error message']) end end + + describe '#track_and_raise_exception' do + context 'when successful' do + let(:response) { described_class.success } + + it 'returns self' do + expect(response.track_and_raise_exception).to be response + end + end + + context 'when an error' do + let(:response) { described_class.error(message: 'bang') } + + it 'tracks and raises' do + expect(::Gitlab::ErrorTracking).to receive(:track_and_raise_exception) + .with(StandardError.new('bang'), {}) + + response.track_and_raise_exception + end + + it 'allows specification of error class' do + error = Class.new(StandardError) + expect(::Gitlab::ErrorTracking).to receive(:track_and_raise_exception) + .with(error.new('bang'), {}) + + response.track_and_raise_exception(as: error) + end + + it 'allows extra data for tracking' do + expect(::Gitlab::ErrorTracking).to receive(:track_and_raise_exception) + .with(StandardError.new('bang'), { foo: 1, bar: 2 }) + + response.track_and_raise_exception(foo: 1, bar: 2) + end + end + end + + describe '#track_exception' do + context 'when successful' do + let(:response) { described_class.success } + + it 'returns self' do + expect(response.track_exception).to be response + end + end + + context 'when an error' do + let(:response) { described_class.error(message: 'bang') } + + it 'tracks' do + expect(::Gitlab::ErrorTracking).to receive(:track_exception) + .with(StandardError.new('bang'), {}) + + expect(response.track_exception).to be response + end + + it 'allows specification of error class' do + error = Class.new(StandardError) + expect(::Gitlab::ErrorTracking).to receive(:track_exception) + .with(error.new('bang'), {}) + + expect(response.track_exception(as: error)).to be response + end + + it 'allows extra data for tracking' do + expect(::Gitlab::ErrorTracking).to receive(:track_exception) + .with(StandardError.new('bang'), { foo: 1, bar: 2 }) + + expect(response.track_exception(foo: 1, bar: 2)).to be response + end + end + end end diff --git a/spec/services/snippets/bulk_destroy_service_spec.rb b/spec/services/snippets/bulk_destroy_service_spec.rb index 2f399d10188..2d2bdd116d1 100644 --- a/spec/services/snippets/bulk_destroy_service_spec.rb +++ b/spec/services/snippets/bulk_destroy_service_spec.rb @@ -22,8 +22,8 @@ RSpec.describe Snippets::BulkDestroyService do it 'deletes the snippets in bulk' do response = nil - expect(Repositories::ShellDestroyService).to receive(:new).with(personal_snippet.repository).and_call_original - expect(Repositories::ShellDestroyService).to receive(:new).with(project_snippet.repository).and_call_original + expect(Repositories::DestroyService).to receive(:new).with(personal_snippet.repository).and_call_original + expect(Repositories::DestroyService).to receive(:new).with(project_snippet.repository).and_call_original aggregate_failures do expect do @@ -94,12 +94,6 @@ RSpec.describe Snippets::BulkDestroyService do it_behaves_like 'error is raised' do let(:error_message) { 'Failed to delete snippet repositories.' } end - - it 'tries to rollback the repository' do - expect(subject).to receive(:attempt_rollback_repositories) - - subject.execute - end end context 'when an error is raised deleting the records' do @@ -110,22 +104,17 @@ RSpec.describe Snippets::BulkDestroyService do it_behaves_like 'error is raised' do let(:error_message) { 'Failed to remove snippets.' } end - - it 'tries to rollback the repository' do - expect(subject).to receive(:attempt_rollback_repositories) - - subject.execute - end end context 'when snippet does not have a repository attached' do let!(:snippet_without_repo) { create(:personal_snippet, author: user) } - it 'does not schedule anything for the snippet without repository and return success' do + it 'returns success' do response = nil - expect(Repositories::ShellDestroyService).to receive(:new).with(personal_snippet.repository).and_call_original - expect(Repositories::ShellDestroyService).to receive(:new).with(project_snippet.repository).and_call_original + expect(Repositories::DestroyService).to receive(:new).with(personal_snippet.repository).and_call_original + expect(Repositories::DestroyService).to receive(:new).with(project_snippet.repository).and_call_original + expect(Repositories::DestroyService).to receive(:new).with(snippet_without_repo.repository).and_call_original expect do response = subject.execute @@ -136,38 +125,6 @@ RSpec.describe Snippets::BulkDestroyService do end end - describe '#attempt_rollback_repositories' do - before do - Repositories::DestroyService.new(personal_snippet.repository).execute - end - - it 'rollbacks the repository' do - error_msg = personal_snippet.disk_path + "+#{personal_snippet.id}+deleted.git" - expect(repository_exists?(personal_snippet, error_msg)).to be_truthy - - subject.__send__(:attempt_rollback_repositories) - - aggregate_failures do - expect(repository_exists?(personal_snippet, error_msg)).to be_falsey - expect(repository_exists?(personal_snippet)).to be_truthy - end - end - - context 'when an error is raised' do - before do - allow_next_instance_of(Repositories::DestroyRollbackService) do |instance| - allow(instance).to receive(:execute).and_return({ status: :error }) - end - end - - it 'logs the error' do - expect(Gitlab::AppLogger).to receive(:error).with(/\ARepository .* in path .* could not be rolled back\z/).twice - - subject.__send__(:attempt_rollback_repositories) - end - end - end - def repository_exists?(snippet, path = snippet.disk_path + ".git") gitlab_shell.repository_exists?(snippet.snippet_repository.shard_name, path) end diff --git a/spec/services/snippets/destroy_service_spec.rb b/spec/services/snippets/destroy_service_spec.rb index e53d00b9ca1..23765243dd6 100644 --- a/spec/services/snippets/destroy_service_spec.rb +++ b/spec/services/snippets/destroy_service_spec.rb @@ -41,7 +41,6 @@ RSpec.describe Snippets::DestroyService do shared_examples 'deletes the snippet repository' do it 'removes the snippet repository' do expect(snippet.repository.exists?).to be_truthy - expect(GitlabShellWorker).to receive(:perform_in) expect_next_instance_of(Repositories::DestroyService) do |instance| expect(instance).to receive(:execute).and_call_original end @@ -57,12 +56,6 @@ RSpec.describe Snippets::DestroyService do end it_behaves_like 'an unsuccessful destroy' - - it 'does not try to rollback repository' do - expect(Repositories::DestroyRollbackService).not_to receive(:new) - - subject - end end context 'when a destroy error is raised' do @@ -71,12 +64,6 @@ RSpec.describe Snippets::DestroyService do end it_behaves_like 'an unsuccessful destroy' - - it 'attempts to rollback the repository' do - expect(Repositories::DestroyRollbackService).to receive(:new).and_call_original - - subject - end end context 'when repository is nil' do diff --git a/spec/services/static_site_editor/config_service_spec.rb b/spec/services/static_site_editor/config_service_spec.rb deleted file mode 100644 index fed373828a1..00000000000 --- a/spec/services/static_site_editor/config_service_spec.rb +++ /dev/null @@ -1,126 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe StaticSiteEditor::ConfigService do - let_it_be(:project) { create(:project, :repository) } - let_it_be(:user) { create(:user) } - - # params - let(:ref) { 'master' } - let(:path) { 'README.md' } - let(:return_url) { double(:return_url) } - - # stub data - let(:generated_data) { { generated: true } } - let(:file_data) { { file: true } } - - describe '#execute' do - subject(:execute) do - described_class.new( - container: project, - current_user: user, - params: { - ref: ref, - path: path, - return_url: return_url - } - ).execute - end - - context 'when insufficient permission' do - it 'returns an error' do - expect(execute).to be_error - expect(execute.message).to eq('Insufficient permissions to read configuration') - end - end - - context 'for developer' do - before do - project.add_developer(user) - - allow_next_instance_of(Gitlab::StaticSiteEditor::Config::GeneratedConfig) do |config| - allow(config).to receive(:data) { generated_data } - end - end - - context 'when reading file from repo fails with an unexpected error' do - let(:unexpected_error) { RuntimeError.new('some unexpected error') } - - before do - allow(project.repository).to receive(:blob_data_at).and_raise(unexpected_error) - end - - it 'returns an error response' do - expect(Gitlab::ErrorTracking).to receive(:track_and_raise_exception).with(unexpected_error).and_call_original - expect { execute }.to raise_error(unexpected_error) - end - end - - context 'when file is missing' do - before do - allow(project.repository).to receive(:blob_data_at).and_raise(GRPC::NotFound) - expect_next_instance_of(Gitlab::StaticSiteEditor::Config::FileConfig, '{}') do |config| - allow(config).to receive(:valid?) { true } - allow(config).to receive(:to_hash_with_defaults) { file_data } - end - end - - it 'returns default config' do - expect(execute).to be_success - expect(execute.payload).to eq(generated: true, file: true) - end - end - - context 'when file is present' do - before do - allow(project.repository).to receive(:blob_data_at).with(ref, anything) do - config_content - end - end - - context 'and configuration is not valid' do - let(:config_content) { 'invalid content' } - - before do - expect_next_instance_of(Gitlab::StaticSiteEditor::Config::FileConfig, config_content) do |config| - error = 'error' - allow(config).to receive_message_chain('errors.first') { error } - allow(config).to receive(:valid?) { false } - end - end - - it 'returns an error' do - expect(execute).to be_error - expect(execute.message).to eq('Invalid configuration format') - end - end - - context 'and configuration is valid' do - # NOTE: This has to be a valid config, even though it is mocked, because - # `expect_next_instance_of` executes the constructor logic. - let(:config_content) { 'static_site_generator: middleman' } - - before do - expect_next_instance_of(Gitlab::StaticSiteEditor::Config::FileConfig, config_content) do |config| - allow(config).to receive(:valid?) { true } - allow(config).to receive(:to_hash_with_defaults) { file_data } - end - end - - it 'returns merged generated data and config file data' do - expect(execute).to be_success - expect(execute.payload).to eq(generated: true, file: true) - end - - it 'returns an error if any keys would be overwritten by the merge' do - generated_data[:duplicate_key] = true - file_data[:duplicate_key] = true - expect(execute).to be_error - expect(execute.message).to match(/duplicate key.*duplicate_key.*found/i) - end - end - end - end - end -end diff --git a/spec/services/terraform/remote_state_handler_spec.rb b/spec/services/terraform/remote_state_handler_spec.rb index ca392849d49..19c1d4109e9 100644 --- a/spec/services/terraform/remote_state_handler_spec.rb +++ b/spec/services/terraform/remote_state_handler_spec.rb @@ -33,6 +33,14 @@ RSpec.describe Terraform::RemoteStateHandler do it 'returns the state' do expect(subject.find_with_lock).to eq(state) end + + context 'with a state scheduled for deletion' do + let!(:state) { create(:terraform_state, :deletion_in_progress, project: project, name: 'state') } + + it 'raises an exception' do + expect { subject.find_with_lock }.to raise_error(described_class::StateDeletedError) + end + end end end end @@ -84,6 +92,13 @@ RSpec.describe Terraform::RemoteStateHandler do .to raise_error(described_class::StateLockedError) end + it 'raises an exception if the state is scheduled for deletion' do + create(:terraform_state, :deletion_in_progress, project: project, name: 'new-state') + + expect { handler.handle_with_lock } + .to raise_error(described_class::StateDeletedError) + end + context 'user does not have permission to modify state' do let(:user) { developer } @@ -127,24 +142,28 @@ RSpec.describe Terraform::RemoteStateHandler do expect { handler.lock! }.to raise_error(described_class::StateLockedError) end + + it 'raises an exception when the state exists and is scheduled for deletion' do + create(:terraform_state, :deletion_in_progress, project: project, name: 'new-state') + + expect { handler.lock! }.to raise_error(described_class::StateDeletedError) + end end describe '#unlock!' do - let(:lock_id) { 'abc-abc' } + let_it_be(:state) { create(:terraform_state, :locked, project: project, name: 'new-state', lock_xid: 'abc-abc') } + + let(:lock_id) { state.lock_xid } subject(:handler) do described_class.new( project, user, - name: 'new-state', + name: state.name, lock_id: lock_id ) end - before do - create(:terraform_state, :locked, project: project, name: 'new-state', lock_xid: 'abc-abc') - end - it 'unlocks the state' do state = handler.unlock! @@ -169,6 +188,15 @@ RSpec.describe Terraform::RemoteStateHandler do .to raise_error(described_class::StateLockedError) end end + + context 'with a state scheduled for deletion' do + it 'raises an exception' do + state.update!(deleted_at: Time.current) + + expect { handler.unlock! } + .to raise_error(described_class::StateDeletedError) + end + end end end end diff --git a/spec/services/terraform/states/destroy_service_spec.rb b/spec/services/terraform/states/destroy_service_spec.rb new file mode 100644 index 00000000000..5acf32cd73c --- /dev/null +++ b/spec/services/terraform/states/destroy_service_spec.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Terraform::States::DestroyService do + let_it_be(:state) { create(:terraform_state, :with_version, :deletion_in_progress) } + + let(:file) { instance_double(Terraform::StateUploader, relative_path: 'path') } + + before do + allow_next_found_instance_of(Terraform::StateVersion) do |version| + allow(version).to receive(:file).and_return(file) + end + end + + describe '#execute' do + subject { described_class.new(state).execute } + + it 'removes version files from object storage, followed by the state record' do + expect(file).to receive(:remove!).once + expect(state).to receive(:destroy!) + + subject + end + + context 'state is not marked for deletion' do + let(:state) { create(:terraform_state) } + + it 'does not delete the state' do + expect(state).not_to receive(:destroy!) + + subject + end + end + end +end diff --git a/spec/services/terraform/states/trigger_destroy_service_spec.rb b/spec/services/terraform/states/trigger_destroy_service_spec.rb new file mode 100644 index 00000000000..2e96331779c --- /dev/null +++ b/spec/services/terraform/states/trigger_destroy_service_spec.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Terraform::States::TriggerDestroyService do + let_it_be(:project) { create(:project) } + let_it_be(:user) { create(:user, maintainer_projects: [project]) } + + describe '#execute', :aggregate_failures do + let_it_be(:state) { create(:terraform_state, project: project) } + + subject { described_class.new(state, current_user: user).execute } + + it 'marks the state as deleted and schedules a cleanup worker' do + expect(Terraform::States::DestroyWorker).to receive(:perform_async).with(state.id).once + + expect(subject).to be_success + expect(state.deleted_at).to be_like_time(Time.current) + end + + shared_examples 'unable to delete state' do + it 'does not modify the state' do + expect(Terraform::States::DestroyWorker).not_to receive(:perform_async) + + expect { subject }.not_to change(state, :deleted_at) + expect(subject).to be_error + expect(subject.message).to eq(message) + end + end + + context 'user does not have permission' do + let(:user) { create(:user, developer_projects: [project]) } + let(:message) { 'You have insufficient permissions to delete this state' } + + include_examples 'unable to delete state' + end + + context 'state is locked' do + let(:state) { create(:terraform_state, :locked, project: project) } + let(:message) { 'Cannot remove a locked state' } + + include_examples 'unable to delete state' + end + end +end diff --git a/spec/services/user_project_access_changed_service_spec.rb b/spec/services/user_project_access_changed_service_spec.rb index 438db6b987b..be4f205afb5 100644 --- a/spec/services/user_project_access_changed_service_spec.rb +++ b/spec/services/user_project_access_changed_service_spec.rb @@ -31,6 +31,19 @@ RSpec.describe UserProjectAccessChangedService do priority: described_class::LOW_PRIORITY) end + it 'permits medium-priority operation' do + expect(AuthorizedProjectUpdate::UserRefreshWithLowUrgencyWorker).to( + receive(:bulk_perform_in).with( + described_class::MEDIUM_DELAY, + [[1], [2]], + { batch_delay: 30.seconds, batch_size: 100 } + ) + ) + + described_class.new([1, 2]).execute(blocking: false, + 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, diff --git a/spec/services/web_hook_service_spec.rb b/spec/services/web_hook_service_spec.rb index b99bc860523..068550ec234 100644 --- a/spec/services/web_hook_service_spec.rb +++ b/spec/services/web_hook_service_spec.rb @@ -5,6 +5,7 @@ require 'spec_helper' RSpec.describe WebHookService, :request_store, :clean_gitlab_redis_shared_state do include StubRequests + let(:ellipsis) { '…' } let_it_be(:project) { create(:project) } let_it_be_with_reload(:project_hook) { create(:project_hook, project: project) } @@ -268,6 +269,20 @@ RSpec.describe WebHookService, :request_store, :clean_gitlab_redis_shared_state end context 'execution logging' do + let(:default_log_data) do + { + trigger: 'push_hooks', + url: project_hook.url, + request_headers: headers, + request_data: data, + response_body: 'Success', + response_headers: {}, + response_status: 200, + execution_duration: be > 0, + internal_error_message: nil + } + end + context 'with success' do before do stub_full_request(project_hook.url, method: :post).to_return(status: 200, body: 'Success') @@ -280,7 +295,7 @@ RSpec.describe WebHookService, :request_store, :clean_gitlab_redis_shared_state expect(::WebHooks::LogExecutionWorker).not_to receive(:perform_async) expect(::WebHooks::LogExecutionService) .to receive(:new) - .with(hook: project_hook, log_data: Hash, response_category: :ok) + .with(hook: project_hook, log_data: default_log_data, response_category: :ok) .and_return(double(execute: nil)) service_instance.execute @@ -291,17 +306,7 @@ RSpec.describe WebHookService, :request_store, :clean_gitlab_redis_shared_state expect(WebHooks::LogExecutionWorker).to receive(:perform_async) .with( project_hook.id, - hash_including( - trigger: 'push_hooks', - url: project_hook.url, - request_headers: headers, - request_data: data, - response_body: 'Success', - response_headers: {}, - response_status: 200, - execution_duration: be > 0, - internal_error_message: nil - ), + hash_including(default_log_data), :ok, nil ) @@ -328,15 +333,10 @@ RSpec.describe WebHookService, :request_store, :clean_gitlab_redis_shared_state .with( project_hook.id, hash_including( - trigger: 'push_hooks', - url: project_hook.url, - request_headers: headers, - request_data: data, - response_body: 'Bad request', - response_headers: {}, - response_status: 400, - execution_duration: be > 0, - internal_error_message: nil + default_log_data.merge( + response_body: 'Bad request', + response_status: 400 + ) ), :failed, nil @@ -356,15 +356,11 @@ RSpec.describe WebHookService, :request_store, :clean_gitlab_redis_shared_state .with( project_hook.id, hash_including( - trigger: 'push_hooks', - url: project_hook.url, - request_headers: headers, - request_data: data, - response_body: '', - response_headers: {}, - response_status: 'internal error', - execution_duration: be > 0, - internal_error_message: 'Some HTTP Post error' + default_log_data.merge( + response_body: '', + response_status: 'internal error', + internal_error_message: 'Some HTTP Post error' + ) ), :error, nil @@ -383,23 +379,137 @@ RSpec.describe WebHookService, :request_store, :clean_gitlab_redis_shared_state expect(WebHooks::LogExecutionWorker).to receive(:perform_async) .with( project_hook.id, - hash_including( - trigger: 'push_hooks', - url: project_hook.url, - request_headers: headers, - request_data: data, - response_body: '', - response_headers: {}, - response_status: 200, - execution_duration: be > 0, - internal_error_message: nil - ), + hash_including(default_log_data.merge(response_body: '')), + :ok, + nil + ) + + service_instance.execute + end + end + + context 'with oversize response body' do + let(:oversize_body) { 'a' * (described_class::RESPONSE_BODY_SIZE_LIMIT + 1) } + let(:stripped_body) { 'a' * (described_class::RESPONSE_BODY_SIZE_LIMIT - ellipsis.bytesize) + ellipsis } + + before do + stub_full_request(project_hook.url, method: :post).to_return(status: 200, body: oversize_body) + end + + it 'queues LogExecutionWorker with stripped response_body' do + expect(WebHooks::LogExecutionWorker).to receive(:perform_async) + .with( + project_hook.id, + hash_including(default_log_data.merge(response_body: stripped_body)), + :ok, + nil + ) + + service_instance.execute + end + end + + context 'with massive amount of headers' do + let(:response_headers) do + (1..described_class::RESPONSE_HEADERS_COUNT_LIMIT + 1).to_a.to_h do |num| + ["header-#{num}", SecureRandom.hex(num)] + end + end + + let(:expected_response_headers) do + (1..described_class::RESPONSE_HEADERS_COUNT_LIMIT).to_a.to_h do |num| + # Capitalized + ["Header-#{num}", response_headers["header-#{num}"]] + end + end + + before do + stub_full_request(project_hook.url, method: :post).to_return( + status: 200, body: 'Success', headers: response_headers + ) + end + + it 'queues LogExecutionWorker with limited amount of headers' do + expect(WebHooks::LogExecutionWorker).to receive(:perform_async) + .with( + project_hook.id, + hash_including(default_log_data.merge(response_headers: expected_response_headers)), + :ok, + nil + ) + + service_instance.execute + end + end + + context 'with oversize header' do + let(:oversize_header) { 'a' * (described_class::RESPONSE_HEADERS_SIZE_LIMIT + 1) } + let(:stripped_header) { 'a' * (described_class::RESPONSE_HEADERS_SIZE_LIMIT - ellipsis.bytesize) + ellipsis } + let(:response_headers) { { 'oversized-header' => oversize_header } } + let(:expected_response_headers) { { 'Oversized-Header' => stripped_header } } + + before do + stub_full_request(project_hook.url, method: :post).to_return( + status: 200, body: 'Success', headers: response_headers + ) + end + + it 'queues LogExecutionWorker with stripped header value' do + expect(WebHooks::LogExecutionWorker).to receive(:perform_async) + .with( + project_hook.id, + hash_including(default_log_data.merge(response_headers: expected_response_headers)), + :ok, + nil + ) + + service_instance.execute + end + end + + context 'with log data exceeding Sidekiq limit' do + before do + stub_full_request(project_hook.url, method: :post).to_return(status: 200, body: 'Success') + end + + it 'queues LogExecutionWorker with request_data overrided in the second attempt' do + expect(WebHooks::LogExecutionWorker).to receive(:perform_async) + .with( + project_hook.id, + hash_including(default_log_data), + :ok, + nil + ) + .and_raise( + Gitlab::SidekiqMiddleware::SizeLimiter::ExceedLimitError.new(WebHooks::LogExecutionWorker, 100, 50) + ) + .ordered + expect(WebHooks::LogExecutionWorker).to receive(:perform_async) + .with( + project_hook.id, + hash_including(default_log_data.merge(request_data: WebHookLog::OVERSIZE_REQUEST_DATA)), :ok, nil ) + .and_call_original + .ordered service_instance.execute end + + context 'new log data still exceeds limit' do + before do + allow(WebHooks::LogExecutionWorker).to receive(:perform_async).and_raise( + Gitlab::SidekiqMiddleware::SizeLimiter::ExceedLimitError.new(WebHooks::LogExecutionWorker, 100, 50) + ) + end + + it 'raises an exception' do + expect do + service_instance.execute + end.to raise_error(Gitlab::SidekiqMiddleware::SizeLimiter::ExceedLimitError) + end + end end end end @@ -411,7 +521,7 @@ RSpec.describe WebHookService, :request_store, :clean_gitlab_redis_shared_state def expect_to_rate_limit(hook, threshold:, throttled: false) expect(Gitlab::ApplicationRateLimiter).to receive(:throttled?) - .with(:web_hook_calls, scope: [hook], threshold: threshold) + .with(:web_hook_calls, scope: [hook.parent.root_namespace], threshold: threshold) .and_return(throttled) end @@ -460,13 +570,8 @@ RSpec.describe WebHookService, :request_store, :clean_gitlab_redis_shared_state end end - context 'when the hook is throttled (via Redis)', :clean_gitlab_redis_rate_limiting do + context 'when the hook is throttled (via Redis)', :clean_gitlab_redis_rate_limiting, :freeze_time do before do - # Set a high interval to avoid intermittent failures in CI - allow(Gitlab::ApplicationRateLimiter).to receive(:rate_limits).and_return( - web_hook_calls: { interval: 1.day } - ) - expect_to_perform_worker(project_hook).exactly(threshold).times threshold.times { service_instance.async_execute } diff --git a/spec/services/web_hooks/destroy_service_spec.rb b/spec/services/web_hooks/destroy_service_spec.rb index 5269fe08ac0..4d9bb18e540 100644 --- a/spec/services/web_hooks/destroy_service_spec.rb +++ b/spec/services/web_hooks/destroy_service_spec.rb @@ -7,50 +7,46 @@ RSpec.describe WebHooks::DestroyService do subject { described_class.new(user) } - shared_examples 'batched destroys' do - it 'destroys all hooks in batches' do - stub_const("#{described_class}::BATCH_SIZE", 1) - expect(subject).to receive(:delete_web_hook_logs_in_batches).exactly(4).times.and_call_original - - expect do - status = subject.execute(hook) - expect(status[:async]).to be false - end - .to change { WebHook.count }.from(1).to(0) - .and change { WebHookLog.count }.from(3).to(0) - end - - it 'returns an error if sync destroy fails' do - expect(hook).to receive(:destroy).and_return(false) + describe '#execute' do + %i[system_hook project_hook].each do |factory| + context "deleting a #{factory}" do + let!(:hook) { create(factory) } # rubocop: disable Rails/SaveBang (false-positive!) + let!(:log) { create_list(:web_hook_log, 3, web_hook: hook) } - result = subject.sync_destroy(hook) - - expect(result[:status]).to eq(:error) - expect(result[:message]).to eq("Unable to destroy #{hook.model_name.human}") - end + it 'is successful' do + expect(subject.execute(hook)).to be_success + end - it 'schedules an async delete' do - stub_const('WebHooks::DestroyService::LOG_COUNT_THRESHOLD', 1) + it 'destroys the hook' do + expect { subject.execute(hook) }.to change(WebHook, :count).from(1).to(0) + end - expect(WebHooks::DestroyWorker).to receive(:perform_async).with(user.id, hook.id).and_call_original + it 'does not destroy logs' do + expect { subject.execute(hook) }.not_to change(WebHookLog, :count) + end - status = subject.execute(hook) + it 'schedules the destruction of logs' do + expect(WebHooks::LogDestroyWorker).to receive(:perform_async).with({ 'hook_id' => hook.id }) + expect(Gitlab::AppLogger).to receive(:info).with(match(/scheduled a deletion of logs/)) - expect(status[:async]).to be true - end - end + subject.execute(hook) + end - context 'with system hook' do - let!(:hook) { create(:system_hook, url: "http://example.com") } - let!(:log) { create_list(:web_hook_log, 3, web_hook: hook) } + context 'when the hook fails to destroy' do + before do + allow(hook).to receive(:destroy).and_return(false) + end - it_behaves_like 'batched destroys' - end + it 'is not a success' do + expect(WebHooks::LogDestroyWorker).not_to receive(:perform_async) - context 'with project hook' do - let!(:hook) { create(:project_hook) } - let!(:log) { create_list(:web_hook_log, 3, web_hook: hook) } + r = subject.execute(hook) - it_behaves_like 'batched destroys' + expect(r).to be_error + expect(r[:message]).to match %r{Unable to destroy} + end + end + end + end end end diff --git a/spec/services/web_hooks/log_destroy_service_spec.rb b/spec/services/web_hooks/log_destroy_service_spec.rb new file mode 100644 index 00000000000..7634726e5a4 --- /dev/null +++ b/spec/services/web_hooks/log_destroy_service_spec.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe WebHooks::LogDestroyService do + subject(:service) { described_class.new(hook.id) } + + describe '#execute' do + shared_examples 'deletes web hook logs for hook' do + before do + create_list(:web_hook_log, 3, web_hook: hook) + hook.destroy! # The LogDestroyService is expected to be called _after_ hook destruction + end + + it 'deletes the logs' do + expect { service.execute } + .to change(WebHookLog, :count).from(3).to(0) + end + + context 'when the data-set exceeds the batch size' do + before do + stub_const("#{described_class}::BATCH_SIZE", 2) + end + + it 'deletes the logs' do + expect { service.execute } + .to change(WebHookLog, :count).from(3).to(0) + end + end + + context 'when it encounters an error' do + before do + allow(WebHookLog).to receive(:delete_batch_for).and_raise(StandardError.new('bang')) + end + + it 'reports the error' do + expect(service.execute) + .to be_error + .and have_attributes(message: 'bang') + end + end + end + + context 'with system hook' do + let!(:hook) { create(:system_hook, url: "http://example.com") } + + it_behaves_like 'deletes web hook logs for hook' + end + + context 'with project hook' do + let!(:hook) { create(:project_hook) } + + it_behaves_like 'deletes web hook logs for hook' + end + end +end diff --git a/spec/services/work_items/update_service_spec.rb b/spec/services/work_items/update_service_spec.rb index b2d3f428899..9030326dadb 100644 --- a/spec/services/work_items/update_service_spec.rb +++ b/spec/services/work_items/update_service_spec.rb @@ -8,11 +8,12 @@ RSpec.describe WorkItems::UpdateService do let_it_be_with_reload(:work_item) { create(:work_item, project: project, assignees: [developer]) } let(:spam_params) { double } + let(:widget_params) { {} } let(:opts) { {} } let(:current_user) { developer } describe '#execute' do - subject(:update_work_item) { described_class.new(project: project, current_user: current_user, params: opts, spam_params: spam_params).execute(work_item) } + subject(:update_work_item) { described_class.new(project: project, current_user: current_user, params: opts, spam_params: spam_params, widget_params: widget_params).execute(work_item) } before do stub_spam_services @@ -69,5 +70,17 @@ RSpec.describe WorkItems::UpdateService do end end end + + context 'when updating widgets' do + context 'for the description widget' do + let(:widget_params) { { description_widget: { description: 'changed' } } } + + it 'updates the description of the work item' do + update_work_item + + expect(work_item.description).to eq('changed') + end + end + end end end |