diff options
Diffstat (limited to 'spec/workers')
37 files changed, 1057 insertions, 527 deletions
diff --git a/spec/workers/bulk_import_worker_spec.rb b/spec/workers/bulk_import_worker_spec.rb index ec8550bb3bc..c96e5ace124 100644 --- a/spec/workers/bulk_import_worker_spec.rb +++ b/spec/workers/bulk_import_worker_spec.rb @@ -137,5 +137,174 @@ RSpec.describe BulkImportWorker, feature_category: :importers do end end end + + context 'when importing a 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) + + subject.perform(bulk_import.id) + + 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 importing a 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) + + subject.perform(bulk_import.id) + + 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 + subject.perform(bulk_import.id) + + 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', + bulk_import_entity_id: entity.id, + bulk_import_id: entity.bulk_import_id, + bulk_import_entity_type: entity.source_type, + source_full_path: entity.source_full_path, + importer: 'gitlab_migration', + 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', + bulk_import_entity_id: entity.id, + bulk_import_id: entity.bulk_import_id, + bulk_import_entity_type: entity.source_type, + source_full_path: entity.source_full_path, + importer: 'gitlab_migration', + pipeline_name: 'PipelineClass5', + minimum_source_version: '16.0.0', + maximum_source_version: nil, + source_version: '15.0.0' + }) + end + + subject.perform(bulk_import.id) + 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) + + subject.perform(bulk_import.id) + + 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) + + subject.perform(bulk_import.id) + + 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.perform_inline(bulk_import_1.id) + described_class.perform_inline(bulk_import_2.id) + + 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/workers/bulk_imports/finish_project_import_worker_spec.rb b/spec/workers/bulk_imports/finish_project_import_worker_spec.rb new file mode 100644 index 00000000000..3f5f8477667 --- /dev/null +++ b/spec/workers/bulk_imports/finish_project_import_worker_spec.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe BulkImports::FinishProjectImportWorker, feature_category: :importers do + let_it_be(:project) { create(:project) } + let_it_be(:job_args) { [project.id] } + + describe '#perform' do + it_behaves_like 'an idempotent worker' do + it 'calls after_import for the project' do + expect_next_found_instance_of(Project) do |project| + expect(project).to receive(:after_import) + end + + described_class.new.perform(project.id) + end + + context 'when no project is found' do + let(:job_args) { nil } + + it 'returns without error' do + expect { described_class.new.perform(project.id) }.not_to raise_error + end + end + end + end +end diff --git a/spec/workers/bulk_imports/pipeline_batch_worker_spec.rb b/spec/workers/bulk_imports/pipeline_batch_worker_spec.rb index c10e1b486ab..3c33910b62c 100644 --- a/spec/workers/bulk_imports/pipeline_batch_worker_spec.rb +++ b/spec/workers/bulk_imports/pipeline_batch_worker_spec.rb @@ -73,6 +73,16 @@ RSpec.describe BulkImports::PipelineBatchWorker, feature_category: :importers do end end + context 'when batch status is started' do + let(:batch) { create(:bulk_import_batch_tracker, :started, tracker: tracker) } + + it 'runs the given pipeline batch successfully' do + subject.perform(batch.id) + + expect(batch.reload).to be_finished + end + end + context 'when exclusive lease cannot be obtained' do it 'does not run the pipeline' do expect(subject).to receive(:try_obtain_lease).and_return(false) diff --git a/spec/workers/click_house/events_sync_worker_spec.rb b/spec/workers/click_house/events_sync_worker_spec.rb index 8f328839cfd..01267db36a7 100644 --- a/spec/workers/click_house/events_sync_worker_spec.rb +++ b/spec/workers/click_house/events_sync_worker_spec.rb @@ -3,48 +3,125 @@ require 'spec_helper' RSpec.describe ClickHouse::EventsSyncWorker, feature_category: :value_stream_management do - let(:databases) { { main: :some_db } } let(:worker) { described_class.new } - before do - allow(ClickHouse::Client.configuration).to receive(:databases).and_return(databases) - end - - include_examples 'an idempotent worker' do - context 'when the event_sync_worker_for_click_house feature flag is on' do + it_behaves_like 'an idempotent worker' do + context 'when the event_sync_worker_for_click_house feature flag is on', :click_house do before do stub_feature_flags(event_sync_worker_for_click_house: true) end - it 'returns true' do - expect(worker).to receive(:log_extra_metadata_on_done).with(:result, { status: :processed }) + context 'when there is nothing to sync' do + it 'adds metadata for the worker' do + expect(worker).to receive(:log_extra_metadata_on_done).with(:result, + { status: :processed, records_inserted: 0, reached_end_of_table: true }) - worker.perform + worker.perform + + events = ClickHouse::Client.select('SELECT * FROM events', :main) + expect(events).to be_empty + end end - context 'when no ClickHouse databases are configured' do - let(:databases) { {} } + context 'when syncing records' do + let_it_be(:group) { create(:group) } + let_it_be(:project) { create(:project, group: group) } + let_it_be(:issue) { create(:issue, project: project) } + let_it_be(:project_event2) { create(:event, :closed, project: project, target: issue) } + let_it_be(:event_without_parent) { create(:event, :joined, project: nil, group: nil) } + let_it_be(:group_event) { create(:event, :created, group: group, project: nil) } + let_it_be(:project_event1) { create(:event, :created, project: project, target: issue) } + # looks invalid but we have some records like this on PRD - it 'skips execution' do - expect(worker).to receive(:log_extra_metadata_on_done).with(:result, { status: :disabled }) + it 'inserts all records' do + expect(worker).to receive(:log_extra_metadata_on_done).with(:result, + { status: :processed, records_inserted: 4, reached_end_of_table: true }) worker.perform + + expected_records = [ + hash_including('id' => project_event2.id, 'path' => "#{group.id}/#{project.project_namespace.id}/", + 'target_type' => 'Issue'), + hash_including('id' => event_without_parent.id, 'path' => '', 'target_type' => ''), + hash_including('id' => group_event.id, 'path' => "#{group.id}/", 'target_type' => ''), + hash_including('id' => project_event1.id, 'path' => "#{group.id}/#{project.project_namespace.id}/", + 'target_type' => 'Issue') + ] + + events = ClickHouse::Client.select('SELECT * FROM events ORDER BY id', :main) + + expect(events).to match(expected_records) + + last_processed_id = ClickHouse::SyncCursor.cursor_for(:events) + expect(last_processed_id).to eq(project_event1.id) end - end - context 'when exclusive lease error happens' do - it 'skips execution' do - expect(worker).to receive(:in_lock).and_raise(Gitlab::ExclusiveLeaseHelpers::FailedToObtainLockError) - expect(worker).to receive(:log_extra_metadata_on_done).with(:result, { status: :skipped }) + context 'when multiple batches are needed' do + before do + stub_const("#{described_class}::BATCH_SIZE", 1) + stub_const("#{described_class}::INSERT_BATCH_SIZE", 1) + end - worker.perform + it 'inserts all records' do + worker.perform + + events = ClickHouse::Client.select('SELECT * FROM events', :main) + expect(events.size).to eq(4) + end + end + + context 'when time limit is reached' do + before do + stub_const("#{described_class}::BATCH_SIZE", 1) + end + + it 'stops the processing' do + allow_next_instance_of(Analytics::CycleAnalytics::RuntimeLimiter) do |runtime_limiter| + allow(runtime_limiter).to receive(:over_time?).and_return(false, true) + end + + expect(worker).to receive(:log_extra_metadata_on_done).with(:result, + { status: :processed, records_inserted: 2, reached_end_of_table: false }) + + worker.perform + + last_processed_id = ClickHouse::SyncCursor.cursor_for(:events) + expect(last_processed_id).to eq(event_without_parent.id) + end + end + + context 'when syncing from a certain point' do + before do + ClickHouse::SyncCursor.update_cursor_for(:events, project_event2.id) + end + + it 'syncs records after the cursor' do + worker.perform + + events = ClickHouse::Client.select('SELECT id FROM events ORDER BY id', :main) + expect(events).to eq([{ 'id' => event_without_parent.id }, { 'id' => group_event.id }, + { 'id' => project_event1.id }]) + end + + context 'when there is nothing to sync' do + it 'does nothing' do + expect(worker).to receive(:log_extra_metadata_on_done).with(:result, + { status: :processed, records_inserted: 0, reached_end_of_table: true }) + + ClickHouse::SyncCursor.update_cursor_for(:events, project_event1.id) + worker.perform + + events = ClickHouse::Client.select('SELECT id FROM events ORDER BY id', :main) + expect(events).to be_empty + end + end end end end - context 'when the event_sync_worker_for_click_house feature flag is off' do + context 'when clickhouse is not configured' do before do - stub_feature_flags(event_sync_worker_for_click_house: false) + allow(ClickHouse::Client.configuration).to receive(:databases).and_return({}) end it 'skips execution' do @@ -54,4 +131,28 @@ RSpec.describe ClickHouse::EventsSyncWorker, feature_category: :value_stream_man end end end + + context 'when exclusive lease error happens' do + it 'skips execution' do + stub_feature_flags(event_sync_worker_for_click_house: true) + allow(ClickHouse::Client.configuration).to receive(:databases).and_return({ main: :some_db }) + + expect(worker).to receive(:in_lock).and_raise(Gitlab::ExclusiveLeaseHelpers::FailedToObtainLockError) + expect(worker).to receive(:log_extra_metadata_on_done).with(:result, { status: :skipped }) + + worker.perform + end + end + + context 'when the event_sync_worker_for_click_house feature flag is off' do + before do + stub_feature_flags(event_sync_worker_for_click_house: false) + end + + it 'skips execution' do + expect(worker).to receive(:log_extra_metadata_on_done).with(:result, { status: :disabled }) + + worker.perform + end + end end diff --git a/spec/workers/concerns/gitlab/github_import/object_importer_spec.rb b/spec/workers/concerns/gitlab/github_import/object_importer_spec.rb index 3b7bbfc8a7b..27e1077b138 100644 --- a/spec/workers/concerns/gitlab/github_import/object_importer_spec.rb +++ b/spec/workers/concerns/gitlab/github_import/object_importer_spec.rb @@ -273,7 +273,8 @@ RSpec.describe Gitlab::GithubImport::ObjectImporter, :aggregate_failures, featur .to receive(:notify) .with( job['args'].last, - job['jid'] + job['jid'], + ttl: Gitlab::Import::JOB_WAITER_TTL ) sidekiq_retries_exhausted diff --git a/spec/workers/concerns/gitlab/github_import/rescheduling_methods_spec.rb b/spec/workers/concerns/gitlab/github_import/rescheduling_methods_spec.rb index 6475be0243c..c76ce6b555f 100644 --- a/spec/workers/concerns/gitlab/github_import/rescheduling_methods_spec.rb +++ b/spec/workers/concerns/gitlab/github_import/rescheduling_methods_spec.rb @@ -110,7 +110,7 @@ RSpec.describe Gitlab::GithubImport::ReschedulingMethods, feature_category: :imp expect(Gitlab::JobWaiter) .to receive(:notify) - .with('123', 'abc123') + .with('123', 'abc123', ttl: Gitlab::Import::JOB_WAITER_TTL) worker.notify_waiter('123') end diff --git a/spec/workers/concerns/gitlab/notify_upon_death_spec.rb b/spec/workers/concerns/gitlab/import/notify_upon_death_spec.rb index 36faf3ee296..1f760e8542b 100644 --- a/spec/workers/concerns/gitlab/notify_upon_death_spec.rb +++ b/spec/workers/concerns/gitlab/import/notify_upon_death_spec.rb @@ -2,11 +2,11 @@ require 'spec_helper' -RSpec.describe Gitlab::NotifyUponDeath, feature_category: :shared do +RSpec.describe Gitlab::Import::NotifyUponDeath, feature_category: :importers do let(:worker_class) do Class.new do include Sidekiq::Worker - include Gitlab::NotifyUponDeath + include Gitlab::Import::NotifyUponDeath end end @@ -16,13 +16,13 @@ RSpec.describe Gitlab::NotifyUponDeath, feature_category: :shared do expect(Gitlab::JobWaiter) .to receive(:notify) - .with('123abc', '123') + .with('123abc', '123', ttl: Gitlab::Import::JOB_WAITER_TTL) worker_class.sidekiq_retries_exhausted_block.call(job) end it 'does not notify the JobWaiter when only 2 arguments are given' do - job = { 'args' => [12, {}], 'jid' => '123' } + job = { 'args' => [12, '123abc'], 'jid' => '123' } expect(Gitlab::JobWaiter) .not_to receive(:notify) @@ -31,7 +31,7 @@ RSpec.describe Gitlab::NotifyUponDeath, feature_category: :shared do end it 'does not notify the JobWaiter when only 1 argument is given' do - job = { 'args' => [12], 'jid' => '123' } + job = { 'args' => ['123abc'], 'jid' => '123' } expect(Gitlab::JobWaiter) .not_to receive(:notify) diff --git a/spec/workers/concerns/limited_capacity/worker_spec.rb b/spec/workers/concerns/limited_capacity/worker_spec.rb index 65906eef0fa..8092adec3b9 100644 --- a/spec/workers/concerns/limited_capacity/worker_spec.rb +++ b/spec/workers/concerns/limited_capacity/worker_spec.rb @@ -57,10 +57,26 @@ RSpec.describe LimitedCapacity::Worker, :clean_gitlab_redis_queues, :aggregate_f it 'enqueues jobs' do expect(worker_class) .to receive(:bulk_perform_async) - .with([[:arg], [:arg], [:arg]]) + .with([[:arg], [:arg], [:arg]]).and_call_original + + expect(Sidekiq::Client).to receive(:push_bulk) perform_with_capacity end + + context 'when max_running_jobs is 0' do + let(:max_running_jobs) { 0 } + + it 'does not enqueue jobs' do + expect(worker_class) + .to receive(:bulk_perform_async) + .with([]).and_call_original + + expect(Sidekiq::Client).not_to receive(:push_bulk) + + perform_with_capacity + end + end end describe '#perform' do diff --git a/spec/workers/database/lock_tables_worker_spec.rb b/spec/workers/database/lock_tables_worker_spec.rb new file mode 100644 index 00000000000..cb720959db0 --- /dev/null +++ b/spec/workers/database/lock_tables_worker_spec.rb @@ -0,0 +1,136 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Database::LockTablesWorker, feature_category: :cell do + using RSpec::Parameterized::TableSyntax + + let(:worker) { described_class.new } + let(:exception_class) { described_class::TableShouldNotBeLocked } + + describe '#perform' do + context 'when running with single database' do # this covers both single-db and single-db-ci-connection cases + before do + skip_if_database_exists(:ci) + end + + it 'skips executing the job' do + expect do + worker.perform('ci', %w[ci_pipelines]) + end.to raise_error(exception_class, 'GitLab is not running in multiple database mode') + end + end + + context 'when running in decomposed database' do + before do + skip_if_shared_database(:ci) + end + + context 'when the table is wrong' do + context 'when trying to lock tables on an unknown database' do + it 'raises an exception' do + expect do + worker.perform('foobar', %w[ci_pipelines]) + end.to raise_error(exception_class, /does not support locking writes on tables/) + end + end + + context 'when trying to lock tables on the database that does not support locking' do + it 'raises an exception' do + expect do + worker.perform('geo', %w[ci_pipelines]) # ci tables should be locked only on main + end.to raise_error(exception_class, /does not support locking writes on tables/) + end + end + + context 'when trying to lock tables on the wrong database' do + it 'raises an exception' do + expect do + worker.perform('ci', %w[ci_pipelines]) # ci tables should be locked only on main + end.to raise_error(exception_class, "table 'ci_pipelines' should not be locked on the database 'ci'") + end + end + + context 'when trying to lock shared tables on the database' do + it 'raises an exception' do + expect do + worker.perform('main', %w[loose_foreign_keys_deleted_records]) + end.to raise_error(exception_class, /should not be locked on the database 'main'/) + end + end + end + + context 'when the table is correct' do + context 'when the table is not locked for writes' do + where(:database_name, :tables) do + :ci | %w[users namespaces] + :main | %w[ci_pipelines ci_builds] + end + + with_them do + it 'locks the tables on the corresponding database' do + tables.each do |table_name| + unlock_table(database_name, table_name) + expect(lock_writes_manager(database_name, table_name).table_locked_for_writes?).to eq(false) + end + + expected_log_results = tables.map do |table_name| + { action: "locked", database: database_name, dry_run: false, table: table_name } + end + expect(worker).to receive(:log_extra_metadata_on_done).with(:performed_actions, expected_log_results) + + worker.perform(database_name, tables) + tables.each do |table_name| + expect(lock_writes_manager(database_name, table_name).table_locked_for_writes?).to eq(true) + end + end + end + + context 'when the table is already locked for writes' do + where(:database_name, :tables) do + :ci | %w[users namespaces] + :main | %w[ci_pipelines ci_builds] + end + + with_them do + it 'skips locking the tables on the corresponding database' do + tables.each do |table_name| + lock_table(database_name, table_name) + end + + expected_log_results = tables.map do |table_name| + { action: 'skipped', database: database_name, dry_run: false, table: table_name } + end + expect(worker).to receive(:log_extra_metadata_on_done).with(:performed_actions, expected_log_results) + + worker.perform(database_name, tables) + tables.each do |table_name| + expect(lock_writes_manager(database_name, table_name).table_locked_for_writes?).to eq(true) + end + end + end + end + end + end + end + end + + def lock_table(database_name, table_name) + lock_writes_manager(database_name, table_name).lock_writes + end + + def unlock_table(database_name, table_name) + lock_writes_manager(database_name, table_name).unlock_writes + end + + def lock_writes_manager(database_name, table_name) + connection = Gitlab::Database.database_base_models_with_gitlab_shared[database_name].connection + Gitlab::Database::LockWritesManager.new( + table_name: table_name, + connection: connection, + database_name: database_name, + with_retries: false, + dry_run: false + ) + end +end diff --git a/spec/workers/database/monitor_locked_tables_worker_spec.rb b/spec/workers/database/monitor_locked_tables_worker_spec.rb index 47475a0ad4a..7e900259265 100644 --- a/spec/workers/database/monitor_locked_tables_worker_spec.rb +++ b/spec/workers/database/monitor_locked_tables_worker_spec.rb @@ -19,6 +19,10 @@ RSpec.describe Database::MonitorLockedTablesWorker, feature_category: :cell do end context 'when running in decomposed database' do + before do + skip_if_shared_database(:ci) + end + context 'when the feature flag is disabled' do before do stub_feature_flags(monitor_database_locked_tables: false) @@ -32,7 +36,6 @@ RSpec.describe Database::MonitorLockedTablesWorker, feature_category: :cell do context 'when the feature flag is enabled' do before do - skip_if_shared_database(:ci) stub_feature_flags(monitor_database_locked_tables: true) allow(Gitlab::Database::TablesLocker).to receive(:new).and_return(tables_locker) end @@ -73,6 +76,56 @@ RSpec.describe Database::MonitorLockedTablesWorker, feature_category: :cell do worker.perform end + + context 'with automatically locking the unlocked tables' do + context 'when there are no tables to be locked' do + before do + stub_feature_flags(lock_tables_in_monitoring: true) + allow(tables_locker).to receive(:lock_writes).and_return([]) + end + + it 'does not call the Database::LockTablesWorker' do + expect(Database::LockTablesWorker).not_to receive(:perform_async) + end + end + + context 'when there are tables to be locked' do + before do + lock_writes_results = [ + { table: 'users', database: 'ci', action: 'needs_lock' }, + { table: 'projects', database: 'ci', action: 'needs_lock' }, + { table: 'ci_builds', database: 'main', action: 'needs_lock' }, + { table: 'ci_pipelines', database: 'main', action: 'skipped' } + ] + allow(tables_locker).to receive(:lock_writes).and_return(lock_writes_results) + end + + context 'when feature flag lock_tables_in_monitoring is enabled' do + before do + stub_feature_flags(lock_tables_in_monitoring: true) + end + + it 'locks the tables that need to be locked' do + expect(Database::LockTablesWorker).to receive(:perform_async).once.with('ci', %w[users projects]) + expect(Database::LockTablesWorker).to receive(:perform_async).once.with('main', %w[ci_builds]) + + worker.perform + end + end + + context 'when feature flag lock_tables_in_monitoring is disabled' do + before do + stub_feature_flags(lock_tables_in_monitoring: false) + end + + it 'does not lock the tables that need to be locked' do + expect(Database::LockTablesWorker).not_to receive(:perform_async) + + worker.perform + end + end + end + end end end end diff --git a/spec/workers/environments/stop_job_success_worker_spec.rb b/spec/workers/environments/stop_job_success_worker_spec.rb index 3a2db8cfb77..df0acf46bd9 100644 --- a/spec/workers/environments/stop_job_success_worker_spec.rb +++ b/spec/workers/environments/stop_job_success_worker_spec.rb @@ -4,39 +4,54 @@ require 'spec_helper' RSpec.describe Environments::StopJobSuccessWorker, feature_category: :continuous_delivery do describe '#perform' do - subject { described_class.new.perform(build.id) } + let_it_be_with_refind(:environment) { create(:environment, state: :available) } - context 'when build exists' do - context 'when the build will stop an environment' do - let!(:build) { create(:ci_build, :stop_review_app, environment: environment.name, project: environment.project, status: :success) } # rubocop:disable Layout/LineLength - let(:environment) { create(:environment, state: :available) } + subject { described_class.new.perform(job.id) } - it 'stops the environment' do + shared_examples_for 'stopping an associated environment' do + it 'stops the environment' do + expect(environment).to be_available + + subject + + expect(environment.reload).to be_stopped + end + + context 'when the job fails' do + before do + job.update!(status: :failed) + environment.update!(state: :available) + end + + it 'does not stop the environment' do expect(environment).to be_available subject - expect(environment.reload).to be_stopped + expect(environment.reload).not_to be_stopped end + end + end - context 'when the build fails' do - before do - build.update!(status: :failed) - environment.update!(state: :available) - end - - it 'does not stop the environment' do - expect(environment).to be_available + context 'with build job' do + let!(:job) do + create(:ci_build, :stop_review_app, environment: environment.name, project: environment.project, + status: :success) + end - subject + it_behaves_like 'stopping an associated environment' + end - expect(environment.reload).not_to be_stopped - end - end + context 'with bridge job' do + let!(:job) do + create(:ci_bridge, :stop_review_app, environment: environment.name, project: environment.project, + status: :success) end + + it_behaves_like 'stopping an associated environment' end - context 'when build does not exist' do + context 'when job does not exist' do it 'does not raise exception' do expect { described_class.new.perform(123) } .not_to raise_error diff --git a/spec/workers/every_sidekiq_worker_spec.rb b/spec/workers/every_sidekiq_worker_spec.rb index 3cd030e678d..9a94a836d60 100644 --- a/spec/workers/every_sidekiq_worker_spec.rb +++ b/spec/workers/every_sidekiq_worker_spec.rb @@ -142,6 +142,7 @@ RSpec.describe 'Every Sidekiq worker', feature_category: :shared do 'BulkImports::EntityWorker' => false, 'BulkImports::PipelineWorker' => false, 'BulkImports::PipelineBatchWorker' => false, + 'BulkImports::FinishProjectImportWorker' => 5, 'Chaos::CpuSpinWorker' => 3, 'Chaos::DbSpinWorker' => 3, 'Chaos::KillWorker' => false, @@ -194,6 +195,7 @@ RSpec.describe 'Every Sidekiq worker', feature_category: :shared do 'CreateGithubWebhookWorker' => 3, 'CreateNoteDiffFileWorker' => 3, 'CreatePipelineWorker' => 3, + 'Database::LockTablesWorker' => false, 'Database::BatchedBackgroundMigration::CiExecutionWorker' => 0, 'Database::BatchedBackgroundMigration::MainExecutionWorker' => 0, 'DeleteContainerRepositoryWorker' => 3, @@ -233,8 +235,6 @@ RSpec.describe 'Every Sidekiq worker', feature_category: :shared do 'Geo::Batch::ProjectRegistrySchedulerWorker' => 3, 'Geo::Batch::ProjectRegistryWorker' => 3, 'Geo::ContainerRepositorySyncWorker' => 1, - 'Geo::DesignRepositoryShardSyncWorker' => false, - 'Geo::DesignRepositorySyncWorker' => 1, 'Geo::DestroyWorker' => 3, 'Geo::EventWorker' => 3, 'Geo::FileRemovalWorker' => 3, @@ -247,6 +247,7 @@ RSpec.describe 'Every Sidekiq worker', feature_category: :shared do 'Geo::RepositoryVerification::Secondary::SingleWorker' => false, 'Geo::ReverificationBatchWorker' => 0, 'Geo::BulkMarkPendingBatchWorker' => 0, + 'Geo::BulkMarkVerificationPendingBatchWorker' => 0, 'Geo::Scheduler::Primary::SchedulerWorker' => false, 'Geo::Scheduler::SchedulerWorker' => false, 'Geo::Scheduler::Secondary::SchedulerWorker' => false, @@ -255,6 +256,10 @@ RSpec.describe 'Every Sidekiq worker', feature_category: :shared do 'Geo::VerificationTimeoutWorker' => false, 'Geo::VerificationWorker' => 3, 'GeoRepositoryDestroyWorker' => 3, + 'Gitlab::BitbucketImport::AdvanceStageWorker' => 3, + 'Gitlab::BitbucketImport::Stage::FinishImportWorker' => 3, + 'Gitlab::BitbucketImport::Stage::ImportPullRequestsWorker' => 3, + 'Gitlab::BitbucketImport::Stage::ImportRepositoryWorker' => 3, 'Gitlab::BitbucketServerImport::AdvanceStageWorker' => 3, 'Gitlab::BitbucketServerImport::Stage::FinishImportWorker' => 3, 'Gitlab::BitbucketServerImport::Stage::ImportLfsObjectsWorker' => 3, @@ -342,6 +347,9 @@ RSpec.describe 'Every Sidekiq worker', feature_category: :shared do 'JiraConnect::SyncProjectWorker' => 3, 'LdapGroupSyncWorker' => 3, 'Licenses::ResetSubmitLicenseUsageDataBannerWorker' => 13, + 'Llm::Embedding::GitlabDocumentation::SetEmbeddingsOnTheRecordWorker' => 5, + 'Llm::Embedding::GitlabDocumentation::CreateEmptyEmbeddingsRecordsWorker' => 3, + 'Llm::Embedding::GitlabDocumentation::CreateDbEmbeddingsPerDocFileWorker' => 5, 'Llm::TanukiBot::UpdateWorker' => 1, 'Llm::TanukiBot::RecreateRecordsWorker' => 3, 'MailScheduler::IssueDueWorker' => 3, @@ -366,10 +374,8 @@ RSpec.describe 'Every Sidekiq worker', feature_category: :shared do 'Onboarding::PipelineCreatedWorker' => 3, 'Onboarding::ProgressWorker' => 3, 'Onboarding::UserAddedWorker' => 3, - 'Namespaces::FreeUserCap::OverLimitNotificationWorker' => false, 'Namespaces::RootStatisticsWorker' => 3, 'Namespaces::ScheduleAggregationWorker' => 3, - 'Namespaces::FreeUserCap::NotificationClearingWorker' => false, 'NewEpicWorker' => 3, 'NewIssueWorker' => 3, 'NewMergeRequestWorker' => 3, diff --git a/spec/workers/gitlab/bitbucket_import/advance_stage_worker_spec.rb b/spec/workers/gitlab/bitbucket_import/advance_stage_worker_spec.rb new file mode 100644 index 00000000000..16e3a3dc481 --- /dev/null +++ b/spec/workers/gitlab/bitbucket_import/advance_stage_worker_spec.rb @@ -0,0 +1,115 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BitbucketImport::AdvanceStageWorker, :clean_gitlab_redis_shared_state, feature_category: :importers do + let(:project) { create(:project) } + let(:import_state) { create(:import_state, project: project, jid: '123') } + let(:worker) { described_class.new } + + describe '#perform' do + context 'when the project no longer exists' do + it 'does not perform any work' do + expect(worker).not_to receive(:wait_for_jobs) + + worker.perform(-1, { '123' => 2 }, :finish) + end + end + + context 'when there are remaining jobs' do + before do + allow(worker) + .to receive(:find_import_state) + .and_return(import_state) + end + + it 'reschedules itself' do + expect(worker) + .to receive(:wait_for_jobs) + .with({ '123' => 2 }) + .and_return({ '123' => 1 }) + + expect(described_class) + .to receive(:perform_in) + .with(described_class::INTERVAL, project.id, { '123' => 1 }, :finish) + + worker.perform(project.id, { '123' => 2 }, :finish) + end + end + + context 'when there are no remaining jobs' do + before do + allow(worker) + .to receive(:find_import_state) + .and_return(import_state) + + allow(worker) + .to receive(:wait_for_jobs) + .with({ '123' => 2 }) + .and_return({}) + end + + it 'schedules the next stage' do + expect(import_state) + .to receive(:refresh_jid_expiration) + + expect(Gitlab::BitbucketImport::Stage::FinishImportWorker) + .to receive(:perform_async) + .with(project.id) + + worker.perform(project.id, { '123' => 2 }, :finish) + end + + it 'raises KeyError when the stage name is invalid' do + expect { worker.perform(project.id, { '123' => 2 }, :kittens) } + .to raise_error(KeyError) + end + end + end + + describe '#wait_for_jobs' do + it 'waits for jobs to complete and returns a new pair of keys to wait for' do + waiter1 = instance_double(Gitlab::JobWaiter, jobs_remaining: 1, key: '123') + waiter2 = instance_double(Gitlab::JobWaiter, jobs_remaining: 0, key: '456') + + expect(Gitlab::JobWaiter) + .to receive(:new) + .ordered + .with(2, '123') + .and_return(waiter1) + + expect(Gitlab::JobWaiter) + .to receive(:new) + .ordered + .with(1, '456') + .and_return(waiter2) + + expect(waiter1) + .to receive(:wait) + .with(described_class::BLOCKING_WAIT_TIME) + + expect(waiter2) + .to receive(:wait) + .with(described_class::BLOCKING_WAIT_TIME) + + new_waiters = worker.wait_for_jobs({ '123' => 2, '456' => 1 }) + + expect(new_waiters).to eq({ '123' => 1 }) + end + end + + describe '#find_import_state' do + it 'returns a ProjectImportState' do + import_state.update_column(:status, 'started') + + found = worker.find_import_state(project.id) + + expect(found).to be_an_instance_of(ProjectImportState) + expect(found.attributes.keys).to match_array(%w[id jid]) + end + + it 'returns nil if the project import is not running' do + expect(worker.find_import_state(project.id)).to be_nil + end + end +end diff --git a/spec/workers/gitlab/bitbucket_import/import_pull_request_worker_spec.rb b/spec/workers/gitlab/bitbucket_import/import_pull_request_worker_spec.rb new file mode 100644 index 00000000000..082499be515 --- /dev/null +++ b/spec/workers/gitlab/bitbucket_import/import_pull_request_worker_spec.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BitbucketImport::ImportPullRequestWorker, feature_category: :importers do + subject(:worker) { described_class.new } + + it_behaves_like Gitlab::BitbucketImport::ObjectImporter +end diff --git a/spec/workers/gitlab/bitbucket_import/stage/finish_import_worker_spec.rb b/spec/workers/gitlab/bitbucket_import/stage/finish_import_worker_spec.rb new file mode 100644 index 00000000000..11baa58f1ab --- /dev/null +++ b/spec/workers/gitlab/bitbucket_import/stage/finish_import_worker_spec.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BitbucketImport::Stage::FinishImportWorker, feature_category: :importers do + let_it_be(:project) { create(:project, :import_started) } + + subject(:worker) { described_class.new } + + it_behaves_like Gitlab::BitbucketImport::StageMethods + + it 'does not abort on failure' do + expect(worker.abort_on_failure).to be_falsey + end + + describe '#perform' do + it 'finalises the import process' do + expect_next_instance_of(Gitlab::Import::Metrics, :bitbucket_importer, project) do |metric| + expect(metric).to receive(:track_finished_import) + end + + worker.perform(project.id) + + expect(project.import_state.reload).to be_finished + end + end +end diff --git a/spec/workers/gitlab/bitbucket_import/stage/import_pull_requests_worker_spec.rb b/spec/workers/gitlab/bitbucket_import/stage/import_pull_requests_worker_spec.rb new file mode 100644 index 00000000000..8f425066160 --- /dev/null +++ b/spec/workers/gitlab/bitbucket_import/stage/import_pull_requests_worker_spec.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BitbucketImport::Stage::ImportPullRequestsWorker, feature_category: :importers do + let_it_be(:project) { create(:project, :import_started) } + + subject(:worker) { described_class.new } + + it_behaves_like Gitlab::BitbucketImport::StageMethods + + describe '#perform' do + context 'when the import succeeds' do + before do + allow_next_instance_of(Gitlab::BitbucketImport::Importers::PullRequestsImporter) do |importer| + allow(importer).to receive(:execute).and_return(Gitlab::JobWaiter.new(2, '123')) + end + end + + it 'schedules the next stage' do + expect(Gitlab::BitbucketImport::AdvanceStageWorker).to receive(:perform_async) + .with(project.id, { '123' => 2 }, :finish) + + worker.perform(project.id) + end + + it 'logs stage start and finish' do + expect(Gitlab::BitbucketImport::Logger) + .to receive(:info).with(hash_including(message: 'starting stage', project_id: project.id)) + expect(Gitlab::BitbucketImport::Logger) + .to receive(:info).with(hash_including(message: 'stage finished', project_id: project.id)) + + worker.perform(project.id) + end + end + + context 'when project does not exists' do + it 'does not call the importer' do + expect(Gitlab::BitbucketImport::Importers::PullRequestsImporter).not_to receive(:new) + + worker.perform(-1) + end + end + + context 'when project import state is not `started`' do + it 'does not call the importer' do + project = create(:project, :import_canceled) + + expect(Gitlab::BitbucketImport::Importers::PullRequestsImporter).not_to receive(:new) + + worker.perform(project.id) + end + end + + context 'when the importer fails' do + it 'does not schedule the next stage and raises error' do + exception = StandardError.new('Error') + + allow_next_instance_of(Gitlab::BitbucketImport::Importers::PullRequestsImporter) do |importer| + allow(importer).to receive(:execute).and_raise(exception) + end + + expect(Gitlab::Import::ImportFailureService) + .to receive(:track).with( + project_id: project.id, + exception: exception, + error_source: described_class.name, + fail_import: false + ).and_call_original + + expect { worker.perform(project.id) } + .to change { Gitlab::BitbucketImport::AdvanceStageWorker.jobs.size }.by(0) + .and raise_error(exception) + end + end + end +end diff --git a/spec/workers/gitlab/bitbucket_import/stage/import_repository_worker_spec.rb b/spec/workers/gitlab/bitbucket_import/stage/import_repository_worker_spec.rb new file mode 100644 index 00000000000..2234a49d66c --- /dev/null +++ b/spec/workers/gitlab/bitbucket_import/stage/import_repository_worker_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BitbucketImport::Stage::ImportRepositoryWorker, feature_category: :importers do + let_it_be(:project) { create(:project, :import_started) } + + let(:worker) { described_class.new } + + it_behaves_like Gitlab::BitbucketImport::StageMethods + + it 'executes the importer and enqueues ImportPullRequestsWorker' do + expect(Gitlab::BitbucketImport::Importers::RepositoryImporter).to receive_message_chain(:new, :execute) + .and_return(true) + + expect(Gitlab::BitbucketImport::Stage::ImportPullRequestsWorker).to receive(:perform_async).with(project.id) + .and_return(true).once + + worker.perform(project.id) + end +end diff --git a/spec/workers/gitlab/bitbucket_server_import/advance_stage_worker_spec.rb b/spec/workers/gitlab/bitbucket_server_import/advance_stage_worker_spec.rb new file mode 100644 index 00000000000..14e93440422 --- /dev/null +++ b/spec/workers/gitlab/bitbucket_server_import/advance_stage_worker_spec.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BitbucketServerImport::AdvanceStageWorker, feature_category: :importers do + it_behaves_like Gitlab::Import::AdvanceStage, factory: :import_state +end diff --git a/spec/workers/gitlab/bitbucket_server_import/import_pull_request_worker_spec.rb b/spec/workers/gitlab/bitbucket_server_import/import_pull_request_worker_spec.rb index dd3235f846c..376078532cd 100644 --- a/spec/workers/gitlab/bitbucket_server_import/import_pull_request_worker_spec.rb +++ b/spec/workers/gitlab/bitbucket_server_import/import_pull_request_worker_spec.rb @@ -26,7 +26,7 @@ RSpec.describe Gitlab::BitbucketServerImport::ImportPullRequestWorker, feature_c end it 'notifies job waiter' do - expect(Gitlab::JobWaiter).to receive(:notify).with(job_waiter_key, 'jid') + expect(Gitlab::JobWaiter).to receive(:notify).with(job_waiter_key, 'jid', ttl: Gitlab::Import::JOB_WAITER_TTL) worker.perform(project.id, {}, job_waiter_key) end @@ -44,7 +44,7 @@ RSpec.describe Gitlab::BitbucketServerImport::ImportPullRequestWorker, feature_c context 'when project does not exists' do it 'does not call importer and notifies job waiter' do expect(importer_class).not_to receive(:new) - expect(Gitlab::JobWaiter).to receive(:notify).with(job_waiter_key, 'jid') + expect(Gitlab::JobWaiter).to receive(:notify).with(job_waiter_key, 'jid', ttl: Gitlab::Import::JOB_WAITER_TTL) worker.perform(-1, {}, job_waiter_key) end @@ -55,7 +55,7 @@ RSpec.describe Gitlab::BitbucketServerImport::ImportPullRequestWorker, feature_c project = create(:project, :import_canceled) expect(importer_class).not_to receive(:new) - expect(Gitlab::JobWaiter).to receive(:notify).with(job_waiter_key, 'jid') + expect(Gitlab::JobWaiter).to receive(:notify).with(job_waiter_key, 'jid', ttl: Gitlab::Import::JOB_WAITER_TTL) worker.perform(project.id, {}, job_waiter_key) end diff --git a/spec/workers/gitlab/github_gists_import/import_gist_worker_spec.rb b/spec/workers/gitlab/github_gists_import/import_gist_worker_spec.rb index 2e89263bcf3..dc715c3026b 100644 --- a/spec/workers/gitlab/github_gists_import/import_gist_worker_spec.rb +++ b/spec/workers/gitlab/github_gists_import/import_gist_worker_spec.rb @@ -68,7 +68,7 @@ RSpec.describe Gitlab::GithubGistsImport::ImportGistWorker, feature_category: :i .to receive(:info) .with(log_attributes.merge('message' => 'start importer')) expect(importer).to receive(:execute).and_return(importer_result) - expect(Gitlab::JobWaiter).to receive(:notify).with('some_key', subject.jid) + expect(Gitlab::JobWaiter).to receive(:notify).with('some_key', subject.jid, ttl: Gitlab::Import::JOB_WAITER_TTL) expect(Gitlab::GithubImport::Logger) .to receive(:info) .with(log_attributes.merge('message' => 'importer finished')) @@ -114,7 +114,9 @@ RSpec.describe Gitlab::GithubGistsImport::ImportGistWorker, feature_category: :i expect(Gitlab::GithubImport::Logger) .to receive(:error) .with(log_attributes.merge('message' => 'importer failed', 'error.message' => 'error_message')) - expect(Gitlab::JobWaiter).to receive(:notify).with('some_key', subject.jid) + expect(Gitlab::JobWaiter) + .to receive(:notify) + .with('some_key', subject.jid, ttl: Gitlab::Import::JOB_WAITER_TTL) subject.perform(user.id, gist_hash, 'some_key') @@ -189,7 +191,7 @@ RSpec.describe Gitlab::GithubGistsImport::ImportGistWorker, feature_category: :i it 'notifies the JobWaiter' do expect(Gitlab::JobWaiter) .to receive(:notify) - .with(job['args'].last, job['jid']) + .with(job['args'].last, job['jid'], ttl: Gitlab::Import::JOB_WAITER_TTL) sidekiq_retries_exhausted end diff --git a/spec/workers/gitlab/github_import/advance_stage_worker_spec.rb b/spec/workers/gitlab/github_import/advance_stage_worker_spec.rb index 121f30ea9d5..60c117a2a90 100644 --- a/spec/workers/gitlab/github_import/advance_stage_worker_spec.rb +++ b/spec/workers/gitlab/github_import/advance_stage_worker_spec.rb @@ -2,114 +2,6 @@ require 'spec_helper' -RSpec.describe Gitlab::GithubImport::AdvanceStageWorker, :clean_gitlab_redis_shared_state, feature_category: :importers do - let(:project) { create(:project) } - let(:import_state) { create(:import_state, project: project, jid: '123') } - let(:worker) { described_class.new } - - describe '#perform' do - context 'when the project no longer exists' do - it 'does not perform any work' do - expect(worker).not_to receive(:wait_for_jobs) - - worker.perform(-1, { '123' => 2 }, :finish) - end - end - - context 'when there are remaining jobs' do - before do - allow(worker) - .to receive(:find_import_state) - .and_return(import_state) - end - - it 'reschedules itself' do - expect(worker) - .to receive(:wait_for_jobs) - .with({ '123' => 2 }) - .and_return({ '123' => 1 }) - - expect(described_class) - .to receive(:perform_in) - .with(described_class::INTERVAL, project.id, { '123' => 1 }, :finish) - - worker.perform(project.id, { '123' => 2 }, :finish) - end - end - - context 'when there are no remaining jobs' do - before do - allow(worker) - .to receive(:find_import_state) - .and_return(import_state) - - allow(worker) - .to receive(:wait_for_jobs) - .with({ '123' => 2 }) - .and_return({}) - end - - it 'schedules the next stage' do - expect(import_state) - .to receive(:refresh_jid_expiration) - - expect(Gitlab::GithubImport::Stage::FinishImportWorker) - .to receive(:perform_async) - .with(project.id) - - worker.perform(project.id, { '123' => 2 }, :finish) - end - - it 'raises KeyError when the stage name is invalid' do - expect { worker.perform(project.id, { '123' => 2 }, :kittens) } - .to raise_error(KeyError) - end - end - end - - describe '#wait_for_jobs' do - it 'waits for jobs to complete and returns a new pair of keys to wait for' do - waiter1 = double(:waiter1, jobs_remaining: 1, key: '123') - waiter2 = double(:waiter2, jobs_remaining: 0, key: '456') - - expect(Gitlab::JobWaiter) - .to receive(:new) - .ordered - .with(2, '123') - .and_return(waiter1) - - expect(Gitlab::JobWaiter) - .to receive(:new) - .ordered - .with(1, '456') - .and_return(waiter2) - - expect(waiter1) - .to receive(:wait) - .with(described_class::BLOCKING_WAIT_TIME) - - expect(waiter2) - .to receive(:wait) - .with(described_class::BLOCKING_WAIT_TIME) - - new_waiters = worker.wait_for_jobs({ '123' => 2, '456' => 1 }) - - expect(new_waiters).to eq({ '123' => 1 }) - end - end - - describe '#find_import_state' do - it 'returns a ProjectImportState' do - import_state.update_column(:status, 'started') - - found = worker.find_import_state(project.id) - - expect(found).to be_an_instance_of(ProjectImportState) - expect(found.attributes.keys).to match_array(%w(id jid)) - end - - it 'returns nil if the project import is not running' do - expect(worker.find_import_state(project.id)).to be_nil - end - end +RSpec.describe Gitlab::GithubImport::AdvanceStageWorker, feature_category: :importers do + it_behaves_like Gitlab::Import::AdvanceStage, factory: :import_state end diff --git a/spec/workers/gitlab/jira_import/advance_stage_worker_spec.rb b/spec/workers/gitlab/jira_import/advance_stage_worker_spec.rb new file mode 100644 index 00000000000..d7c5d8aba4d --- /dev/null +++ b/spec/workers/gitlab/jira_import/advance_stage_worker_spec.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::JiraImport::AdvanceStageWorker, feature_category: :importers do + it_behaves_like Gitlab::Import::AdvanceStage, factory: :jira_import_state +end diff --git a/spec/workers/gitlab/jira_import/import_issue_worker_spec.rb b/spec/workers/gitlab/jira_import/import_issue_worker_spec.rb index 5209395923f..6dfab44b228 100644 --- a/spec/workers/gitlab/jira_import/import_issue_worker_spec.rb +++ b/spec/workers/gitlab/jira_import/import_issue_worker_spec.rb @@ -12,9 +12,9 @@ RSpec.describe Gitlab::JiraImport::ImportIssueWorker, feature_category: :importe describe 'modules' do it { expect(described_class).to include_module(ApplicationWorker) } - it { expect(described_class).to include_module(Gitlab::NotifyUponDeath) } it { expect(described_class).to include_module(Gitlab::JiraImport::QueueOptions) } it { expect(described_class).to include_module(Gitlab::Import::DatabaseHelpers) } + it { expect(described_class).to include_module(Gitlab::Import::NotifyUponDeath) } end subject { described_class.new } diff --git a/spec/workers/incident_management/close_incident_worker_spec.rb b/spec/workers/incident_management/close_incident_worker_spec.rb index 02ca5260fbd..b218bf4ced1 100644 --- a/spec/workers/incident_management/close_incident_worker_spec.rb +++ b/spec/workers/incident_management/close_incident_worker_spec.rb @@ -6,7 +6,7 @@ RSpec.describe IncidentManagement::CloseIncidentWorker, feature_category: :incid subject(:worker) { described_class.new } describe '#perform' do - let_it_be(:user) { User.alert_bot } + let_it_be(:user) { Users::Internal.alert_bot } let_it_be(:project) { create(:project) } let_it_be(:issue, reload: true) { create(:incident, project: project) } diff --git a/spec/workers/incident_management/process_alert_worker_v2_spec.rb b/spec/workers/incident_management/process_alert_worker_v2_spec.rb index 476b6f04942..b9d7cd9fee8 100644 --- a/spec/workers/incident_management/process_alert_worker_v2_spec.rb +++ b/spec/workers/incident_management/process_alert_worker_v2_spec.rb @@ -19,14 +19,14 @@ RSpec.describe IncidentManagement::ProcessAlertWorkerV2, feature_category: :inci allow(Gitlab::AppLogger).to receive(:warn).and_call_original allow(AlertManagement::CreateAlertIssueService) - .to receive(:new).with(alert, User.alert_bot) + .to receive(:new).with(alert, Users::Internal.alert_bot) .and_call_original end shared_examples 'creates issue successfully' do it 'creates an issue' do expect(AlertManagement::CreateAlertIssueService) - .to receive(:new).with(alert, User.alert_bot) + .to receive(:new).with(alert, Users::Internal.alert_bot) expect { perform_worker }.to change { Issue.count }.by(1) end diff --git a/spec/workers/loose_foreign_keys/cleanup_worker_spec.rb b/spec/workers/loose_foreign_keys/cleanup_worker_spec.rb index 2e77f38e221..278efd3406c 100644 --- a/spec/workers/loose_foreign_keys/cleanup_worker_spec.rb +++ b/spec/workers/loose_foreign_keys/cleanup_worker_spec.rb @@ -166,4 +166,66 @@ RSpec.describe LooseForeignKeys::CleanupWorker, feature_category: :cell do end end end + + describe 'turbo mode' do + context 'when turbo mode is off' do + where(:database_name, :feature_flag) do + :main | :loose_foreign_keys_turbo_mode_main + :ci | :loose_foreign_keys_turbo_mode_ci + end + + with_them do + before do + skip unless Gitlab::Database.has_config?(database_name) + stub_feature_flags(feature_flag => false) + end + + it 'does not use TurboModificationTracker' do + allow_next_instance_of(LooseForeignKeys::TurboModificationTracker) do |instance| + expect(instance).not_to receive(:over_limit?) + end + + perform_for(db: database_name) + end + + it 'logs not using turbo mode' do + expect_next_instance_of(LooseForeignKeys::CleanupWorker) do |instance| + expect(instance).to receive(:log_extra_metadata_on_done).with(:stats, a_hash_including(turbo_mode: false)) + end + + perform_for(db: database_name) + end + end + end + + context 'when turbo mode is on' do + where(:database_name, :feature_flag) do + :main | :loose_foreign_keys_turbo_mode_main + :ci | :loose_foreign_keys_turbo_mode_ci + end + + with_them do + before do + skip unless Gitlab::Database.has_config?(database_name) + stub_feature_flags(feature_flag => true) + end + + it 'does not use TurboModificationTracker' do + expect_next_instance_of(LooseForeignKeys::TurboModificationTracker) do |instance| + expect(instance).to receive(:over_limit?).at_least(:once) + end + + perform_for(db: database_name) + end + + it 'logs using turbo mode' do + expect_next_instance_of(LooseForeignKeys::CleanupWorker) do |instance| + expect(instance).to receive(:log_extra_metadata_on_done).with(:stats, a_hash_including(turbo_mode: true)) + end + + perform_for(db: database_name) + end + end + end + end end diff --git a/spec/workers/merge_requests/ensure_prepared_worker_spec.rb b/spec/workers/merge_requests/ensure_prepared_worker_spec.rb new file mode 100644 index 00000000000..8f599ffe642 --- /dev/null +++ b/spec/workers/merge_requests/ensure_prepared_worker_spec.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe MergeRequests::EnsurePreparedWorker, :sidekiq_inline, feature_category: :code_review_workflow do + subject(:worker) { described_class.new } + + let_it_be(:merge_request_1, reload: true) { create(:merge_request, prepared_at: :nil) } + let_it_be(:merge_request_2, reload: true) { create(:merge_request, prepared_at: Time.current) } + let_it_be(:merge_request_3, reload: true) { create(:merge_request, prepared_at: :nil) } + + describe '#perform' do + context 'when ensure_merge_requests_prepared is enabled' do + it 'creates the expected NewMergeRequestWorkers of the unprepared merge requests' do + expect(merge_request_1.prepared_at).to eq(nil) + expect(merge_request_2.prepared_at).to eq(merge_request_2.prepared_at) + expect(merge_request_3.prepared_at).to eq(nil) + + worker.perform + + expect(merge_request_1.reload.prepared_at).not_to eq(nil) + expect(merge_request_2.reload.prepared_at).to eq(merge_request_2.prepared_at) + expect(merge_request_3.reload.prepared_at).not_to eq(nil) + end + end + + context 'when ensure_merge_requests_prepared is disabled' do + before do + stub_feature_flags(ensure_merge_requests_prepared: false) + end + + it 'does not prepare any merge requests' do + expect(merge_request_1.prepared_at).to eq(nil) + expect(merge_request_2.prepared_at).to eq(merge_request_2.prepared_at) + expect(merge_request_3.prepared_at).to eq(nil) + + worker.perform + + expect(merge_request_1.prepared_at).to eq(nil) + expect(merge_request_2.prepared_at).to eq(merge_request_2.prepared_at) + expect(merge_request_3.prepared_at).to eq(nil) + end + end + end + + it_behaves_like 'an idempotent worker' do + it 'creates the expected NewMergeRequestWorkers of the unprepared merge requests' do + expect(merge_request_1.prepared_at).to eq(nil) + expect(merge_request_2.prepared_at).to eq(merge_request_2.prepared_at) + expect(merge_request_3.prepared_at).to eq(nil) + + subject + + expect(merge_request_1.reload.prepared_at).not_to eq(nil) + expect(merge_request_2.reload.prepared_at).to eq(merge_request_2.prepared_at) + expect(merge_request_3.reload.prepared_at).not_to eq(nil) + end + end +end diff --git a/spec/workers/metrics/global_metrics_update_worker_spec.rb b/spec/workers/metrics/global_metrics_update_worker_spec.rb deleted file mode 100644 index d5bfbcc928a..00000000000 --- a/spec/workers/metrics/global_metrics_update_worker_spec.rb +++ /dev/null @@ -1,30 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe ::Metrics::GlobalMetricsUpdateWorker, feature_category: :metrics do - subject { described_class.new } - - describe '#perform' do - let(:service) { ::Metrics::GlobalMetricsUpdateService.new } - - it 'delegates to ::Metrics::GlobalMetricsUpdateService' do - expect(::Metrics::GlobalMetricsUpdateService).to receive(:new).and_return(service) - expect(service).to receive(:execute) - - subject.perform - end - - context 'for an idempotent worker' do - include_examples 'an idempotent worker' do - it 'exports metrics' do - allow(Gitlab).to receive(:maintenance_mode?).and_return(true).at_least(1).time - - perform_multiple - - expect(service.maintenance_mode_metric.get).to eq(1) - end - end - end - end -end diff --git a/spec/workers/namespaces/in_product_marketing_emails_worker_spec.rb b/spec/workers/namespaces/in_product_marketing_emails_worker_spec.rb deleted file mode 100644 index 237b5081bb1..00000000000 --- a/spec/workers/namespaces/in_product_marketing_emails_worker_spec.rb +++ /dev/null @@ -1,32 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Namespaces::InProductMarketingEmailsWorker, '#perform', unless: Gitlab.ee?, feature_category: :experimentation_activation do - # Running this in EE would call the overridden method, which can't be tested in CE. - # The EE code is covered in a separate EE spec. - - context 'when the in_product_marketing_emails_enabled setting is disabled' do - before do - stub_application_setting(in_product_marketing_emails_enabled: false) - end - - it 'does not execute the email service' do - expect(Namespaces::InProductMarketingEmailsService).not_to receive(:send_for_all_tracks_and_intervals) - - subject.perform - end - end - - context 'when the in_product_marketing_emails_enabled setting is enabled' do - before do - stub_application_setting(in_product_marketing_emails_enabled: true) - end - - it 'executes the email service' do - expect(Namespaces::InProductMarketingEmailsService).to receive(:send_for_all_tracks_and_intervals) - - subject.perform - end - end -end diff --git a/spec/workers/new_merge_request_worker_spec.rb b/spec/workers/new_merge_request_worker_spec.rb index 58f6792f9a0..4ed9b61a9d7 100644 --- a/spec/workers/new_merge_request_worker_spec.rb +++ b/spec/workers/new_merge_request_worker_spec.rb @@ -88,33 +88,25 @@ RSpec.describe NewMergeRequestWorker, feature_category: :code_review_workflow do worker.perform(merge_request.id, user.id) end - context 'when add_prepared_state_to_mr feature flag is off' do + context 'when the merge request is prepared' do before do - stub_feature_flags(add_prepared_state_to_mr: false) + merge_request.update!(prepared_at: Time.current) end - it 'calls the create service' do - expect_next_instance_of(MergeRequests::AfterCreateService, project: merge_request.target_project, current_user: user) do |service| - expect(service).to receive(:execute).with(merge_request) - end + it 'does not call the create service' do + expect(MergeRequests::AfterCreateService).not_to receive(:new) worker.perform(merge_request.id, user.id) end end - context 'when add_prepared_state_to_mr feature flag is on' do - before do - stub_feature_flags(add_prepared_state_to_mr: true) - end - - context 'when the merge request is not prepared' do - it 'calls the create service' do - expect_next_instance_of(MergeRequests::AfterCreateService, project: merge_request.target_project, current_user: user) do |service| - expect(service).to receive(:execute).with(merge_request) - end - - worker.perform(merge_request.id, user.id) + context 'when the merge request is not prepared' do + it 'calls the create service' do + expect_next_instance_of(MergeRequests::AfterCreateService, project: merge_request.target_project, current_user: user) do |service| + expect(service).to receive(:execute).with(merge_request).and_call_original end + + worker.perform(merge_request.id, user.id) end end end diff --git a/spec/workers/new_note_worker_spec.rb b/spec/workers/new_note_worker_spec.rb index 651b5742854..3465cffea2d 100644 --- a/spec/workers/new_note_worker_spec.rb +++ b/spec/workers/new_note_worker_spec.rb @@ -77,7 +77,7 @@ RSpec.describe NewNoteWorker, feature_category: :team_planning do end context 'when Note author has been deleted' do - let_it_be(:note) { create(:note, author: User.ghost) } + let_it_be(:note) { create(:note, author: Users::Internal.ghost) } it "does not call NotificationService" do expect(NotificationService).not_to receive(:new) diff --git a/spec/workers/pages/invalidate_domain_cache_worker_spec.rb b/spec/workers/pages/invalidate_domain_cache_worker_spec.rb deleted file mode 100644 index b3c81b25a93..00000000000 --- a/spec/workers/pages/invalidate_domain_cache_worker_spec.rb +++ /dev/null @@ -1,267 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Pages::InvalidateDomainCacheWorker, feature_category: :pages do - shared_examples 'clears caches with' do |event_class:, event_data:, caches:| - include AfterNextHelpers - - let(:event) { event_class.new(data: event_data) } - - subject { consume_event(subscriber: described_class, event: event) } - - it_behaves_like 'subscribes to event' - - it 'clears the cache with Gitlab::Pages::CacheControl' do - caches.each do |cache| - expect_next(Gitlab::Pages::CacheControl, type: cache[:type], id: cache[:id]) - .to receive(:clear_cache) - end - - subject - end - end - - context 'when a project have multiple domains' do - include AfterNextHelpers - - let_it_be(:project) { create(:project) } - let_it_be(:pages_domain) { create(:pages_domain, project: project) } - let_it_be(:pages_domain2) { create(:pages_domain, project: project) } - - let(:event) do - Pages::PageDeployedEvent.new( - data: { - project_id: project.id, - namespace_id: project.namespace_id, - root_namespace_id: project.root_ancestor.id - } - ) - end - - subject { consume_event(subscriber: described_class, event: event) } - - it 'clears the cache with Gitlab::Pages::CacheControl' do - expect_next(Gitlab::Pages::CacheControl, type: :namespace, id: project.namespace_id) - .to receive(:clear_cache) - expect_next(Gitlab::Pages::CacheControl, type: :domain, id: pages_domain.id) - .to receive(:clear_cache) - expect_next(Gitlab::Pages::CacheControl, type: :domain, id: pages_domain2.id) - .to receive(:clear_cache) - - subject - end - end - - it_behaves_like 'clears caches with', - event_class: Pages::PageDeployedEvent, - event_data: { project_id: 1, namespace_id: 2, root_namespace_id: 3 }, - caches: [ - { type: :namespace, id: 3 } - ] - - it_behaves_like 'clears caches with', - event_class: Pages::PageDeletedEvent, - event_data: { project_id: 1, namespace_id: 2, root_namespace_id: 3 }, - caches: [ - { type: :namespace, id: 3 } - ] - - it_behaves_like 'clears caches with', - event_class: Projects::ProjectDeletedEvent, - event_data: { project_id: 1, namespace_id: 2, root_namespace_id: 3 }, - caches: [ - { type: :namespace, id: 3 } - ] - - it_behaves_like 'clears caches with', - event_class: Projects::ProjectCreatedEvent, - event_data: { project_id: 1, namespace_id: 2, root_namespace_id: 3 }, - caches: [ - { type: :namespace, id: 3 } - ] - - it_behaves_like 'clears caches with', - event_class: Projects::ProjectArchivedEvent, - event_data: { project_id: 1, namespace_id: 2, root_namespace_id: 3 }, - caches: [ - { type: :namespace, id: 3 } - ] - - it_behaves_like 'clears caches with', - event_class: Projects::ProjectPathChangedEvent, - event_data: { - project_id: 1, - namespace_id: 2, - root_namespace_id: 3, - old_path: 'old_path', - new_path: 'new_path' - }, - caches: [ - { type: :namespace, id: 3 } - ] - - it_behaves_like 'clears caches with', - event_class: Projects::ProjectTransferedEvent, - event_data: { - project_id: 1, - old_namespace_id: 2, - old_root_namespace_id: 3, - new_namespace_id: 4, - new_root_namespace_id: 5 - }, - caches: [ - { type: :namespace, id: 3 }, - { type: :namespace, id: 5 } - ] - - it_behaves_like 'clears caches with', - event_class: Groups::GroupTransferedEvent, - event_data: { - group_id: 1, - old_root_namespace_id: 3, - new_root_namespace_id: 5 - }, - caches: [ - { type: :namespace, id: 3 }, - { type: :namespace, id: 5 } - ] - - it_behaves_like 'clears caches with', - event_class: Groups::GroupPathChangedEvent, - event_data: { - group_id: 1, - root_namespace_id: 2, - old_path: 'old_path', - new_path: 'new_path' - }, - caches: [ - { type: :namespace, id: 2 } - ] - - it_behaves_like 'clears caches with', - event_class: Groups::GroupDeletedEvent, - event_data: { - group_id: 1, - root_namespace_id: 3 - }, - caches: [ - { type: :namespace, id: 3 } - ] - - it_behaves_like 'clears caches with', - event_class: PagesDomains::PagesDomainDeletedEvent, - event_data: { - project_id: 1, - namespace_id: 2, - root_namespace_id: 3, - domain_id: 4, - domain: 'somedomain.com' - }, - caches: [ - { type: :domain, id: 4 }, - { type: :namespace, id: 3 } - ] - - it_behaves_like 'clears caches with', - event_class: PagesDomains::PagesDomainUpdatedEvent, - event_data: { - project_id: 1, - namespace_id: 2, - root_namespace_id: 3, - domain_id: 4, - domain: 'somedomain.com' - }, - caches: [ - { type: :domain, id: 4 }, - { type: :namespace, id: 3 } - ] - - it_behaves_like 'clears caches with', - event_class: PagesDomains::PagesDomainCreatedEvent, - event_data: { - project_id: 1, - namespace_id: 2, - root_namespace_id: 3, - domain_id: 4, - domain: 'somedomain.com' - }, - caches: [ - { type: :domain, id: 4 }, - { type: :namespace, id: 3 } - ] - - context 'when project attributes change' do - Projects::ProjectAttributesChangedEvent::PAGES_RELATED_ATTRIBUTES.each do |attribute| - it_behaves_like 'clears caches with', - event_class: Projects::ProjectAttributesChangedEvent, - event_data: { - project_id: 1, - namespace_id: 2, - root_namespace_id: 3, - domain_id: 4, - attributes: [attribute] - }, - caches: [ - { type: :domain, id: 4 }, - { type: :namespace, id: 3 } - ] - end - - it_behaves_like 'ignores the published event' do - let(:event) do - Projects::ProjectAttributesChangedEvent.new( - data: { - project_id: 1, - namespace_id: 2, - root_namespace_id: 3, - attributes: ['unknown'] - } - ) - end - end - end - - context 'when project features change' do - it_behaves_like 'clears caches with', - event_class: Projects::ProjectFeaturesChangedEvent, - event_data: { - project_id: 1, - namespace_id: 2, - root_namespace_id: 3, - features: ['pages_access_level'] - }, - caches: [ - { type: :namespace, id: 3 } - ] - - it_behaves_like 'ignores the published event' do - let(:event) do - Projects::ProjectFeaturesChangedEvent.new( - data: { - project_id: 1, - namespace_id: 2, - root_namespace_id: 3, - features: ['unknown'] - } - ) - end - end - end - - context 'when namespace based cache keys are duplicated' do - # de-dups namespace cache keys - it_behaves_like 'clears caches with', - event_class: Projects::ProjectTransferedEvent, - event_data: { - project_id: 1, - old_namespace_id: 2, - old_root_namespace_id: 5, - new_namespace_id: 4, - new_root_namespace_id: 5 - }, - caches: [ - { type: :namespace, id: 5 } - ] - end -end diff --git a/spec/workers/personal_access_tokens/expiring_worker_spec.rb b/spec/workers/personal_access_tokens/expiring_worker_spec.rb index 01ce4e85fe2..0cc63fdb85e 100644 --- a/spec/workers/personal_access_tokens/expiring_worker_spec.rb +++ b/spec/workers/personal_access_tokens/expiring_worker_spec.rb @@ -58,5 +58,32 @@ RSpec.describe PersonalAccessTokens::ExpiringWorker, type: :worker, feature_cate expect { worker.perform }.not_to change { pat.reload.expire_notification_delivered } end end + + context 'when a token is owned by a project bot' do + let_it_be(:maintainer1) { create(:user) } + let_it_be(:maintainer2) { create(:user) } + let_it_be(:project_bot) { create(:user, :project_bot) } + let_it_be(:project) { create(:project) } + let_it_be(:expiring_token) { create(:personal_access_token, user: project_bot, expires_at: 5.days.from_now) } + + before_all do + project.add_developer(project_bot) + project.add_maintainer(maintainer1) + project.add_maintainer(maintainer2) + end + + it 'uses notification service to send the email' do + expect_next_instance_of(NotificationService) do |notification_service| + expect(notification_service).to receive(:resource_access_tokens_about_to_expire) + .with(project_bot, match_array([expiring_token.name])) + end + + worker.perform + end + + it 'marks the notification as delivered' do + expect { worker.perform }.to change { expiring_token.reload.expire_notification_delivered }.from(false).to(true) + end + end end end diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb index 5c8a75aca3f..2e0a2535453 100644 --- a/spec/workers/post_receive_spec.rb +++ b/spec/workers/post_receive_spec.rb @@ -282,7 +282,7 @@ RSpec.describe PostReceive, feature_category: :source_code_management do let(:user) { project.creator } let(:label) { 'counts.source_code_pushes' } let(:property) { 'source_code_pushes' } - let(:context) { [Gitlab::Tracking::ServicePingContext.new(data_source: :redis, key_path: label).to_h] } + let(:context) { [Gitlab::Usage::MetricDefinition.context_for(label).to_h] } subject(:post_receive) { perform } end diff --git a/spec/workers/projects/record_target_platforms_worker_spec.rb b/spec/workers/projects/record_target_platforms_worker_spec.rb index ecb6aab7349..116da404112 100644 --- a/spec/workers/projects/record_target_platforms_worker_spec.rb +++ b/spec/workers/projects/record_target_platforms_worker_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Projects::RecordTargetPlatformsWorker, feature_category: :groups_and_projects do +RSpec.describe Projects::RecordTargetPlatformsWorker, feature_category: :experimentation_activation do include ExclusiveLeaseHelpers let_it_be(:swift) { create(:programming_language, name: 'Swift') } diff --git a/spec/workers/users/migrate_records_to_ghost_user_in_batches_worker_spec.rb b/spec/workers/users/migrate_records_to_ghost_user_in_batches_worker_spec.rb index 38ea7c43267..fbbfd44bba6 100644 --- a/spec/workers/users/migrate_records_to_ghost_user_in_batches_worker_spec.rb +++ b/spec/workers/users/migrate_records_to_ghost_user_in_batches_worker_spec.rb @@ -34,8 +34,8 @@ RSpec.describe Users::MigrateRecordsToGhostUserInBatchesWorker, feature_category it 'migrates issue to ghost user' do subject - expect(issue.reload.author).to eq(User.ghost) - expect(issue.last_edited_by).to eq(User.ghost) + expect(issue.reload.author).to eq(Users::Internal.ghost) + expect(issue.last_edited_by).to eq(Users::Internal.ghost) end end end diff --git a/spec/workers/users/track_namespace_visits_worker_spec.rb b/spec/workers/users/track_namespace_visits_worker_spec.rb new file mode 100644 index 00000000000..cfb2b7ab5eb --- /dev/null +++ b/spec/workers/users/track_namespace_visits_worker_spec.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Users::TrackNamespaceVisitsWorker, feature_category: :navigation do + describe '#perform' do + let_it_be(:user) { create(:user) } + + context 'when tracking a group' do + let_it_be(:entity) { create(:group) } + let_it_be(:entity_type) { 'groups' } + let_it_be(:worker) { described_class.new } + let_it_be(:model) { ::Users::GroupVisit } + + it_behaves_like 'namespace visits tracking worker' + end + + context 'when tracking a project' do + let_it_be(:entity) { create(:project) } + let_it_be(:entity_type) { 'projects' } + let_it_be(:worker) { described_class.new } + let_it_be(:model) { ::Users::ProjectVisit } + + it_behaves_like 'namespace visits tracking worker' + end + end +end |