summaryrefslogtreecommitdiff
path: root/spec
diff options
context:
space:
mode:
Diffstat (limited to 'spec')
-rw-r--r--spec/lib/gitlab/background_migration/backfill_ci_queuing_tables_spec.rb245
-rw-r--r--spec/lib/gitlab/background_migration/backfill_group_features_spec.rb2
-rw-r--r--spec/lib/gitlab/background_migration/backfill_integrations_type_new_spec.rb67
-rw-r--r--spec/lib/gitlab/background_migration/backfill_member_namespace_for_group_members_spec.rb2
-rw-r--r--spec/lib/gitlab/background_migration/backfill_namespace_id_for_namespace_route_spec.rb2
-rw-r--r--spec/lib/gitlab/background_migration/backfill_snippet_repositories_spec.rb2
-rw-r--r--spec/lib/gitlab/background_migration/encrypt_integration_properties_spec.rb63
-rw-r--r--spec/lib/gitlab/background_migration/encrypt_static_object_token_spec.rb64
-rw-r--r--spec/lib/gitlab/background_migration/fix_vulnerability_occurrences_with_hashes_as_raw_metadata_spec.rb2
-rw-r--r--spec/lib/gitlab/background_migration/merge_topics_with_same_name_spec.rb148
-rw-r--r--spec/lib/gitlab/background_migration/migrate_personal_namespace_project_maintainer_to_owner_spec.rb2
-rw-r--r--spec/lib/gitlab/background_migration/nullify_orphan_runner_id_on_ci_builds_spec.rb2
-rw-r--r--spec/lib/gitlab/background_migration/populate_namespace_statistics_spec.rb71
-rw-r--r--spec/lib/gitlab/background_migration/populate_topics_non_private_projects_count_spec.rb50
-rw-r--r--spec/lib/gitlab/background_migration/populate_vulnerability_reads_spec.rb93
-rw-r--r--spec/lib/gitlab/background_migration/recalculate_vulnerabilities_occurrences_uuid_spec.rb530
-rw-r--r--spec/lib/gitlab/background_migration/remove_all_trace_expiration_dates_spec.rb54
-rw-r--r--spec/lib/gitlab/background_migration/remove_vulnerability_finding_links_spec.rb2
-rw-r--r--spec/lib/gitlab/background_migration/update_timelogs_null_spent_at_spec.rb40
-rw-r--r--spec/migrations/20211203091642_add_index_to_projects_on_marked_for_deletion_at_spec.rb18
-rw-r--r--spec/migrations/20211207125331_remove_jobs_for_recalculate_vulnerabilities_occurrences_uuid_spec.rb45
-rw-r--r--spec/migrations/20211207135331_schedule_recalculate_uuid_on_vulnerabilities_occurrences4_spec.rb148
-rw-r--r--spec/migrations/20211210140629_encrypt_static_object_token_spec.rb50
-rw-r--r--spec/migrations/20211214012507_backfill_incident_issue_escalation_statuses_spec.rb19
-rw-r--r--spec/migrations/20211217174331_mark_recalculate_finding_signatures_as_completed_spec.rb64
-rw-r--r--spec/migrations/20220106111958_add_insert_or_update_vulnerability_reads_trigger_spec.rb151
-rw-r--r--spec/migrations/20220106112043_add_update_vulnerability_reads_trigger_spec.rb128
-rw-r--r--spec/migrations/20220106112085_add_update_vulnerability_reads_location_trigger_spec.rb136
-rw-r--r--spec/migrations/20220106163326_add_has_issues_on_vulnerability_reads_trigger_spec.rb134
-rw-r--r--spec/migrations/20220107064845_populate_vulnerability_reads_spec.rb106
-rw-r--r--spec/migrations/20220120094340_drop_position_from_security_findings_spec.rb21
-rw-r--r--spec/migrations/20220124130028_dedup_runner_projects_spec.rb66
-rw-r--r--spec/migrations/20220128155251_remove_dangling_running_builds_spec.rb53
-rw-r--r--spec/migrations/20220128155814_fix_approval_rules_code_owners_rule_type_index_spec.rb33
-rw-r--r--spec/migrations/20220202105733_delete_service_template_records_spec.rb42
-rw-r--r--spec/migrations/20220204095121_backfill_namespace_statistics_with_dependency_proxy_size_spec.rb64
-rw-r--r--spec/migrations/20220204194347_encrypt_integration_properties_spec.rb40
-rw-r--r--spec/migrations/20220208080921_schedule_migrate_personal_namespace_project_maintainer_to_owner_spec.rb20
-rw-r--r--spec/migrations/20220211214605_update_integrations_trigger_type_new_on_insert_null_safe_spec.rb37
-rw-r--r--spec/migrations/20220213103859_remove_integrations_type_spec.rb31
-rw-r--r--spec/migrations/20220222192524_create_not_null_constraint_releases_tag_spec.rb23
-rw-r--r--spec/migrations/20220222192525_remove_null_releases_spec.rb22
-rw-r--r--spec/migrations/20220223124428_schedule_merge_topics_with_same_name_spec.rb36
-rw-r--r--spec/migrations/20220305223212_add_security_training_providers_spec.rb25
-rw-r--r--spec/migrations/20220307192610_remove_duplicate_project_tag_releases_spec.rb45
-rw-r--r--spec/migrations/20220309084954_remove_leftover_external_pull_request_deletions_spec.rb43
-rw-r--r--spec/migrations/20220310141349_remove_dependency_list_usage_data_from_redis_spec.rb24
-rw-r--r--spec/migrations/backfill_all_project_namespaces_spec.rb37
-rw-r--r--spec/migrations/backfill_cycle_analytics_aggregations_spec.rb36
-rw-r--r--spec/migrations/backfill_group_features_spec.rb31
-rw-r--r--spec/migrations/backfill_member_namespace_id_for_group_members_spec.rb29
-rw-r--r--spec/migrations/backfill_namespace_id_for_namespace_routes_spec.rb29
-rw-r--r--spec/migrations/backfill_project_namespaces_for_group_spec.rb43
-rw-r--r--spec/migrations/populate_audit_event_streaming_verification_token_spec.rb22
-rw-r--r--spec/migrations/recreate_index_security_ci_builds_on_name_and_id_parser_with_new_features_spec.rb28
-rw-r--r--spec/migrations/remove_not_null_contraint_on_title_from_sprints_spec.rb29
-rw-r--r--spec/migrations/schedule_fix_incorrect_max_seats_used2_spec.rb34
-rw-r--r--spec/migrations/schedule_fix_incorrect_max_seats_used_spec.rb26
-rw-r--r--spec/migrations/schedule_update_timelogs_null_spent_at_spec.rb44
-rw-r--r--spec/migrations/start_backfill_ci_queuing_tables_spec.rb49
-rw-r--r--spec/migrations/update_application_settings_container_registry_exp_pol_worker_capacity_default_spec.rb41
-rw-r--r--spec/migrations/update_application_settings_protected_paths_spec.rb47
-rw-r--r--spec/migrations/update_default_scan_method_of_dast_site_profile_spec.rb42
-rw-r--r--spec/migrations/update_invalid_member_states_spec.rb30
64 files changed, 3654 insertions, 8 deletions
diff --git a/spec/lib/gitlab/background_migration/backfill_ci_queuing_tables_spec.rb b/spec/lib/gitlab/background_migration/backfill_ci_queuing_tables_spec.rb
new file mode 100644
index 00000000000..aaf8c124a83
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/backfill_ci_queuing_tables_spec.rb
@@ -0,0 +1,245 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::BackfillCiQueuingTables, :migration,
+ :suppress_gitlab_schemas_validate_connection, schema: 20220208115439 do
+ let(:namespaces) { table(:namespaces) }
+ let(:projects) { table(:projects) }
+ let(:ci_cd_settings) { table(:project_ci_cd_settings) }
+ let(:builds) { table(:ci_builds) }
+ let(:queuing_entries) { table(:ci_pending_builds) }
+ let(:tags) { table(:tags) }
+ let(:taggings) { table(:taggings) }
+
+ subject { described_class.new }
+
+ describe '#perform' do
+ let!(:namespace) do
+ namespaces.create!(
+ id: 10,
+ name: 'namespace10',
+ path: 'namespace10',
+ traversal_ids: [10])
+ end
+
+ let!(:other_namespace) do
+ namespaces.create!(
+ id: 11,
+ name: 'namespace11',
+ path: 'namespace11',
+ traversal_ids: [11])
+ end
+
+ let!(:project) do
+ projects.create!(id: 5, namespace_id: 10, name: 'test1', path: 'test1')
+ end
+
+ let!(:ci_cd_setting) do
+ ci_cd_settings.create!(id: 5, project_id: 5, group_runners_enabled: true)
+ end
+
+ let!(:other_project) do
+ projects.create!(id: 7, namespace_id: 11, name: 'test2', path: 'test2')
+ end
+
+ let!(:other_ci_cd_setting) do
+ ci_cd_settings.create!(id: 7, project_id: 7, group_runners_enabled: false)
+ end
+
+ let!(:another_project) do
+ projects.create!(id: 9, namespace_id: 10, name: 'test3', path: 'test3', shared_runners_enabled: false)
+ end
+
+ let!(:ruby_tag) do
+ tags.create!(id: 22, name: 'ruby')
+ end
+
+ let!(:postgres_tag) do
+ tags.create!(id: 23, name: 'postgres')
+ end
+
+ it 'creates ci_pending_builds for all pending builds in range' do
+ builds.create!(id: 50, status: :pending, name: 'test1', project_id: 5, type: 'Ci::Build')
+ builds.create!(id: 51, status: :created, name: 'test2', project_id: 5, type: 'Ci::Build')
+ builds.create!(id: 52, status: :pending, name: 'test3', project_id: 5, protected: true, type: 'Ci::Build')
+
+ taggings.create!(taggable_id: 52, taggable_type: 'CommitStatus', tag_id: 22)
+ taggings.create!(taggable_id: 52, taggable_type: 'CommitStatus', tag_id: 23)
+
+ builds.create!(id: 60, status: :pending, name: 'test1', project_id: 7, type: 'Ci::Build')
+ builds.create!(id: 61, status: :running, name: 'test2', project_id: 7, protected: true, type: 'Ci::Build')
+ builds.create!(id: 62, status: :pending, name: 'test3', project_id: 7, type: 'Ci::Build')
+
+ taggings.create!(taggable_id: 60, taggable_type: 'CommitStatus', tag_id: 23)
+ taggings.create!(taggable_id: 62, taggable_type: 'CommitStatus', tag_id: 22)
+
+ builds.create!(id: 70, status: :pending, name: 'test1', project_id: 9, protected: true, type: 'Ci::Build')
+ builds.create!(id: 71, status: :failed, name: 'test2', project_id: 9, type: 'Ci::Build')
+ builds.create!(id: 72, status: :pending, name: 'test3', project_id: 9, type: 'Ci::Build')
+
+ taggings.create!(taggable_id: 71, taggable_type: 'CommitStatus', tag_id: 22)
+
+ subject.perform(1, 100)
+
+ expect(queuing_entries.all).to contain_exactly(
+ an_object_having_attributes(
+ build_id: 50,
+ project_id: 5,
+ namespace_id: 10,
+ protected: false,
+ instance_runners_enabled: true,
+ minutes_exceeded: false,
+ tag_ids: [],
+ namespace_traversal_ids: [10]),
+ an_object_having_attributes(
+ build_id: 52,
+ project_id: 5,
+ namespace_id: 10,
+ protected: true,
+ instance_runners_enabled: true,
+ minutes_exceeded: false,
+ tag_ids: match_array([22, 23]),
+ namespace_traversal_ids: [10]),
+ an_object_having_attributes(
+ build_id: 60,
+ project_id: 7,
+ namespace_id: 11,
+ protected: false,
+ instance_runners_enabled: true,
+ minutes_exceeded: false,
+ tag_ids: [23],
+ namespace_traversal_ids: []),
+ an_object_having_attributes(
+ build_id: 62,
+ project_id: 7,
+ namespace_id: 11,
+ protected: false,
+ instance_runners_enabled: true,
+ minutes_exceeded: false,
+ tag_ids: [22],
+ namespace_traversal_ids: []),
+ an_object_having_attributes(
+ build_id: 70,
+ project_id: 9,
+ namespace_id: 10,
+ protected: true,
+ instance_runners_enabled: false,
+ minutes_exceeded: false,
+ tag_ids: [],
+ namespace_traversal_ids: []),
+ an_object_having_attributes(
+ build_id: 72,
+ project_id: 9,
+ namespace_id: 10,
+ protected: false,
+ instance_runners_enabled: false,
+ minutes_exceeded: false,
+ tag_ids: [],
+ namespace_traversal_ids: [])
+ )
+ end
+
+ it 'skips builds that already have ci_pending_builds' do
+ builds.create!(id: 50, status: :pending, name: 'test1', project_id: 5, type: 'Ci::Build')
+ builds.create!(id: 51, status: :created, name: 'test2', project_id: 5, type: 'Ci::Build')
+ builds.create!(id: 52, status: :pending, name: 'test3', project_id: 5, protected: true, type: 'Ci::Build')
+
+ taggings.create!(taggable_id: 50, taggable_type: 'CommitStatus', tag_id: 22)
+ taggings.create!(taggable_id: 52, taggable_type: 'CommitStatus', tag_id: 23)
+
+ queuing_entries.create!(build_id: 50, project_id: 5, namespace_id: 10)
+
+ subject.perform(1, 100)
+
+ expect(queuing_entries.all).to contain_exactly(
+ an_object_having_attributes(
+ build_id: 50,
+ project_id: 5,
+ namespace_id: 10,
+ protected: false,
+ instance_runners_enabled: false,
+ minutes_exceeded: false,
+ tag_ids: [],
+ namespace_traversal_ids: []),
+ an_object_having_attributes(
+ build_id: 52,
+ project_id: 5,
+ namespace_id: 10,
+ protected: true,
+ instance_runners_enabled: true,
+ minutes_exceeded: false,
+ tag_ids: [23],
+ namespace_traversal_ids: [10])
+ )
+ end
+
+ it 'upserts values in case of conflicts' do
+ builds.create!(id: 50, status: :pending, name: 'test1', project_id: 5, type: 'Ci::Build')
+ queuing_entries.create!(build_id: 50, project_id: 5, namespace_id: 10)
+
+ build = described_class::Ci::Build.find(50)
+ described_class::Ci::PendingBuild.upsert_from_build!(build)
+
+ expect(queuing_entries.all).to contain_exactly(
+ an_object_having_attributes(
+ build_id: 50,
+ project_id: 5,
+ namespace_id: 10,
+ protected: false,
+ instance_runners_enabled: true,
+ minutes_exceeded: false,
+ tag_ids: [],
+ namespace_traversal_ids: [10])
+ )
+ end
+ end
+
+ context 'Ci::Build' do
+ describe '.each_batch' do
+ let(:model) { described_class::Ci::Build }
+
+ before do
+ builds.create!(id: 1, status: :pending, name: 'test1', project_id: 5, type: 'Ci::Build')
+ builds.create!(id: 2, status: :pending, name: 'test2', project_id: 5, type: 'Ci::Build')
+ builds.create!(id: 3, status: :pending, name: 'test3', project_id: 5, type: 'Ci::Build')
+ builds.create!(id: 4, status: :pending, name: 'test4', project_id: 5, type: 'Ci::Build')
+ builds.create!(id: 5, status: :pending, name: 'test5', project_id: 5, type: 'Ci::Build')
+ end
+
+ it 'yields an ActiveRecord::Relation when a block is given' do
+ model.each_batch do |relation|
+ expect(relation).to be_a_kind_of(ActiveRecord::Relation)
+ end
+ end
+
+ it 'yields a batch index as the second argument' do
+ model.each_batch do |_, index|
+ expect(index).to eq(1)
+ end
+ end
+
+ it 'accepts a custom batch size' do
+ amount = 0
+
+ model.each_batch(of: 1) { amount += 1 }
+
+ expect(amount).to eq(5)
+ end
+
+ it 'does not include ORDER BYs in the yielded relations' do
+ model.each_batch do |relation|
+ expect(relation.to_sql).not_to include('ORDER BY')
+ end
+ end
+
+ it 'orders ascending' do
+ ids = []
+
+ model.each_batch(of: 1) { |rel| ids.concat(rel.ids) }
+
+ expect(ids).to eq(ids.sort)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/backfill_group_features_spec.rb b/spec/lib/gitlab/background_migration/backfill_group_features_spec.rb
index 2c2740434de..e0be5a785b8 100644
--- a/spec/lib/gitlab/background_migration/backfill_group_features_spec.rb
+++ b/spec/lib/gitlab/background_migration/backfill_group_features_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::BackgroundMigration::BackfillGroupFeatures, :migration, schema: 20220314184009 do
+RSpec.describe Gitlab::BackgroundMigration::BackfillGroupFeatures, :migration, schema: 20220302114046 do
let(:group_features) { table(:group_features) }
let(:namespaces) { table(:namespaces) }
diff --git a/spec/lib/gitlab/background_migration/backfill_integrations_type_new_spec.rb b/spec/lib/gitlab/background_migration/backfill_integrations_type_new_spec.rb
new file mode 100644
index 00000000000..e6588644b4f
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/backfill_integrations_type_new_spec.rb
@@ -0,0 +1,67 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::BackfillIntegrationsTypeNew, :migration, schema: 20220212120735 do
+ let(:migration) { described_class.new }
+ let(:integrations) { table(:integrations) }
+
+ let(:namespaced_integrations) do
+ Set.new(
+ %w[
+ Asana Assembla Bamboo Bugzilla Buildkite Campfire Confluence CustomIssueTracker Datadog
+ Discord DroneCi EmailsOnPush Ewm ExternalWiki Flowdock HangoutsChat Harbor Irker Jenkins Jira Mattermost
+ MattermostSlashCommands MicrosoftTeams MockCi MockMonitoring Packagist PipelinesEmail Pivotaltracker
+ Prometheus Pushover Redmine Shimo Slack SlackSlashCommands Teamcity UnifyCircuit WebexTeams Youtrack Zentao
+ Github GitlabSlackApplication
+ ]).freeze
+ end
+
+ before do
+ integrations.connection.execute 'ALTER TABLE integrations DISABLE TRIGGER "trigger_type_new_on_insert"'
+
+ namespaced_integrations.each_with_index do |type, i|
+ integrations.create!(id: i + 1, type: "#{type}Service")
+ end
+
+ integrations.create!(id: namespaced_integrations.size + 1, type: 'LegacyService')
+ ensure
+ integrations.connection.execute 'ALTER TABLE integrations ENABLE TRIGGER "trigger_type_new_on_insert"'
+ end
+
+ it 'backfills `type_new` for the selected records' do
+ # We don't want to mock `Kernel.sleep`, so instead we mock it on the migration
+ # class before it gets forwarded.
+ expect(migration).to receive(:sleep).with(0.05).exactly(5).times
+
+ queries = ActiveRecord::QueryRecorder.new do
+ migration.perform(2, 10, :integrations, :id, 2, 50)
+ end
+
+ expect(queries.count).to be(16)
+ expect(queries.log.grep(/^SELECT/).size).to be(11)
+ expect(queries.log.grep(/^UPDATE/).size).to be(5)
+ expect(queries.log.grep(/^UPDATE/).join.scan(/WHERE .*/)).to eq(
+ [
+ 'WHERE integrations.id BETWEEN 2 AND 3',
+ 'WHERE integrations.id BETWEEN 4 AND 5',
+ 'WHERE integrations.id BETWEEN 6 AND 7',
+ 'WHERE integrations.id BETWEEN 8 AND 9',
+ 'WHERE integrations.id BETWEEN 10 AND 10'
+ ])
+
+ expect(integrations.where(id: 2..10).pluck(:type, :type_new)).to contain_exactly(
+ ['AssemblaService', 'Integrations::Assembla'],
+ ['BambooService', 'Integrations::Bamboo'],
+ ['BugzillaService', 'Integrations::Bugzilla'],
+ ['BuildkiteService', 'Integrations::Buildkite'],
+ ['CampfireService', 'Integrations::Campfire'],
+ ['ConfluenceService', 'Integrations::Confluence'],
+ ['CustomIssueTrackerService', 'Integrations::CustomIssueTracker'],
+ ['DatadogService', 'Integrations::Datadog'],
+ ['DiscordService', 'Integrations::Discord']
+ )
+
+ expect(integrations.where.not(id: 2..10)).to all(have_attributes(type_new: nil))
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/backfill_member_namespace_for_group_members_spec.rb b/spec/lib/gitlab/background_migration/backfill_member_namespace_for_group_members_spec.rb
index ea07079f9ee..e1ef12a1479 100644
--- a/spec/lib/gitlab/background_migration/backfill_member_namespace_for_group_members_spec.rb
+++ b/spec/lib/gitlab/background_migration/backfill_member_namespace_for_group_members_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::BackgroundMigration::BackfillMemberNamespaceForGroupMembers, :migration, schema: 20220314184009 do
+RSpec.describe Gitlab::BackgroundMigration::BackfillMemberNamespaceForGroupMembers, :migration, schema: 20220120211832 do
let(:migration) { described_class.new }
let(:members_table) { table(:members) }
let(:namespaces_table) { table(:namespaces) }
diff --git a/spec/lib/gitlab/background_migration/backfill_namespace_id_for_namespace_route_spec.rb b/spec/lib/gitlab/background_migration/backfill_namespace_id_for_namespace_route_spec.rb
index f4e8fa1bbac..b821efcadb0 100644
--- a/spec/lib/gitlab/background_migration/backfill_namespace_id_for_namespace_route_spec.rb
+++ b/spec/lib/gitlab/background_migration/backfill_namespace_id_for_namespace_route_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::BackgroundMigration::BackfillNamespaceIdForNamespaceRoute, :migration, schema: 20220314184009 do
+RSpec.describe Gitlab::BackgroundMigration::BackfillNamespaceIdForNamespaceRoute, :migration, schema: 20220120123800 do
let(:migration) { described_class.new }
let(:namespaces_table) { table(:namespaces) }
let(:projects_table) { table(:projects) }
diff --git a/spec/lib/gitlab/background_migration/backfill_snippet_repositories_spec.rb b/spec/lib/gitlab/background_migration/backfill_snippet_repositories_spec.rb
index 6f6ff9232e0..4a50d08b2aa 100644
--- a/spec/lib/gitlab/background_migration/backfill_snippet_repositories_spec.rb
+++ b/spec/lib/gitlab/background_migration/backfill_snippet_repositories_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::BackgroundMigration::BackfillSnippetRepositories, :migration, schema: 20220314184009,
+RSpec.describe Gitlab::BackgroundMigration::BackfillSnippetRepositories, :migration, schema: 20211202041233,
feature_category: :source_code_management do
let(:gitlab_shell) { Gitlab::Shell.new }
let(:users) { table(:users) }
diff --git a/spec/lib/gitlab/background_migration/encrypt_integration_properties_spec.rb b/spec/lib/gitlab/background_migration/encrypt_integration_properties_spec.rb
new file mode 100644
index 00000000000..c788b701d79
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/encrypt_integration_properties_spec.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::EncryptIntegrationProperties, schema: 20220415124804 do
+ let(:integrations) do
+ table(:integrations) do |integrations|
+ integrations.send :attr_encrypted, :encrypted_properties_tmp,
+ attribute: :encrypted_properties,
+ mode: :per_attribute_iv,
+ key: ::Settings.attr_encrypted_db_key_base_32,
+ algorithm: 'aes-256-gcm',
+ marshal: true,
+ marshaler: ::Gitlab::Json,
+ encode: false,
+ encode_iv: false
+ end
+ end
+
+ let!(:no_properties) { integrations.create! }
+ let!(:with_plaintext_1) { integrations.create!(properties: json_props(1)) }
+ let!(:with_plaintext_2) { integrations.create!(properties: json_props(2)) }
+ let!(:with_encrypted) do
+ x = integrations.new
+ x.properties = nil
+ x.encrypted_properties_tmp = some_props(3)
+ x.save!
+ x
+ end
+
+ let(:start_id) { integrations.minimum(:id) }
+ let(:end_id) { integrations.maximum(:id) }
+
+ it 'ensures all properties are encrypted', :aggregate_failures do
+ described_class.new.perform(start_id, end_id)
+
+ props = integrations.all.to_h do |record|
+ [record.id, [Gitlab::Json.parse(record.properties), record.encrypted_properties_tmp]]
+ end
+
+ expect(integrations.count).to eq(4)
+
+ expect(props).to match(
+ no_properties.id => both(be_nil),
+ with_plaintext_1.id => both(eq some_props(1)),
+ with_plaintext_2.id => both(eq some_props(2)),
+ with_encrypted.id => match([be_nil, eq(some_props(3))])
+ )
+ end
+
+ private
+
+ def both(obj)
+ match [obj, obj]
+ end
+
+ def some_props(id)
+ HashWithIndifferentAccess.new({ id: id, foo: 1, bar: true, baz: %w[a string array] })
+ end
+
+ def json_props(id)
+ some_props(id).to_json
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/encrypt_static_object_token_spec.rb b/spec/lib/gitlab/background_migration/encrypt_static_object_token_spec.rb
new file mode 100644
index 00000000000..4e7b97d33f6
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/encrypt_static_object_token_spec.rb
@@ -0,0 +1,64 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::EncryptStaticObjectToken do
+ let(:users) { table(:users) }
+ let!(:user_without_tokens) { create_user!(name: 'notoken') }
+ let!(:user_with_plaintext_token_1) { create_user!(name: 'plaintext_1', token: 'token') }
+ let!(:user_with_plaintext_token_2) { create_user!(name: 'plaintext_2', token: 'TOKEN') }
+ let!(:user_with_plaintext_empty_token) { create_user!(name: 'plaintext_3', token: '') }
+ let!(:user_with_encrypted_token) { create_user!(name: 'encrypted', encrypted_token: 'encrypted') }
+ let!(:user_with_both_tokens) { create_user!(name: 'both', token: 'token2', encrypted_token: 'encrypted2') }
+
+ before do
+ allow(Gitlab::CryptoHelper).to receive(:aes256_gcm_encrypt).and_call_original
+ allow(Gitlab::CryptoHelper).to receive(:aes256_gcm_encrypt).with('token') { 'secure_token' }
+ allow(Gitlab::CryptoHelper).to receive(:aes256_gcm_encrypt).with('TOKEN') { 'SECURE_TOKEN' }
+ end
+
+ subject { described_class.new.perform(start_id, end_id) }
+
+ let(:start_id) { users.minimum(:id) }
+ let(:end_id) { users.maximum(:id) }
+
+ it 'backfills encrypted tokens to users with plaintext token only', :aggregate_failures do
+ subject
+
+ new_state = users.pluck(:id, :static_object_token, :static_object_token_encrypted).to_h do |row|
+ [row[0], [row[1], row[2]]]
+ end
+
+ expect(new_state.count).to eq(6)
+
+ expect(new_state[user_with_plaintext_token_1.id]).to match_array(%w[token secure_token])
+ expect(new_state[user_with_plaintext_token_2.id]).to match_array(%w[TOKEN SECURE_TOKEN])
+
+ expect(new_state[user_with_plaintext_empty_token.id]).to match_array(['', nil])
+ expect(new_state[user_without_tokens.id]).to match_array([nil, nil])
+ expect(new_state[user_with_both_tokens.id]).to match_array(%w[token2 encrypted2])
+ expect(new_state[user_with_encrypted_token.id]).to match_array([nil, 'encrypted'])
+ end
+
+ context 'when id range does not include existing user ids' do
+ let(:arguments) { [non_existing_record_id, non_existing_record_id.succ] }
+
+ it_behaves_like 'marks background migration job records' do
+ subject { described_class.new }
+ end
+ end
+
+ private
+
+ def create_user!(name:, token: nil, encrypted_token: nil)
+ email = "#{name}@example.com"
+
+ table(:users).create!(
+ name: name,
+ email: email,
+ username: name,
+ projects_limit: 0,
+ static_object_token: token,
+ static_object_token_encrypted: encrypted_token
+ )
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/fix_vulnerability_occurrences_with_hashes_as_raw_metadata_spec.rb b/spec/lib/gitlab/background_migration/fix_vulnerability_occurrences_with_hashes_as_raw_metadata_spec.rb
index 3cbc05b762a..af551861d47 100644
--- a/spec/lib/gitlab/background_migration/fix_vulnerability_occurrences_with_hashes_as_raw_metadata_spec.rb
+++ b/spec/lib/gitlab/background_migration/fix_vulnerability_occurrences_with_hashes_as_raw_metadata_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::BackgroundMigration::FixVulnerabilityOccurrencesWithHashesAsRawMetadata, schema: 20220314184009 do
+RSpec.describe Gitlab::BackgroundMigration::FixVulnerabilityOccurrencesWithHashesAsRawMetadata, schema: 20211209203821 do
let(:users) { table(:users) }
let(:namespaces) { table(:namespaces) }
let(:projects) { table(:projects) }
diff --git a/spec/lib/gitlab/background_migration/merge_topics_with_same_name_spec.rb b/spec/lib/gitlab/background_migration/merge_topics_with_same_name_spec.rb
new file mode 100644
index 00000000000..2c2c048992f
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/merge_topics_with_same_name_spec.rb
@@ -0,0 +1,148 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::MergeTopicsWithSameName, schema: 20220331133802 do
+ def set_avatar(topic_id, avatar)
+ topic = ::Projects::Topic.find(topic_id)
+ topic.avatar = avatar
+ topic.save!
+ topic.avatar.absolute_path
+ end
+
+ it 'merges project topics with same case insensitive name' do
+ namespaces = table(:namespaces)
+ projects = table(:projects)
+ topics = table(:topics)
+ project_topics = table(:project_topics)
+
+ group_1 = namespaces.create!(name: 'space1', type: 'Group', path: 'space1')
+ group_2 = namespaces.create!(name: 'space2', type: 'Group', path: 'space2')
+ group_3 = namespaces.create!(name: 'space3', type: 'Group', path: 'space3')
+ proj_space_1 = namespaces.create!(name: 'proj1', path: 'proj1', type: 'Project', parent_id: group_1.id)
+ proj_space_2 = namespaces.create!(name: 'proj2', path: 'proj2', type: 'Project', parent_id: group_2.id)
+ proj_space_3 = namespaces.create!(name: 'proj3', path: 'proj3', type: 'Project', parent_id: group_3.id)
+ project_1 = projects.create!(namespace_id: group_1.id, project_namespace_id: proj_space_1.id, visibility_level: 20)
+ project_2 = projects.create!(namespace_id: group_2.id, project_namespace_id: proj_space_2.id, visibility_level: 10)
+ project_3 = projects.create!(namespace_id: group_3.id, project_namespace_id: proj_space_3.id, visibility_level: 0)
+ topic_1_keep = topics.create!(
+ name: 'topic1',
+ title: 'Topic 1',
+ description: 'description 1 to keep',
+ total_projects_count: 2,
+ non_private_projects_count: 2
+ )
+ topic_1_remove = topics.create!(
+ name: 'TOPIC1',
+ title: 'Topic 1',
+ description: 'description 1 to remove',
+ total_projects_count: 2,
+ non_private_projects_count: 1
+ )
+ topic_2_remove = topics.create!(
+ name: 'topic2',
+ title: 'Topic 2',
+ total_projects_count: 0
+ )
+ topic_2_keep = topics.create!(
+ name: 'TOPIC2',
+ title: 'Topic 2',
+ description: 'description 2 to keep',
+ total_projects_count: 1
+ )
+ topic_3_remove_1 = topics.create!(
+ name: 'topic3',
+ title: 'Topic 3',
+ total_projects_count: 2,
+ non_private_projects_count: 1
+ )
+ topic_3_keep = topics.create!(
+ name: 'Topic3',
+ title: 'Topic 3',
+ total_projects_count: 2,
+ non_private_projects_count: 2
+ )
+ topic_3_remove_2 = topics.create!(
+ name: 'TOPIC3',
+ title: 'Topic 3',
+ description: 'description 3 to keep',
+ total_projects_count: 2,
+ non_private_projects_count: 1
+ )
+ topic_4_keep = topics.create!(
+ name: 'topic4',
+ title: 'Topic 4'
+ )
+
+ project_topics_1 = []
+ project_topics_3 = []
+ project_topics_removed = []
+
+ project_topics_1 << project_topics.create!(topic_id: topic_1_keep.id, project_id: project_1.id)
+ project_topics_1 << project_topics.create!(topic_id: topic_1_keep.id, project_id: project_2.id)
+ project_topics_removed << project_topics.create!(topic_id: topic_1_remove.id, project_id: project_2.id)
+ project_topics_1 << project_topics.create!(topic_id: topic_1_remove.id, project_id: project_3.id)
+
+ project_topics_3 << project_topics.create!(topic_id: topic_3_keep.id, project_id: project_1.id)
+ project_topics_3 << project_topics.create!(topic_id: topic_3_keep.id, project_id: project_2.id)
+ project_topics_removed << project_topics.create!(topic_id: topic_3_remove_1.id, project_id: project_1.id)
+ project_topics_3 << project_topics.create!(topic_id: topic_3_remove_1.id, project_id: project_3.id)
+ project_topics_removed << project_topics.create!(topic_id: topic_3_remove_2.id, project_id: project_1.id)
+ project_topics_removed << project_topics.create!(topic_id: topic_3_remove_2.id, project_id: project_3.id)
+
+ avatar_paths = {
+ topic_1_keep: set_avatar(topic_1_keep.id, fixture_file_upload('spec/fixtures/avatars/avatar1.png')),
+ topic_1_remove: set_avatar(topic_1_remove.id, fixture_file_upload('spec/fixtures/avatars/avatar2.png')),
+ topic_2_remove: set_avatar(topic_2_remove.id, fixture_file_upload('spec/fixtures/avatars/avatar3.png')),
+ topic_3_remove_1: set_avatar(topic_3_remove_1.id, fixture_file_upload('spec/fixtures/avatars/avatar4.png')),
+ topic_3_remove_2: set_avatar(topic_3_remove_2.id, fixture_file_upload('spec/fixtures/avatars/avatar5.png'))
+ }
+
+ subject.perform(%w[topic1 topic2 topic3 topic4])
+
+ # Topics
+ [topic_1_keep, topic_2_keep, topic_3_keep, topic_4_keep].each(&:reload)
+ expect(topic_1_keep.name).to eq('topic1')
+ expect(topic_1_keep.description).to eq('description 1 to keep')
+ expect(topic_1_keep.total_projects_count).to eq(3)
+ expect(topic_1_keep.non_private_projects_count).to eq(2)
+ expect(topic_2_keep.name).to eq('TOPIC2')
+ expect(topic_2_keep.description).to eq('description 2 to keep')
+ expect(topic_2_keep.total_projects_count).to eq(0)
+ expect(topic_2_keep.non_private_projects_count).to eq(0)
+ expect(topic_3_keep.name).to eq('Topic3')
+ expect(topic_3_keep.description).to eq('description 3 to keep')
+ expect(topic_3_keep.total_projects_count).to eq(3)
+ expect(topic_3_keep.non_private_projects_count).to eq(2)
+ expect(topic_4_keep.reload.name).to eq('topic4')
+
+ [topic_1_remove, topic_2_remove, topic_3_remove_1, topic_3_remove_2].each do |topic|
+ expect { topic.reload }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+
+ # Topic avatars
+ expect(topic_1_keep.avatar).to eq('avatar1.png')
+ expect(File.exist?(::Projects::Topic.find(topic_1_keep.id).avatar.absolute_path)).to be_truthy
+ expect(topic_2_keep.avatar).to eq('avatar3.png')
+ expect(File.exist?(::Projects::Topic.find(topic_2_keep.id).avatar.absolute_path)).to be_truthy
+ expect(topic_3_keep.avatar).to eq('avatar4.png')
+ expect(File.exist?(::Projects::Topic.find(topic_3_keep.id).avatar.absolute_path)).to be_truthy
+
+ [:topic_1_remove, :topic_2_remove, :topic_3_remove_1, :topic_3_remove_2].each do |topic|
+ expect(File.exist?(avatar_paths[topic])).to be_falsey
+ end
+
+ # Project Topic assignments
+ project_topics_1.each do |project_topic|
+ expect(project_topic.reload.topic_id).to eq(topic_1_keep.id)
+ end
+
+ project_topics_3.each do |project_topic|
+ expect(project_topic.reload.topic_id).to eq(topic_3_keep.id)
+ end
+
+ project_topics_removed.each do |project_topic|
+ expect { project_topic.reload }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/migrate_personal_namespace_project_maintainer_to_owner_spec.rb b/spec/lib/gitlab/background_migration/migrate_personal_namespace_project_maintainer_to_owner_spec.rb
index 90d05ccbe1a..07e77bdbc13 100644
--- a/spec/lib/gitlab/background_migration/migrate_personal_namespace_project_maintainer_to_owner_spec.rb
+++ b/spec/lib/gitlab/background_migration/migrate_personal_namespace_project_maintainer_to_owner_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::BackgroundMigration::MigratePersonalNamespaceProjectMaintainerToOwner, :migration, schema: 20220314184009 do
+RSpec.describe Gitlab::BackgroundMigration::MigratePersonalNamespaceProjectMaintainerToOwner, :migration, schema: 20220208080921 do
let(:migration) { described_class.new }
let(:users_table) { table(:users) }
let(:members_table) { table(:members) }
diff --git a/spec/lib/gitlab/background_migration/nullify_orphan_runner_id_on_ci_builds_spec.rb b/spec/lib/gitlab/background_migration/nullify_orphan_runner_id_on_ci_builds_spec.rb
index 7c78350e697..2f0eef3c399 100644
--- a/spec/lib/gitlab/background_migration/nullify_orphan_runner_id_on_ci_builds_spec.rb
+++ b/spec/lib/gitlab/background_migration/nullify_orphan_runner_id_on_ci_builds_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe Gitlab::BackgroundMigration::NullifyOrphanRunnerIdOnCiBuilds,
- :suppress_gitlab_schemas_validate_connection, migration: :gitlab_ci, schema: 20220314184009 do
+ :suppress_gitlab_schemas_validate_connection, migration: :gitlab_ci, schema: 20220223112304 do
let(:namespaces) { table(:namespaces) }
let(:projects) { table(:projects) }
let(:ci_runners) { table(:ci_runners) }
diff --git a/spec/lib/gitlab/background_migration/populate_namespace_statistics_spec.rb b/spec/lib/gitlab/background_migration/populate_namespace_statistics_spec.rb
new file mode 100644
index 00000000000..4a7d52ee784
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/populate_namespace_statistics_spec.rb
@@ -0,0 +1,71 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::PopulateNamespaceStatistics do
+ let!(:namespaces) { table(:namespaces) }
+ let!(:namespace_statistics) { table(:namespace_statistics) }
+ let!(:dependency_proxy_manifests) { table(:dependency_proxy_manifests) }
+ let!(:dependency_proxy_blobs) { table(:dependency_proxy_blobs) }
+
+ let!(:group1) { namespaces.create!(id: 10, type: 'Group', name: 'group1', path: 'group1') }
+ let!(:group2) { namespaces.create!(id: 20, type: 'Group', name: 'group2', path: 'group2') }
+
+ let!(:group1_manifest) do
+ dependency_proxy_manifests.create!(group_id: 10, size: 20, file_name: 'test-file', file: 'test', digest: 'abc123')
+ end
+
+ let!(:group2_manifest) do
+ dependency_proxy_manifests.create!(group_id: 20, size: 20, file_name: 'test-file', file: 'test', digest: 'abc123')
+ end
+
+ let!(:group1_stats) { namespace_statistics.create!(id: 10, namespace_id: 10) }
+
+ let(:ids) { namespaces.pluck(:id) }
+ let(:statistics) { [] }
+
+ subject(:perform) { described_class.new.perform(ids, statistics) }
+
+ it 'creates/updates all namespace_statistics and updates root storage statistics', :aggregate_failures do
+ expect(Namespaces::ScheduleAggregationWorker).to receive(:perform_async).with(group1.id)
+ expect(Namespaces::ScheduleAggregationWorker).to receive(:perform_async).with(group2.id)
+
+ expect { perform }.to change(namespace_statistics, :count).from(1).to(2)
+
+ namespace_statistics.all.each do |stat|
+ expect(stat.dependency_proxy_size).to eq 20
+ expect(stat.storage_size).to eq 20
+ end
+ end
+
+ context 'when just a stat is passed' do
+ let(:statistics) { [:dependency_proxy_size] }
+
+ it 'calls the statistics update service with just that stat' do
+ expect(Groups::UpdateStatisticsService)
+ .to receive(:new)
+ .with(anything, statistics: [:dependency_proxy_size])
+ .twice.and_call_original
+
+ perform
+ end
+ end
+
+ context 'when a statistics update fails' do
+ before do
+ error_response = instance_double(ServiceResponse, message: 'an error', error?: true)
+
+ allow_next_instance_of(Groups::UpdateStatisticsService) do |instance|
+ allow(instance).to receive(:execute).and_return(error_response)
+ end
+ end
+
+ it 'logs an error' do
+ expect_next_instance_of(Gitlab::BackgroundMigration::Logger) do |instance|
+ expect(instance).to receive(:error).twice
+ end
+
+ perform
+ end
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/populate_topics_non_private_projects_count_spec.rb b/spec/lib/gitlab/background_migration/populate_topics_non_private_projects_count_spec.rb
new file mode 100644
index 00000000000..e72e3392210
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/populate_topics_non_private_projects_count_spec.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::PopulateTopicsNonPrivateProjectsCount, schema: 20220125122640 do
+ it 'correctly populates the non private projects counters' do
+ namespaces = table(:namespaces)
+ projects = table(:projects)
+ topics = table(:topics)
+ project_topics = table(:project_topics)
+
+ group = namespaces.create!(name: 'group', path: 'group')
+ project_public = projects.create!(namespace_id: group.id, visibility_level: Gitlab::VisibilityLevel::PUBLIC)
+ project_internal = projects.create!(namespace_id: group.id, visibility_level: Gitlab::VisibilityLevel::INTERNAL)
+ project_private = projects.create!(namespace_id: group.id, visibility_level: Gitlab::VisibilityLevel::PRIVATE)
+ topic_1 = topics.create!(name: 'Topic1')
+ topic_2 = topics.create!(name: 'Topic2')
+ topic_3 = topics.create!(name: 'Topic3')
+ topic_4 = topics.create!(name: 'Topic4')
+ topic_5 = topics.create!(name: 'Topic5')
+ topic_6 = topics.create!(name: 'Topic6')
+ topic_7 = topics.create!(name: 'Topic7')
+ topic_8 = topics.create!(name: 'Topic8')
+
+ project_topics.create!(topic_id: topic_1.id, project_id: project_public.id)
+ project_topics.create!(topic_id: topic_2.id, project_id: project_internal.id)
+ project_topics.create!(topic_id: topic_3.id, project_id: project_private.id)
+ project_topics.create!(topic_id: topic_4.id, project_id: project_public.id)
+ project_topics.create!(topic_id: topic_4.id, project_id: project_internal.id)
+ project_topics.create!(topic_id: topic_5.id, project_id: project_public.id)
+ project_topics.create!(topic_id: topic_5.id, project_id: project_private.id)
+ project_topics.create!(topic_id: topic_6.id, project_id: project_internal.id)
+ project_topics.create!(topic_id: topic_6.id, project_id: project_private.id)
+ project_topics.create!(topic_id: topic_7.id, project_id: project_public.id)
+ project_topics.create!(topic_id: topic_7.id, project_id: project_internal.id)
+ project_topics.create!(topic_id: topic_7.id, project_id: project_private.id)
+ project_topics.create!(topic_id: topic_8.id, project_id: project_public.id)
+
+ subject.perform(topic_1.id, topic_7.id)
+
+ expect(topic_1.reload.non_private_projects_count).to eq(1)
+ expect(topic_2.reload.non_private_projects_count).to eq(1)
+ expect(topic_3.reload.non_private_projects_count).to eq(0)
+ expect(topic_4.reload.non_private_projects_count).to eq(2)
+ expect(topic_5.reload.non_private_projects_count).to eq(1)
+ expect(topic_6.reload.non_private_projects_count).to eq(1)
+ expect(topic_7.reload.non_private_projects_count).to eq(2)
+ expect(topic_8.reload.non_private_projects_count).to eq(0)
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/populate_vulnerability_reads_spec.rb b/spec/lib/gitlab/background_migration/populate_vulnerability_reads_spec.rb
new file mode 100644
index 00000000000..c0470f26d9e
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/populate_vulnerability_reads_spec.rb
@@ -0,0 +1,93 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::PopulateVulnerabilityReads, :migration, schema: 20220326161803 do
+ let(:vulnerabilities) { table(:vulnerabilities) }
+ let(:vulnerability_reads) { table(:vulnerability_reads) }
+ let(:vulnerabilities_findings) { table(:vulnerability_occurrences) }
+ let(:vulnerability_issue_links) { table(:vulnerability_issue_links) }
+ let(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') }
+ let(:user) { table(:users).create!(email: 'author@example.com', username: 'author', projects_limit: 10) }
+ let(:project) { table(:projects).create!(namespace_id: namespace.id) }
+ let(:scanner) { table(:vulnerability_scanners).create!(project_id: project.id, external_id: 'test 1', name: 'test scanner 1') }
+ let(:sub_batch_size) { 1000 }
+
+ before do
+ vulnerabilities_findings.connection.execute 'ALTER TABLE vulnerability_occurrences DISABLE TRIGGER "trigger_insert_or_update_vulnerability_reads_from_occurrences"'
+ vulnerabilities.connection.execute 'ALTER TABLE vulnerabilities DISABLE TRIGGER "trigger_update_vulnerability_reads_on_vulnerability_update"'
+ vulnerability_issue_links.connection.execute 'ALTER TABLE vulnerability_issue_links DISABLE TRIGGER "trigger_update_has_issues_on_vulnerability_issue_links_update"'
+
+ 10.times.each do |x|
+ vulnerability = create_vulnerability!(
+ project_id: project.id,
+ report_type: 7,
+ author_id: user.id
+ )
+ identifier = table(:vulnerability_identifiers).create!(
+ project_id: project.id,
+ external_type: 'uuid-v5',
+ external_id: 'uuid-v5',
+ fingerprint: Digest::SHA1.hexdigest(vulnerability.id.to_s),
+ name: 'Identifier for UUIDv5')
+
+ create_finding!(
+ vulnerability_id: vulnerability.id,
+ project_id: project.id,
+ scanner_id: scanner.id,
+ primary_identifier_id: identifier.id
+ )
+ end
+ end
+
+ it 'creates vulnerability_reads for the given records' do
+ described_class.new.perform(vulnerabilities.first.id, vulnerabilities.last.id, sub_batch_size)
+
+ expect(vulnerability_reads.count).to eq(10)
+ end
+
+ it 'does not create new records when records already exists' do
+ described_class.new.perform(vulnerabilities.first.id, vulnerabilities.last.id, sub_batch_size)
+ described_class.new.perform(vulnerabilities.first.id, vulnerabilities.last.id, sub_batch_size)
+
+ expect(vulnerability_reads.count).to eq(10)
+ end
+
+ private
+
+ def create_vulnerability!(project_id:, author_id:, title: 'test', severity: 7, confidence: 7, report_type: 0)
+ vulnerabilities.create!(
+ project_id: project_id,
+ author_id: author_id,
+ title: title,
+ severity: severity,
+ confidence: confidence,
+ report_type: report_type
+ )
+ end
+
+ # rubocop:disable Metrics/ParameterLists
+ def create_finding!(
+ project_id:, scanner_id:, primary_identifier_id:, vulnerability_id: nil,
+ name: "test", severity: 7, confidence: 7, report_type: 0,
+ project_fingerprint: '123qweasdzxc', location: { "image" => "alpine:3.4" }, location_fingerprint: 'test',
+ metadata_version: 'test', raw_metadata: 'test', uuid: SecureRandom.uuid)
+ vulnerabilities_findings.create!(
+ vulnerability_id: vulnerability_id,
+ project_id: project_id,
+ name: name,
+ severity: severity,
+ confidence: confidence,
+ report_type: report_type,
+ project_fingerprint: project_fingerprint,
+ scanner_id: scanner_id,
+ primary_identifier_id: primary_identifier_id,
+ location: location,
+ location_fingerprint: location_fingerprint,
+ metadata_version: metadata_version,
+ raw_metadata: raw_metadata,
+ uuid: uuid
+ )
+ end
+ # rubocop:enable Metrics/ParameterLists
+end
diff --git a/spec/lib/gitlab/background_migration/recalculate_vulnerabilities_occurrences_uuid_spec.rb b/spec/lib/gitlab/background_migration/recalculate_vulnerabilities_occurrences_uuid_spec.rb
new file mode 100644
index 00000000000..543dd204f89
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/recalculate_vulnerabilities_occurrences_uuid_spec.rb
@@ -0,0 +1,530 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+def create_background_migration_job(ids, status)
+ proper_status = case status
+ when :pending
+ Gitlab::Database::BackgroundMigrationJob.statuses['pending']
+ when :succeeded
+ Gitlab::Database::BackgroundMigrationJob.statuses['succeeded']
+ else
+ raise ArgumentError
+ end
+
+ background_migration_jobs.create!(
+ class_name: 'RecalculateVulnerabilitiesOccurrencesUuid',
+ arguments: Array(ids),
+ status: proper_status,
+ created_at: Time.now.utc
+ )
+end
+
+RSpec.describe Gitlab::BackgroundMigration::RecalculateVulnerabilitiesOccurrencesUuid, :suppress_gitlab_schemas_validate_connection, schema: 20211202041233 do
+ let(:background_migration_jobs) { table(:background_migration_jobs) }
+ let(:pending_jobs) { background_migration_jobs.where(status: Gitlab::Database::BackgroundMigrationJob.statuses['pending']) }
+ let(:succeeded_jobs) { background_migration_jobs.where(status: Gitlab::Database::BackgroundMigrationJob.statuses['succeeded']) }
+ let(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') }
+ let(:users) { table(:users) }
+ let(:user) { create_user! }
+ let(:project) { table(:projects).create!(id: 123, namespace_id: namespace.id) }
+ let(:scanners) { table(:vulnerability_scanners) }
+ let(:scanner) { scanners.create!(project_id: project.id, external_id: 'test 1', name: 'test scanner 1') }
+ let(:scanner2) { scanners.create!(project_id: project.id, external_id: 'test 2', name: 'test scanner 2') }
+ let(:vulnerabilities) { table(:vulnerabilities) }
+ let(:vulnerability_findings) { table(:vulnerability_occurrences) }
+ let(:vulnerability_finding_pipelines) { table(:vulnerability_occurrence_pipelines) }
+ let(:vulnerability_finding_signatures) { table(:vulnerability_finding_signatures) }
+ let(:vulnerability_identifiers) { table(:vulnerability_identifiers) }
+
+ let(:identifier_1) { 'identifier-1' }
+ let!(:vulnerability_identifier) do
+ vulnerability_identifiers.create!(
+ project_id: project.id,
+ external_type: identifier_1,
+ external_id: identifier_1,
+ fingerprint: Gitlab::Database::ShaAttribute.serialize('ff9ef548a6e30a0462795d916f3f00d1e2b082ca'),
+ name: 'Identifier 1')
+ end
+
+ let(:identifier_2) { 'identifier-2' }
+ let!(:vulnerability_identfier2) do
+ vulnerability_identifiers.create!(
+ project_id: project.id,
+ external_type: identifier_2,
+ external_id: identifier_2,
+ fingerprint: Gitlab::Database::ShaAttribute.serialize('4299e8ddd819f9bde9cfacf45716724c17b5ddf7'),
+ name: 'Identifier 2')
+ end
+
+ let(:identifier_3) { 'identifier-3' }
+ let!(:vulnerability_identifier3) do
+ vulnerability_identifiers.create!(
+ project_id: project.id,
+ external_type: identifier_3,
+ external_id: identifier_3,
+ fingerprint: Gitlab::Database::ShaAttribute.serialize('8e91632f9c6671e951834a723ee221c44cc0d844'),
+ name: 'Identifier 3')
+ end
+
+ let(:known_uuid_v4) { "b3cc2518-5446-4dea-871c-89d5e999c1ac" }
+ let(:known_uuid_v5) { "05377088-dc26-5161-920e-52a7159fdaa1" }
+ let(:desired_uuid_v5) { "f3e9a23f-9181-54bf-a5ab-c5bc7a9b881a" }
+
+ subject { described_class.new.perform(start_id, end_id) }
+
+ context "when finding has a UUIDv4" do
+ before do
+ @uuid_v4 = create_finding!(
+ vulnerability_id: nil,
+ project_id: project.id,
+ scanner_id: scanner2.id,
+ primary_identifier_id: vulnerability_identfier2.id,
+ report_type: 0, # "sast"
+ location_fingerprint: Gitlab::Database::ShaAttribute.serialize("fa18f432f1d56675f4098d318739c3cd5b14eb3e"),
+ uuid: known_uuid_v4
+ )
+ end
+
+ let(:start_id) { @uuid_v4.id }
+ let(:end_id) { @uuid_v4.id }
+
+ it "replaces it with UUIDv5" do
+ expect(vulnerability_findings.pluck(:uuid)).to match_array([known_uuid_v4])
+
+ subject
+
+ expect(vulnerability_findings.pluck(:uuid)).to match_array([desired_uuid_v5])
+ end
+
+ it 'logs recalculation' do
+ expect_next_instance_of(Gitlab::BackgroundMigration::Logger) do |instance|
+ expect(instance).to receive(:info).twice
+ end
+
+ subject
+ end
+ end
+
+ context "when finding has a UUIDv5" do
+ before do
+ @uuid_v5 = create_finding!(
+ vulnerability_id: nil,
+ project_id: project.id,
+ scanner_id: scanner.id,
+ primary_identifier_id: vulnerability_identifier.id,
+ report_type: 0, # "sast"
+ location_fingerprint: Gitlab::Database::ShaAttribute.serialize("838574be0210968bf6b9f569df9c2576242cbf0a"),
+ uuid: known_uuid_v5
+ )
+ end
+
+ let(:start_id) { @uuid_v5.id }
+ let(:end_id) { @uuid_v5.id }
+
+ it "stays the same" do
+ expect(vulnerability_findings.pluck(:uuid)).to match_array([known_uuid_v5])
+
+ subject
+
+ expect(vulnerability_findings.pluck(:uuid)).to match_array([known_uuid_v5])
+ end
+ end
+
+ context 'if a duplicate UUID would be generated' do # rubocop: disable RSpec/MultipleMemoizedHelpers
+ let(:v1) do
+ create_vulnerability!(
+ project_id: project.id,
+ author_id: user.id
+ )
+ end
+
+ let!(:finding_with_incorrect_uuid) do
+ create_finding!(
+ vulnerability_id: v1.id,
+ project_id: project.id,
+ scanner_id: scanner.id,
+ primary_identifier_id: vulnerability_identifier.id,
+ report_type: 0, # "sast"
+ location_fingerprint: Gitlab::Database::ShaAttribute.serialize('ca41a2544e941a007a73a666cb0592b255316ab8'), # sha1('youshouldntusethis')
+ uuid: 'bd95c085-71aa-51d7-9bb6-08ae669c262e'
+ )
+ end
+
+ let(:v2) do
+ create_vulnerability!(
+ project_id: project.id,
+ author_id: user.id
+ )
+ end
+
+ let!(:finding_with_correct_uuid) do
+ create_finding!(
+ vulnerability_id: v2.id,
+ project_id: project.id,
+ primary_identifier_id: vulnerability_identifier.id,
+ scanner_id: scanner2.id,
+ report_type: 0, # "sast"
+ location_fingerprint: Gitlab::Database::ShaAttribute.serialize('ca41a2544e941a007a73a666cb0592b255316ab8'), # sha1('youshouldntusethis')
+ uuid: '91984483-5efe-5215-b471-d524ac5792b1'
+ )
+ end
+
+ let(:v3) do
+ create_vulnerability!(
+ project_id: project.id,
+ author_id: user.id
+ )
+ end
+
+ let!(:finding_with_incorrect_uuid2) do
+ create_finding!(
+ vulnerability_id: v3.id,
+ project_id: project.id,
+ scanner_id: scanner.id,
+ primary_identifier_id: vulnerability_identfier2.id,
+ report_type: 0, # "sast"
+ location_fingerprint: Gitlab::Database::ShaAttribute.serialize('ca41a2544e941a007a73a666cb0592b255316ab8'), # sha1('youshouldntusethis')
+ uuid: '00000000-1111-2222-3333-444444444444'
+ )
+ end
+
+ let(:v4) do
+ create_vulnerability!(
+ project_id: project.id,
+ author_id: user.id
+ )
+ end
+
+ let!(:finding_with_correct_uuid2) do
+ create_finding!(
+ vulnerability_id: v4.id,
+ project_id: project.id,
+ scanner_id: scanner2.id,
+ primary_identifier_id: vulnerability_identfier2.id,
+ report_type: 0, # "sast"
+ location_fingerprint: Gitlab::Database::ShaAttribute.serialize('ca41a2544e941a007a73a666cb0592b255316ab8'), # sha1('youshouldntusethis')
+ uuid: '1edd751e-ef9a-5391-94db-a832c8635bfc'
+ )
+ end
+
+ let!(:finding_with_incorrect_uuid3) do
+ create_finding!(
+ vulnerability_id: nil,
+ project_id: project.id,
+ scanner_id: scanner.id,
+ primary_identifier_id: vulnerability_identifier3.id,
+ report_type: 0, # "sast"
+ location_fingerprint: Gitlab::Database::ShaAttribute.serialize('ca41a2544e941a007a73a666cb0592b255316ab8'), # sha1('youshouldntusethis')
+ uuid: '22222222-3333-4444-5555-666666666666'
+ )
+ end
+
+ let!(:duplicate_not_in_the_same_batch) do
+ create_finding!(
+ id: 99999,
+ vulnerability_id: nil,
+ project_id: project.id,
+ scanner_id: scanner2.id,
+ primary_identifier_id: vulnerability_identifier3.id,
+ report_type: 0, # "sast"
+ location_fingerprint: Gitlab::Database::ShaAttribute.serialize('ca41a2544e941a007a73a666cb0592b255316ab8'), # sha1('youshouldntusethis')
+ uuid: '4564f9d5-3c6b-5cc3-af8c-7c25285362a7'
+ )
+ end
+
+ let(:start_id) { finding_with_incorrect_uuid.id }
+ let(:end_id) { finding_with_incorrect_uuid3.id }
+
+ before do
+ 4.times do
+ create_finding_pipeline!(project_id: project.id, finding_id: finding_with_incorrect_uuid.id)
+ create_finding_pipeline!(project_id: project.id, finding_id: finding_with_correct_uuid.id)
+ create_finding_pipeline!(project_id: project.id, finding_id: finding_with_incorrect_uuid2.id)
+ create_finding_pipeline!(project_id: project.id, finding_id: finding_with_correct_uuid2.id)
+ end
+ end
+
+ it 'drops duplicates and related records', :aggregate_failures do
+ expect(vulnerability_findings.pluck(:id)).to match_array(
+ [
+ finding_with_correct_uuid.id,
+ finding_with_incorrect_uuid.id,
+ finding_with_correct_uuid2.id,
+ finding_with_incorrect_uuid2.id,
+ finding_with_incorrect_uuid3.id,
+ duplicate_not_in_the_same_batch.id
+ ])
+
+ expect { subject }.to change(vulnerability_finding_pipelines, :count).from(16).to(8)
+ .and change(vulnerability_findings, :count).from(6).to(3)
+ .and change(vulnerabilities, :count).from(4).to(2)
+
+ expect(vulnerability_findings.pluck(:id)).to match_array([finding_with_incorrect_uuid.id, finding_with_incorrect_uuid2.id, finding_with_incorrect_uuid3.id])
+ end
+
+ context 'if there are conflicting UUID values within the batch' do # rubocop: disable RSpec/MultipleMemoizedHelpers
+ let(:end_id) { finding_with_broken_data_integrity.id }
+ let(:vulnerability_5) { create_vulnerability!(project_id: project.id, author_id: user.id) }
+ let(:different_project) { table(:projects).create!(namespace_id: namespace.id) }
+ let!(:identifier_with_broken_data_integrity) do
+ vulnerability_identifiers.create!(
+ project_id: different_project.id,
+ external_type: identifier_2,
+ external_id: identifier_2,
+ fingerprint: Gitlab::Database::ShaAttribute.serialize('4299e8ddd819f9bde9cfacf45716724c17b5ddf7'),
+ name: 'Identifier 2')
+ end
+
+ let(:finding_with_broken_data_integrity) do
+ create_finding!(
+ vulnerability_id: vulnerability_5,
+ project_id: project.id,
+ scanner_id: scanner.id,
+ primary_identifier_id: identifier_with_broken_data_integrity.id,
+ report_type: 0, # "sast"
+ location_fingerprint: Gitlab::Database::ShaAttribute.serialize('ca41a2544e941a007a73a666cb0592b255316ab8'), # sha1('youshouldntusethis')
+ uuid: SecureRandom.uuid
+ )
+ end
+
+ it 'deletes the conflicting record' do
+ expect { subject }.to change { vulnerability_findings.find_by_id(finding_with_broken_data_integrity.id) }.to(nil)
+ end
+ end
+
+ context 'if a conflicting UUID is found during the migration' do # rubocop:disable RSpec/MultipleMemoizedHelpers
+ let(:finding_class) { Gitlab::BackgroundMigration::RecalculateVulnerabilitiesOccurrencesUuid::VulnerabilitiesFinding }
+ let(:uuid) { '4564f9d5-3c6b-5cc3-af8c-7c25285362a7' }
+
+ before do
+ exception = ActiveRecord::RecordNotUnique.new("(uuid)=(#{uuid})")
+
+ call_count = 0
+ allow(::Gitlab::Database::BulkUpdate).to receive(:execute) do
+ call_count += 1
+ call_count.eql?(1) ? raise(exception) : {}
+ end
+
+ allow(finding_class).to receive(:find_by).with(uuid: uuid).and_return(duplicate_not_in_the_same_batch)
+ end
+
+ it 'retries the recalculation' do
+ subject
+
+ expect(Gitlab::BackgroundMigration::RecalculateVulnerabilitiesOccurrencesUuid::VulnerabilitiesFinding)
+ .to have_received(:find_by).with(uuid: uuid).once
+ end
+
+ it 'logs the conflict' do
+ expect_next_instance_of(Gitlab::BackgroundMigration::Logger) do |instance|
+ expect(instance).to receive(:info).exactly(6).times
+ end
+
+ subject
+ end
+
+ it 'marks the job as done' do
+ create_background_migration_job([start_id, end_id], :pending)
+
+ subject
+
+ expect(pending_jobs.count).to eq(0)
+ expect(succeeded_jobs.count).to eq(1)
+ end
+ end
+
+ it 'logs an exception if a different uniquness problem was found' do
+ exception = ActiveRecord::RecordNotUnique.new("Totally not an UUID uniqueness problem")
+ allow(::Gitlab::Database::BulkUpdate).to receive(:execute).and_raise(exception)
+ allow(Gitlab::ErrorTracking).to receive(:track_and_raise_exception)
+
+ subject
+
+ expect(Gitlab::ErrorTracking).to have_received(:track_and_raise_exception).with(exception).once
+ end
+
+ it 'logs a duplicate found message' do
+ expect_next_instance_of(Gitlab::BackgroundMigration::Logger) do |instance|
+ expect(instance).to receive(:info).exactly(3).times
+ end
+
+ subject
+ end
+ end
+
+ context 'when finding has a signature' do
+ before do
+ @f1 = create_finding!(
+ vulnerability_id: nil,
+ project_id: project.id,
+ scanner_id: scanner.id,
+ primary_identifier_id: vulnerability_identifier.id,
+ report_type: 0, # "sast"
+ location_fingerprint: Gitlab::Database::ShaAttribute.serialize('ca41a2544e941a007a73a666cb0592b255316ab8'), # sha1('youshouldntusethis')
+ uuid: 'd15d774d-e4b1-5a1b-929b-19f2a53e35ec'
+ )
+
+ vulnerability_finding_signatures.create!(
+ finding_id: @f1.id,
+ algorithm_type: 2, # location
+ signature_sha: Gitlab::Database::ShaAttribute.serialize('57d4e05205f6462a73f039a5b2751aa1ab344e6e') # sha1('youshouldusethis')
+ )
+
+ vulnerability_finding_signatures.create!(
+ finding_id: @f1.id,
+ algorithm_type: 1, # hash
+ signature_sha: Gitlab::Database::ShaAttribute.serialize('c554d8d8df1a7a14319eafdaae24af421bf5b587') # sha1('andnotthis')
+ )
+
+ @f2 = create_finding!(
+ vulnerability_id: nil,
+ project_id: project.id,
+ scanner_id: scanner.id,
+ primary_identifier_id: vulnerability_identfier2.id,
+ report_type: 0, # "sast"
+ location_fingerprint: Gitlab::Database::ShaAttribute.serialize('ca41a2544e941a007a73a666cb0592b255316ab8'), # sha1('youshouldntusethis')
+ uuid: '4be029b5-75e5-5ac0-81a2-50ab41726135'
+ )
+
+ vulnerability_finding_signatures.create!(
+ finding_id: @f2.id,
+ algorithm_type: 2, # location
+ signature_sha: Gitlab::Database::ShaAttribute.serialize('57d4e05205f6462a73f039a5b2751aa1ab344e6e') # sha1('youshouldusethis')
+ )
+
+ vulnerability_finding_signatures.create!(
+ finding_id: @f2.id,
+ algorithm_type: 1, # hash
+ signature_sha: Gitlab::Database::ShaAttribute.serialize('c554d8d8df1a7a14319eafdaae24af421bf5b587') # sha1('andnotthis')
+ )
+ end
+
+ let(:start_id) { @f1.id }
+ let(:end_id) { @f2.id }
+
+ let(:uuids_before) { [@f1.uuid, @f2.uuid] }
+ let(:uuids_after) { %w[d3b60ddd-d312-5606-b4d3-ad058eebeacb 349d9bec-c677-5530-a8ac-5e58889c3b1a] }
+
+ it 'is recalculated using signature' do
+ expect(vulnerability_findings.pluck(:uuid)).to match_array(uuids_before)
+
+ subject
+
+ expect(vulnerability_findings.pluck(:uuid)).to match_array(uuids_after)
+ end
+ end
+
+ context 'if all records are removed before the job ran' do
+ let(:start_id) { 1 }
+ let(:end_id) { 9 }
+
+ before do
+ create_background_migration_job([start_id, end_id], :pending)
+ end
+
+ it 'does not error out' do
+ expect { subject }.not_to raise_error
+ end
+
+ it 'marks the job as done' do
+ subject
+
+ expect(pending_jobs.count).to eq(0)
+ expect(succeeded_jobs.count).to eq(1)
+ end
+ end
+
+ context 'when recalculation fails' do
+ before do
+ @uuid_v4 = create_finding!(
+ vulnerability_id: nil,
+ project_id: project.id,
+ scanner_id: scanner2.id,
+ primary_identifier_id: vulnerability_identfier2.id,
+ report_type: 0, # "sast"
+ location_fingerprint: Gitlab::Database::ShaAttribute.serialize("fa18f432f1d56675f4098d318739c3cd5b14eb3e"),
+ uuid: known_uuid_v4
+ )
+
+ allow(Gitlab::ErrorTracking).to receive(:track_and_raise_exception)
+ allow(::Gitlab::Database::BulkUpdate).to receive(:execute).and_raise(expected_error)
+ end
+
+ let(:start_id) { @uuid_v4.id }
+ let(:end_id) { @uuid_v4.id }
+ let(:expected_error) { RuntimeError.new }
+
+ it 'captures the errors and does not crash entirely' do
+ expect { subject }.not_to raise_error
+
+ allow(Gitlab::ErrorTracking).to receive(:track_and_raise_exception)
+ expect(Gitlab::ErrorTracking).to have_received(:track_and_raise_exception).with(expected_error).once
+ end
+
+ it_behaves_like 'marks background migration job records' do
+ let(:arguments) { [1, 4] }
+ subject { described_class.new }
+ end
+ end
+
+ it_behaves_like 'marks background migration job records' do
+ let(:arguments) { [1, 4] }
+ subject { described_class.new }
+ end
+
+ private
+
+ def create_vulnerability!(project_id:, author_id:, title: 'test', severity: 7, confidence: 7, report_type: 0)
+ vulnerabilities.create!(
+ project_id: project_id,
+ author_id: author_id,
+ title: title,
+ severity: severity,
+ confidence: confidence,
+ report_type: report_type
+ )
+ end
+
+ # rubocop:disable Metrics/ParameterLists
+ def create_finding!(
+ vulnerability_id:, project_id:, scanner_id:, primary_identifier_id:, id: nil,
+ name: "test", severity: 7, confidence: 7, report_type: 0,
+ project_fingerprint: '123qweasdzxc', location_fingerprint: 'test',
+ metadata_version: 'test', raw_metadata: 'test', uuid: SecureRandom.uuid)
+ vulnerability_findings.create!({
+ id: id,
+ vulnerability_id: vulnerability_id,
+ project_id: project_id,
+ name: name,
+ severity: severity,
+ confidence: confidence,
+ report_type: report_type,
+ project_fingerprint: project_fingerprint,
+ scanner_id: scanner_id,
+ primary_identifier_id: primary_identifier_id,
+ location_fingerprint: location_fingerprint,
+ metadata_version: metadata_version,
+ raw_metadata: raw_metadata,
+ uuid: uuid
+ }.compact
+ )
+ end
+ # rubocop:enable Metrics/ParameterLists
+
+ def create_user!(name: "Example User", email: "user@example.com", user_type: nil, created_at: Time.zone.now, confirmed_at: Time.zone.now)
+ users.create!(
+ name: name,
+ email: email,
+ username: name,
+ projects_limit: 0,
+ user_type: user_type,
+ confirmed_at: confirmed_at
+ )
+ end
+
+ def create_finding_pipeline!(project_id:, finding_id:)
+ pipeline = table(:ci_pipelines).create!(project_id: project_id)
+ vulnerability_finding_pipelines.create!(pipeline_id: pipeline.id, occurrence_id: finding_id)
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/remove_all_trace_expiration_dates_spec.rb b/spec/lib/gitlab/background_migration/remove_all_trace_expiration_dates_spec.rb
new file mode 100644
index 00000000000..eabc012f98b
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/remove_all_trace_expiration_dates_spec.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::RemoveAllTraceExpirationDates, :migration,
+ :suppress_gitlab_schemas_validate_connection, schema: 20220131000001 do
+ subject(:perform) { migration.perform(1, 99) }
+
+ let(:migration) { described_class.new }
+
+ let(:trace_in_range) { create_trace!(id: 10, created_at: Date.new(2020, 06, 20), expire_at: Date.new(2021, 01, 22)) }
+ let(:trace_outside_range) { create_trace!(id: 40, created_at: Date.new(2020, 06, 22), expire_at: Date.new(2021, 01, 22)) }
+ let(:trace_without_expiry) { create_trace!(id: 30, created_at: Date.new(2020, 06, 21), expire_at: nil) }
+ let(:archive_in_range) { create_archive!(id: 10, created_at: Date.new(2020, 06, 20), expire_at: Date.new(2021, 01, 22)) }
+ let(:trace_outside_id_range) { create_trace!(id: 100, created_at: Date.new(2020, 06, 20), expire_at: Date.new(2021, 01, 22)) }
+
+ before do
+ table(:namespaces).create!(id: 1, name: 'the-namespace', path: 'the-path')
+ table(:projects).create!(id: 1, name: 'the-project', namespace_id: 1)
+ table(:ci_builds).create!(id: 1, allow_failure: false)
+ end
+
+ context 'for self-hosted instances' do
+ it 'sets expire_at for artifacts in range to nil' do
+ expect { perform }.not_to change { trace_in_range.reload.expire_at }
+ end
+
+ it 'does not change expire_at timestamps that are not set to midnight' do
+ expect { perform }.not_to change { trace_outside_range.reload.expire_at }
+ end
+
+ it 'does not change expire_at timestamps that are set to midnight on a day other than the 22nd' do
+ expect { perform }.not_to change { trace_without_expiry.reload.expire_at }
+ end
+
+ it 'does not touch artifacts outside id range' do
+ expect { perform }.not_to change { archive_in_range.reload.expire_at }
+ end
+
+ it 'does not touch artifacts outside date range' do
+ expect { perform }.not_to change { trace_outside_id_range.reload.expire_at }
+ end
+ end
+
+ private
+
+ def create_trace!(**args)
+ table(:ci_job_artifacts).create!(**args, project_id: 1, job_id: 1, file_type: 3)
+ end
+
+ def create_archive!(**args)
+ table(:ci_job_artifacts).create!(**args, project_id: 1, job_id: 1, file_type: 1)
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/remove_vulnerability_finding_links_spec.rb b/spec/lib/gitlab/background_migration/remove_vulnerability_finding_links_spec.rb
index da14381aae2..32134b99e37 100644
--- a/spec/lib/gitlab/background_migration/remove_vulnerability_finding_links_spec.rb
+++ b/spec/lib/gitlab/background_migration/remove_vulnerability_finding_links_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::BackgroundMigration::RemoveVulnerabilityFindingLinks, :migration, schema: 20220314184009 do
+RSpec.describe Gitlab::BackgroundMigration::RemoveVulnerabilityFindingLinks, :migration, schema: 20211202041233 do
let(:vulnerability_findings) { table(:vulnerability_occurrences) }
let(:finding_links) { table(:vulnerability_finding_links) }
diff --git a/spec/lib/gitlab/background_migration/update_timelogs_null_spent_at_spec.rb b/spec/lib/gitlab/background_migration/update_timelogs_null_spent_at_spec.rb
new file mode 100644
index 00000000000..908f11aabc3
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/update_timelogs_null_spent_at_spec.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::UpdateTimelogsNullSpentAt, schema: 20211215090620 do
+ let!(:previous_time) { 10.days.ago }
+ let!(:namespace) { table(:namespaces).create!(name: 'namespace', path: 'namespace') }
+ let!(:project) { table(:projects).create!(namespace_id: namespace.id) }
+ let!(:issue) { table(:issues).create!(project_id: project.id) }
+ let!(:merge_request) { table(:merge_requests).create!(target_project_id: project.id, source_branch: 'master', target_branch: 'feature') }
+ let!(:timelog1) { create_timelog!(issue_id: issue.id) }
+ let!(:timelog2) { create_timelog!(merge_request_id: merge_request.id) }
+ let!(:timelog3) { create_timelog!(issue_id: issue.id, spent_at: previous_time) }
+ let!(:timelog4) { create_timelog!(merge_request_id: merge_request.id, spent_at: previous_time) }
+
+ subject(:background_migration) { described_class.new }
+
+ before do
+ table(:timelogs).where.not(id: [timelog3.id, timelog4.id]).update_all(spent_at: nil)
+ end
+
+ describe '#perform' do
+ it 'sets correct spent_at' do
+ background_migration.perform(timelog1.id, timelog4.id)
+
+ expect(timelog1.reload.spent_at).to be_like_time(timelog1.created_at)
+ expect(timelog2.reload.spent_at).to be_like_time(timelog2.created_at)
+ expect(timelog3.reload.spent_at).to be_like_time(previous_time)
+ expect(timelog4.reload.spent_at).to be_like_time(previous_time)
+ expect(timelog3.reload.spent_at).not_to be_like_time(timelog3.created_at)
+ expect(timelog4.reload.spent_at).not_to be_like_time(timelog4.created_at)
+ end
+ end
+
+ private
+
+ def create_timelog!(**args)
+ table(:timelogs).create!(**args, time_spent: 1)
+ end
+end
diff --git a/spec/migrations/20211203091642_add_index_to_projects_on_marked_for_deletion_at_spec.rb b/spec/migrations/20211203091642_add_index_to_projects_on_marked_for_deletion_at_spec.rb
new file mode 100644
index 00000000000..7be54bc13cc
--- /dev/null
+++ b/spec/migrations/20211203091642_add_index_to_projects_on_marked_for_deletion_at_spec.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe AddIndexToProjectsOnMarkedForDeletionAt, feature_category: :projects do
+ it 'correctly migrates up and down' do
+ reversible_migration do |migration|
+ migration.before -> {
+ expect(ActiveRecord::Base.connection.indexes('projects').map(&:name)).not_to include('index_projects_not_aimed_for_deletion')
+ }
+
+ migration.after -> {
+ expect(ActiveRecord::Base.connection.indexes('projects').map(&:name)).to include('index_projects_not_aimed_for_deletion')
+ }
+ end
+ end
+end
diff --git a/spec/migrations/20211207125331_remove_jobs_for_recalculate_vulnerabilities_occurrences_uuid_spec.rb b/spec/migrations/20211207125331_remove_jobs_for_recalculate_vulnerabilities_occurrences_uuid_spec.rb
new file mode 100644
index 00000000000..9fa2ac2313a
--- /dev/null
+++ b/spec/migrations/20211207125331_remove_jobs_for_recalculate_vulnerabilities_occurrences_uuid_spec.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+require 'spec_helper'
+require_migration!
+
+def create_background_migration_jobs(ids, status, created_at)
+ proper_status = case status
+ when :pending
+ Gitlab::Database::BackgroundMigrationJob.statuses['pending']
+ when :succeeded
+ Gitlab::Database::BackgroundMigrationJob.statuses['succeeded']
+ else
+ raise ArgumentError
+ end
+
+ background_migration_jobs.create!(
+ class_name: 'RecalculateVulnerabilitiesOccurrencesUuid',
+ arguments: Array(ids),
+ status: proper_status,
+ created_at: created_at
+ )
+end
+
+RSpec.describe RemoveJobsForRecalculateVulnerabilitiesOccurrencesUuid, :migration,
+ feature_category: :vulnerability_management do
+ let!(:background_migration_jobs) { table(:background_migration_jobs) }
+
+ context 'when RecalculateVulnerabilitiesOccurrencesUuid jobs are present' do
+ before do
+ create_background_migration_jobs([1, 2, 3], :succeeded, DateTime.new(2021, 5, 5, 0, 2))
+ create_background_migration_jobs([4, 5, 6], :pending, DateTime.new(2021, 5, 5, 0, 4))
+
+ create_background_migration_jobs([1, 2, 3], :succeeded, DateTime.new(2021, 8, 18, 0, 0))
+ create_background_migration_jobs([4, 5, 6], :pending, DateTime.new(2021, 8, 18, 0, 2))
+ create_background_migration_jobs([7, 8, 9], :pending, DateTime.new(2021, 8, 18, 0, 4))
+ end
+
+ it 'removes all jobs' do
+ expect(background_migration_jobs.count).to eq(5)
+
+ migrate!
+
+ expect(background_migration_jobs.count).to eq(0)
+ end
+ end
+end
diff --git a/spec/migrations/20211207135331_schedule_recalculate_uuid_on_vulnerabilities_occurrences4_spec.rb b/spec/migrations/20211207135331_schedule_recalculate_uuid_on_vulnerabilities_occurrences4_spec.rb
new file mode 100644
index 00000000000..c7401c4790d
--- /dev/null
+++ b/spec/migrations/20211207135331_schedule_recalculate_uuid_on_vulnerabilities_occurrences4_spec.rb
@@ -0,0 +1,148 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe ScheduleRecalculateUuidOnVulnerabilitiesOccurrences4, feature_category: :vulnerability_management do
+ let(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') }
+ let(:users) { table(:users) }
+ let(:user) { create_user! }
+ let(:project) { table(:projects).create!(id: 123, namespace_id: namespace.id) }
+ let(:scanners) { table(:vulnerability_scanners) }
+ let(:scanner) { scanners.create!(project_id: project.id, external_id: 'test 1', name: 'test scanner 1') }
+ let(:different_scanner) { scanners.create!(project_id: project.id, external_id: 'test 2', name: 'test scanner 2') }
+ let(:vulnerabilities) { table(:vulnerabilities) }
+ let(:vulnerabilities_findings) { table(:vulnerability_occurrences) }
+ let(:vulnerability_finding_signatures) { table(:vulnerability_finding_signatures) }
+ let(:vulnerability_identifiers) { table(:vulnerability_identifiers) }
+ let(:vulnerability_identifier) do
+ vulnerability_identifiers.create!(
+ project_id: project.id,
+ external_type: 'uuid-v5',
+ external_id: 'uuid-v5',
+ fingerprint: '7e394d1b1eb461a7406d7b1e08f057a1cf11287a',
+ name: 'Identifier for UUIDv5')
+ end
+
+ let(:different_vulnerability_identifier) do
+ vulnerability_identifiers.create!(
+ project_id: project.id,
+ external_type: 'uuid-v4',
+ external_id: 'uuid-v4',
+ fingerprint: '772da93d34a1ba010bcb5efa9fb6f8e01bafcc89',
+ name: 'Identifier for UUIDv4')
+ end
+
+ let!(:uuidv4_finding) do
+ create_finding!(
+ vulnerability_id: vulnerability_for_uuidv4.id,
+ project_id: project.id,
+ scanner_id: different_scanner.id,
+ primary_identifier_id: different_vulnerability_identifier.id,
+ location_fingerprint: Gitlab::Database::ShaAttribute.serialize('fa18f432f1d56675f4098d318739c3cd5b14eb3e'),
+ uuid: 'b3cc2518-5446-4dea-871c-89d5e999c1ac'
+ )
+ end
+
+ let(:vulnerability_for_uuidv4) do
+ create_vulnerability!(
+ project_id: project.id,
+ author_id: user.id
+ )
+ end
+
+ let!(:uuidv5_finding) do
+ create_finding!(
+ vulnerability_id: vulnerability_for_uuidv5.id,
+ project_id: project.id,
+ scanner_id: scanner.id,
+ primary_identifier_id: vulnerability_identifier.id,
+ location_fingerprint: Gitlab::Database::ShaAttribute.serialize('838574be0210968bf6b9f569df9c2576242cbf0a'),
+ uuid: '77211ed6-7dff-5f6b-8c9a-da89ad0a9b60'
+ )
+ end
+
+ let(:vulnerability_for_uuidv5) do
+ create_vulnerability!(
+ project_id: project.id,
+ author_id: user.id
+ )
+ end
+
+ let(:vulnerability_for_finding_with_signature) do
+ create_vulnerability!(
+ project_id: project.id,
+ author_id: user.id
+ )
+ end
+
+ let!(:finding_with_signature) do
+ create_finding!(
+ vulnerability_id: vulnerability_for_finding_with_signature.id,
+ project_id: project.id,
+ scanner_id: scanner.id,
+ primary_identifier_id: vulnerability_identifier.id,
+ report_type: 0, # "sast"
+ location_fingerprint: Gitlab::Database::ShaAttribute.serialize('123609eafffffa2207a9ca2425ba4337h34fga1b'),
+ uuid: '252aa474-d689-5d2b-ab42-7bbb5a100c02'
+ )
+ end
+
+ before do
+ stub_const("#{described_class}::BATCH_SIZE", 1)
+ end
+
+ around do |example|
+ freeze_time { Sidekiq::Testing.fake! { example.run } }
+ end
+
+ it 'schedules background migrations', :aggregate_failures do
+ migrate!
+
+ expect(BackgroundMigrationWorker.jobs.size).to eq(3)
+ expect(described_class::MIGRATION).to be_scheduled_delayed_migration(2.minutes, uuidv4_finding.id, uuidv4_finding.id)
+ expect(described_class::MIGRATION).to be_scheduled_delayed_migration(4.minutes, uuidv5_finding.id, uuidv5_finding.id)
+ expect(described_class::MIGRATION).to be_scheduled_delayed_migration(6.minutes, finding_with_signature.id, finding_with_signature.id)
+ end
+
+ private
+
+ def create_vulnerability!(project_id:, author_id:, title: 'test', severity: 7, confidence: 7, report_type: 0)
+ vulnerabilities.create!(
+ project_id: project_id,
+ author_id: author_id,
+ title: title,
+ severity: severity,
+ confidence: confidence,
+ report_type: report_type
+ )
+ end
+
+ def create_finding!(
+ vulnerability_id:, project_id:, scanner_id:, primary_identifier_id:, location_fingerprint:, uuid:, report_type: 0)
+ vulnerabilities_findings.create!(
+ vulnerability_id: vulnerability_id,
+ project_id: project_id,
+ name: 'test',
+ severity: 7,
+ confidence: 7,
+ report_type: report_type,
+ project_fingerprint: '123qweasdzxc',
+ scanner_id: scanner_id,
+ primary_identifier_id: primary_identifier_id,
+ location_fingerprint: location_fingerprint,
+ metadata_version: 'test',
+ raw_metadata: 'test',
+ uuid: uuid
+ )
+ end
+
+ def create_user!(name: "Example User", email: "user@example.com", user_type: nil)
+ users.create!(
+ name: name,
+ email: email,
+ username: name,
+ projects_limit: 0
+ )
+ end
+end
diff --git a/spec/migrations/20211210140629_encrypt_static_object_token_spec.rb b/spec/migrations/20211210140629_encrypt_static_object_token_spec.rb
new file mode 100644
index 00000000000..f103ee54990
--- /dev/null
+++ b/spec/migrations/20211210140629_encrypt_static_object_token_spec.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+require_migration!
+
+RSpec.describe EncryptStaticObjectToken, :migration, feature_category: :source_code_management do
+ let!(:background_migration_jobs) { table(:background_migration_jobs) }
+ let!(:users) { table(:users) }
+
+ let!(:user_without_tokens) { create_user!(name: 'notoken') }
+ let!(:user_with_plaintext_token_1) { create_user!(name: 'plaintext_1', token: 'token') }
+ let!(:user_with_plaintext_token_2) { create_user!(name: 'plaintext_2', token: 'TOKEN') }
+ let!(:user_with_encrypted_token) { create_user!(name: 'encrypted', encrypted_token: 'encrypted') }
+ let!(:user_with_both_tokens) { create_user!(name: 'both', token: 'token2', encrypted_token: 'encrypted2') }
+
+ before do
+ stub_const("#{described_class}::BATCH_SIZE", 1)
+ end
+
+ around do |example|
+ freeze_time { Sidekiq::Testing.fake! { example.run } }
+ end
+
+ it 'schedules background migrations' do
+ migrate!
+
+ expect(background_migration_jobs.count).to eq(2)
+ expect(background_migration_jobs.first.arguments).to match_array([user_with_plaintext_token_1.id, user_with_plaintext_token_1.id])
+ expect(background_migration_jobs.second.arguments).to match_array([user_with_plaintext_token_2.id, user_with_plaintext_token_2.id])
+
+ expect(BackgroundMigrationWorker.jobs.size).to eq(2)
+ expect(described_class::MIGRATION).to be_scheduled_delayed_migration(2.minutes, user_with_plaintext_token_1.id, user_with_plaintext_token_1.id)
+ expect(described_class::MIGRATION).to be_scheduled_delayed_migration(4.minutes, user_with_plaintext_token_2.id, user_with_plaintext_token_2.id)
+ end
+
+ private
+
+ def create_user!(name:, token: nil, encrypted_token: nil)
+ email = "#{name}@example.com"
+
+ table(:users).create!(
+ name: name,
+ email: email,
+ username: name,
+ projects_limit: 0,
+ static_object_token: token,
+ static_object_token_encrypted: encrypted_token
+ )
+ end
+end
diff --git a/spec/migrations/20211214012507_backfill_incident_issue_escalation_statuses_spec.rb b/spec/migrations/20211214012507_backfill_incident_issue_escalation_statuses_spec.rb
new file mode 100644
index 00000000000..0df52df43d8
--- /dev/null
+++ b/spec/migrations/20211214012507_backfill_incident_issue_escalation_statuses_spec.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe BackfillIncidentIssueEscalationStatuses, feature_category: :incident_management do
+ let(:namespaces) { table(:namespaces) }
+ let(:projects) { table(:projects) }
+ let(:issues) { table(:issues) }
+ let(:namespace) { namespaces.create!(name: 'foo', path: 'foo') }
+ let(:project) { projects.create!(namespace_id: namespace.id) }
+
+ # Backfill removed - see db/migrate/20220321234317_remove_all_issuable_escalation_statuses.rb.
+ it 'does nothing' do
+ issues.create!(project_id: project.id, issue_type: 1)
+
+ expect { migrate! }.not_to change { BackgroundMigrationWorker.jobs.size }
+ end
+end
diff --git a/spec/migrations/20211217174331_mark_recalculate_finding_signatures_as_completed_spec.rb b/spec/migrations/20211217174331_mark_recalculate_finding_signatures_as_completed_spec.rb
new file mode 100644
index 00000000000..2d808adf578
--- /dev/null
+++ b/spec/migrations/20211217174331_mark_recalculate_finding_signatures_as_completed_spec.rb
@@ -0,0 +1,64 @@
+# frozen_string_literal: true
+require 'spec_helper'
+require_migration!
+
+def create_background_migration_jobs(ids, status, created_at)
+ proper_status = case status
+ when :pending
+ Gitlab::Database::BackgroundMigrationJob.statuses['pending']
+ when :succeeded
+ Gitlab::Database::BackgroundMigrationJob.statuses['succeeded']
+ else
+ raise ArgumentError
+ end
+
+ background_migration_jobs.create!(
+ class_name: 'RecalculateVulnerabilitiesOccurrencesUuid',
+ arguments: Array(ids),
+ status: proper_status,
+ created_at: created_at
+ )
+end
+
+RSpec.describe MarkRecalculateFindingSignaturesAsCompleted, :migration, feature_category: :vulnerability_management do
+ let!(:background_migration_jobs) { table(:background_migration_jobs) }
+
+ context 'when RecalculateVulnerabilitiesOccurrencesUuid jobs are present' do
+ before do
+ create_background_migration_jobs([1, 2, 3], :succeeded, DateTime.new(2021, 5, 5, 0, 2))
+ create_background_migration_jobs([4, 5, 6], :pending, DateTime.new(2021, 5, 5, 0, 4))
+
+ create_background_migration_jobs([1, 2, 3], :succeeded, DateTime.new(2021, 8, 18, 0, 0))
+ create_background_migration_jobs([4, 5, 6], :pending, DateTime.new(2021, 8, 18, 0, 2))
+ create_background_migration_jobs([7, 8, 9], :pending, DateTime.new(2021, 8, 18, 0, 4))
+ end
+
+ describe 'gitlab.com' do
+ before do
+ allow(::Gitlab).to receive(:com?).and_return(true)
+ end
+
+ it 'marks all jobs as succeeded' do
+ expect(background_migration_jobs.where(status: 1).count).to eq(2)
+
+ migrate!
+
+ expect(background_migration_jobs.where(status: 1).count).to eq(5)
+ end
+ end
+
+ describe 'self managed' do
+ before do
+ allow(::Gitlab).to receive(:com?).and_return(false)
+ end
+
+ it 'does not change job status' do
+ expect(background_migration_jobs.where(status: 1).count).to eq(2)
+
+ migrate!
+
+ expect(background_migration_jobs.where(status: 1).count).to eq(2)
+ end
+ end
+ end
+end
diff --git a/spec/migrations/20220106111958_add_insert_or_update_vulnerability_reads_trigger_spec.rb b/spec/migrations/20220106111958_add_insert_or_update_vulnerability_reads_trigger_spec.rb
new file mode 100644
index 00000000000..263289462ba
--- /dev/null
+++ b/spec/migrations/20220106111958_add_insert_or_update_vulnerability_reads_trigger_spec.rb
@@ -0,0 +1,151 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+require_migration!
+
+RSpec.describe AddInsertOrUpdateVulnerabilityReadsTrigger, feature_category: :vulnerability_management do
+ let(:migration) { described_class.new }
+ let(:vulnerabilities) { table(:vulnerabilities) }
+ let(:vulnerability_reads) { table(:vulnerability_reads) }
+ let(:vulnerabilities_findings) { table(:vulnerability_occurrences) }
+ let(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') }
+ let(:user) { table(:users).create!(id: 13, email: 'author@example.com', username: 'author', projects_limit: 10) }
+ let(:project) { table(:projects).create!(id: 123, namespace_id: namespace.id) }
+ let(:scanner) { table(:vulnerability_scanners).create!(project_id: project.id, external_id: 'test 1', name: 'test scanner 1') }
+
+ let(:vulnerability) do
+ create_vulnerability!(
+ project_id: project.id,
+ author_id: user.id
+ )
+ end
+
+ let(:vulnerability2) do
+ create_vulnerability!(
+ project_id: project.id,
+ author_id: user.id
+ )
+ end
+
+ let(:identifier) do
+ table(:vulnerability_identifiers).create!(
+ project_id: project.id,
+ external_type: 'uuid-v5',
+ external_id: 'uuid-v5',
+ fingerprint: '7e394d1b1eb461a7406d7b1e08f057a1cf11287a',
+ name: 'Identifier for UUIDv5')
+ end
+
+ let(:finding) do
+ create_finding!(
+ project_id: project.id,
+ scanner_id: scanner.id,
+ primary_identifier_id: identifier.id
+ )
+ end
+
+ describe '#up' do
+ before do
+ migrate!
+ end
+
+ describe 'UPDATE trigger' do
+ context 'when vulnerability_id is updated' do
+ it 'creates a new vulnerability_reads row' do
+ expect do
+ finding.update!(vulnerability_id: vulnerability.id)
+ end.to change { vulnerability_reads.count }.from(0).to(1)
+ end
+ end
+
+ context 'when vulnerability_id is not updated' do
+ it 'does not create a new vulnerability_reads row' do
+ finding.update!(vulnerability_id: nil)
+
+ expect do
+ finding.update!(location: '')
+ end.not_to change { vulnerability_reads.count }
+ end
+ end
+ end
+
+ describe 'INSERT trigger' do
+ context 'when vulnerability_id is set' do
+ it 'creates a new vulnerability_reads row' do
+ expect do
+ create_finding!(
+ vulnerability_id: vulnerability2.id,
+ project_id: project.id,
+ scanner_id: scanner.id,
+ primary_identifier_id: identifier.id
+ )
+ end.to change { vulnerability_reads.count }.from(0).to(1)
+ end
+ end
+
+ context 'when vulnerability_id is not set' do
+ it 'does not create a new vulnerability_reads row' do
+ expect do
+ create_finding!(
+ project_id: project.id,
+ scanner_id: scanner.id,
+ primary_identifier_id: identifier.id
+ )
+ end.not_to change { vulnerability_reads.count }
+ end
+ end
+ end
+ end
+
+ describe '#down' do
+ before do
+ migration.up
+ migration.down
+ end
+
+ it 'drops the trigger' do
+ expect do
+ finding.update!(vulnerability_id: vulnerability.id)
+ end.not_to change { vulnerability_reads.count }
+ end
+ end
+
+ private
+
+ def create_vulnerability!(project_id:, author_id:, title: 'test', severity: 7, confidence: 7, report_type: 0)
+ vulnerabilities.create!(
+ project_id: project_id,
+ author_id: author_id,
+ title: title,
+ severity: severity,
+ confidence: confidence,
+ report_type: report_type
+ )
+ end
+
+ # rubocop:disable Metrics/ParameterLists
+ def create_finding!(
+ project_id:, scanner_id:, primary_identifier_id:, vulnerability_id: nil,
+ name: "test", severity: 7, confidence: 7, report_type: 0,
+ project_fingerprint: '123qweasdzxc', location: { "image" => "alpine:3.4" }, location_fingerprint: 'test',
+ metadata_version: 'test', raw_metadata: 'test', uuid: SecureRandom.uuid)
+ vulnerabilities_findings.create!(
+ vulnerability_id: vulnerability_id,
+ project_id: project_id,
+ name: name,
+ severity: severity,
+ confidence: confidence,
+ report_type: report_type,
+ project_fingerprint: project_fingerprint,
+ scanner_id: scanner_id,
+ primary_identifier_id: primary_identifier_id,
+ location: location,
+ location_fingerprint: location_fingerprint,
+ metadata_version: metadata_version,
+ raw_metadata: raw_metadata,
+ uuid: uuid
+ )
+ end
+ # rubocop:enable Metrics/ParameterLists
+end
diff --git a/spec/migrations/20220106112043_add_update_vulnerability_reads_trigger_spec.rb b/spec/migrations/20220106112043_add_update_vulnerability_reads_trigger_spec.rb
new file mode 100644
index 00000000000..152a551bc7b
--- /dev/null
+++ b/spec/migrations/20220106112043_add_update_vulnerability_reads_trigger_spec.rb
@@ -0,0 +1,128 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+require_migration!
+
+RSpec.describe AddUpdateVulnerabilityReadsTrigger, feature_category: :vulnerability_management do
+ let(:migration) { described_class.new }
+ let(:vulnerability_reads) { table(:vulnerability_reads) }
+ let(:issue_links) { table(:vulnerability_issue_links) }
+ let(:vulnerabilities) { table(:vulnerabilities) }
+ let(:vulnerabilities_findings) { table(:vulnerability_occurrences) }
+
+ let(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') }
+ let(:user) { table(:users).create!(id: 13, email: 'author@example.com', username: 'author', projects_limit: 10) }
+ let(:project) { table(:projects).create!(id: 123, namespace_id: namespace.id) }
+ let(:issue) { table(:issues).create!(description: '1234', state_id: 1, project_id: project.id) }
+ let(:scanner) { table(:vulnerability_scanners).create!(project_id: project.id, external_id: 'test 1', name: 'test scanner 1') }
+
+ let(:vulnerability) do
+ create_vulnerability!(
+ project_id: project.id,
+ report_type: 7,
+ author_id: user.id
+ )
+ end
+
+ let(:identifier) do
+ table(:vulnerability_identifiers).create!(
+ project_id: project.id,
+ external_type: 'uuid-v5',
+ external_id: 'uuid-v5',
+ fingerprint: '7e394d1b1eb461a7406d7b1e08f057a1cf11287a',
+ name: 'Identifier for UUIDv5')
+ end
+
+ describe '#up' do
+ before do
+ migrate!
+ end
+
+ describe 'UPDATE trigger' do
+ before do
+ create_finding!(
+ vulnerability_id: vulnerability.id,
+ project_id: project.id,
+ scanner_id: scanner.id,
+ report_type: 7,
+ primary_identifier_id: identifier.id
+ )
+ end
+
+ context 'when vulnerability attributes are updated' do
+ it 'updates vulnerability attributes in vulnerability_reads' do
+ expect do
+ vulnerability.update!(severity: 6)
+ end.to change { vulnerability_reads.first.severity }.from(7).to(6)
+ end
+ end
+
+ context 'when vulnerability attributes are not updated' do
+ it 'does not update vulnerability attributes in vulnerability_reads' do
+ expect do
+ vulnerability.update!(title: "New vulnerability")
+ end.not_to change { vulnerability_reads.first }
+ end
+ end
+ end
+ end
+
+ describe '#down' do
+ before do
+ migration.up
+ migration.down
+ create_finding!(
+ vulnerability_id: vulnerability.id,
+ project_id: project.id,
+ scanner_id: scanner.id,
+ report_type: 7,
+ primary_identifier_id: identifier.id
+ )
+ end
+
+ it 'drops the trigger' do
+ expect do
+ vulnerability.update!(severity: 6)
+ end.not_to change { vulnerability_reads.first.severity }
+ end
+ end
+
+ private
+
+ def create_vulnerability!(project_id:, author_id:, title: 'test', severity: 7, confidence: 7, report_type: 0)
+ vulnerabilities.create!(
+ project_id: project_id,
+ author_id: author_id,
+ title: title,
+ severity: severity,
+ confidence: confidence,
+ report_type: report_type
+ )
+ end
+
+ # rubocop:disable Metrics/ParameterLists
+ def create_finding!(
+ project_id:, scanner_id:, primary_identifier_id:, vulnerability_id: nil,
+ name: "test", severity: 7, confidence: 7, report_type: 0,
+ project_fingerprint: '123qweasdzxc', location: { "image" => "alpine:3.4" }, location_fingerprint: 'test',
+ metadata_version: 'test', raw_metadata: 'test', uuid: SecureRandom.uuid)
+ vulnerabilities_findings.create!(
+ vulnerability_id: vulnerability_id,
+ project_id: project_id,
+ name: name,
+ severity: severity,
+ confidence: confidence,
+ report_type: report_type,
+ project_fingerprint: project_fingerprint,
+ scanner_id: scanner_id,
+ primary_identifier_id: primary_identifier_id,
+ location: location,
+ location_fingerprint: location_fingerprint,
+ metadata_version: metadata_version,
+ raw_metadata: raw_metadata,
+ uuid: uuid
+ )
+ end
+ # rubocop:enable Metrics/ParameterLists
+end
diff --git a/spec/migrations/20220106112085_add_update_vulnerability_reads_location_trigger_spec.rb b/spec/migrations/20220106112085_add_update_vulnerability_reads_location_trigger_spec.rb
new file mode 100644
index 00000000000..9fc40b0b5f1
--- /dev/null
+++ b/spec/migrations/20220106112085_add_update_vulnerability_reads_location_trigger_spec.rb
@@ -0,0 +1,136 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+require_migration!
+
+RSpec.describe AddUpdateVulnerabilityReadsLocationTrigger, feature_category: :vulnerability_management do
+ let(:migration) { described_class.new }
+ let(:vulnerability_reads) { table(:vulnerability_reads) }
+ let(:issue_links) { table(:vulnerability_issue_links) }
+ let(:vulnerabilities) { table(:vulnerabilities) }
+ let(:vulnerabilities_findings) { table(:vulnerability_occurrences) }
+
+ let(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') }
+ let(:user) { table(:users).create!(id: 13, email: 'author@example.com', username: 'author', projects_limit: 10) }
+ let(:project) { table(:projects).create!(id: 123, namespace_id: namespace.id) }
+ let(:issue) { table(:issues).create!(description: '1234', state_id: 1, project_id: project.id) }
+ let(:scanner) { table(:vulnerability_scanners).create!(project_id: project.id, external_id: 'test 1', name: 'test scanner 1') }
+
+ let(:vulnerability) do
+ create_vulnerability!(
+ project_id: project.id,
+ report_type: 7,
+ author_id: user.id
+ )
+ end
+
+ let(:identifier) do
+ table(:vulnerability_identifiers).create!(
+ project_id: project.id,
+ external_type: 'uuid-v5',
+ external_id: 'uuid-v5',
+ fingerprint: '7e394d1b1eb461a7406d7b1e08f057a1cf11287a',
+ name: 'Identifier for UUIDv5')
+ end
+
+ describe '#up' do
+ before do
+ migrate!
+ end
+
+ describe 'UPDATE trigger' do
+ context 'when image is updated' do
+ it 'updates location_image in vulnerability_reads' do
+ finding = create_finding!(
+ vulnerability_id: vulnerability.id,
+ project_id: project.id,
+ scanner_id: scanner.id,
+ report_type: 7,
+ location: { "image" => "alpine:3.4" },
+ primary_identifier_id: identifier.id
+ )
+
+ expect do
+ finding.update!(location: { "image" => "alpine:4", "kubernetes_resource" => { "agent_id" => "1234" } })
+ end.to change { vulnerability_reads.first.location_image }.from("alpine:3.4").to("alpine:4")
+ end
+ end
+
+ context 'when image is not updated' do
+ it 'updates location_image in vulnerability_reads' do
+ finding = create_finding!(
+ vulnerability_id: vulnerability.id,
+ project_id: project.id,
+ scanner_id: scanner.id,
+ report_type: 7,
+ location: { "image" => "alpine:3.4", "kubernetes_resource" => { "agent_id" => "1234" } },
+ primary_identifier_id: identifier.id
+ )
+
+ expect do
+ finding.update!(project_fingerprint: "123qweasdzx")
+ end.not_to change { vulnerability_reads.first.location_image }
+ end
+ end
+ end
+ end
+
+ describe '#down' do
+ before do
+ migration.up
+ migration.down
+ end
+
+ it 'drops the trigger' do
+ finding = create_finding!(
+ vulnerability_id: vulnerability.id,
+ project_id: project.id,
+ scanner_id: scanner.id,
+ primary_identifier_id: identifier.id
+ )
+
+ expect do
+ finding.update!(location: '{"image":"alpine:4"}')
+ end.not_to change { vulnerability_reads.first.location_image }
+ end
+ end
+
+ private
+
+ def create_vulnerability!(project_id:, author_id:, title: 'test', severity: 7, confidence: 7, report_type: 0)
+ vulnerabilities.create!(
+ project_id: project_id,
+ author_id: author_id,
+ title: title,
+ severity: severity,
+ confidence: confidence,
+ report_type: report_type
+ )
+ end
+
+ # rubocop:disable Metrics/ParameterLists
+ def create_finding!(
+ project_id:, scanner_id:, primary_identifier_id:, vulnerability_id: nil,
+ name: "test", severity: 7, confidence: 7, report_type: 0,
+ project_fingerprint: '123qweasdzxc', location: { "image" => "alpine:3.4" }, location_fingerprint: 'test',
+ metadata_version: 'test', raw_metadata: 'test', uuid: SecureRandom.uuid)
+ vulnerabilities_findings.create!(
+ vulnerability_id: vulnerability_id,
+ project_id: project_id,
+ name: name,
+ severity: severity,
+ confidence: confidence,
+ report_type: report_type,
+ project_fingerprint: project_fingerprint,
+ scanner_id: scanner_id,
+ primary_identifier_id: primary_identifier_id,
+ location: location,
+ location_fingerprint: location_fingerprint,
+ metadata_version: metadata_version,
+ raw_metadata: raw_metadata,
+ uuid: uuid
+ )
+ end
+ # rubocop:enable Metrics/ParameterLists
+end
diff --git a/spec/migrations/20220106163326_add_has_issues_on_vulnerability_reads_trigger_spec.rb b/spec/migrations/20220106163326_add_has_issues_on_vulnerability_reads_trigger_spec.rb
new file mode 100644
index 00000000000..e58fdfb1591
--- /dev/null
+++ b/spec/migrations/20220106163326_add_has_issues_on_vulnerability_reads_trigger_spec.rb
@@ -0,0 +1,134 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+require_migration!
+
+RSpec.describe AddHasIssuesOnVulnerabilityReadsTrigger, feature_category: :vulnerability_management do
+ let(:migration) { described_class.new }
+ let(:vulnerability_reads) { table(:vulnerability_reads) }
+ let(:issue_links) { table(:vulnerability_issue_links) }
+ let(:vulnerabilities) { table(:vulnerabilities) }
+ let(:vulnerabilities_findings) { table(:vulnerability_occurrences) }
+
+ let(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') }
+ let(:user) { table(:users).create!(id: 13, email: 'author@example.com', username: 'author', projects_limit: 10) }
+ let(:project) { table(:projects).create!(id: 123, namespace_id: namespace.id) }
+ let(:issue) { table(:issues).create!(description: '1234', state_id: 1, project_id: project.id) }
+ let(:scanner) { table(:vulnerability_scanners).create!(project_id: project.id, external_id: 'test 1', name: 'test scanner 1') }
+
+ let(:vulnerability) do
+ create_vulnerability!(
+ project_id: project.id,
+ author_id: user.id
+ )
+ end
+
+ let(:identifier) do
+ table(:vulnerability_identifiers).create!(
+ project_id: project.id,
+ external_type: 'uuid-v5',
+ external_id: 'uuid-v5',
+ fingerprint: '7e394d1b1eb461a7406d7b1e08f057a1cf11287a',
+ name: 'Identifier for UUIDv5')
+ end
+
+ before do
+ create_finding!(
+ vulnerability_id: vulnerability.id,
+ project_id: project.id,
+ scanner_id: scanner.id,
+ primary_identifier_id: identifier.id
+ )
+
+ @vulnerability_read = vulnerability_reads.first
+ end
+
+ describe '#up' do
+ before do
+ migrate!
+ end
+
+ describe 'INSERT trigger' do
+ it 'updates has_issues in vulnerability_reads' do
+ expect do
+ issue_links.create!(vulnerability_id: vulnerability.id, issue_id: issue.id)
+ end.to change { @vulnerability_read.reload.has_issues }.from(false).to(true)
+ end
+ end
+
+ describe 'DELETE trigger' do
+ let(:issue2) { table(:issues).create!(description: '1234', state_id: 1, project_id: project.id) }
+
+ it 'does not change has_issues when there exists another issue' do
+ issue_link1 = issue_links.create!(vulnerability_id: vulnerability.id, issue_id: issue.id)
+ issue_links.create!(vulnerability_id: vulnerability.id, issue_id: issue2.id)
+
+ expect do
+ issue_link1.delete
+ end.not_to change { @vulnerability_read.reload.has_issues }
+ end
+
+ it 'unsets has_issues when all issues are deleted' do
+ issue_link1 = issue_links.create!(vulnerability_id: vulnerability.id, issue_id: issue.id)
+ issue_link2 = issue_links.create!(vulnerability_id: vulnerability.id, issue_id: issue2.id)
+
+ expect do
+ issue_link1.delete
+ issue_link2.delete
+ end.to change { @vulnerability_read.reload.has_issues }.from(true).to(false)
+ end
+ end
+ end
+
+ describe '#down' do
+ before do
+ migration.up
+ migration.down
+ end
+
+ it 'drops the trigger' do
+ expect do
+ issue_links.create!(vulnerability_id: vulnerability.id, issue_id: issue.id)
+ end.not_to change { @vulnerability_read.has_issues }
+ end
+ end
+
+ private
+
+ def create_vulnerability!(project_id:, author_id:, title: 'test', severity: 7, confidence: 7, report_type: 0)
+ vulnerabilities.create!(
+ project_id: project_id,
+ author_id: author_id,
+ title: title,
+ severity: severity,
+ confidence: confidence,
+ report_type: report_type
+ )
+ end
+
+ # rubocop:disable Metrics/ParameterLists
+ def create_finding!(
+ project_id:, scanner_id:, primary_identifier_id:, vulnerability_id: nil,
+ name: "test", severity: 7, confidence: 7, report_type: 0,
+ project_fingerprint: '123qweasdzxc', location: { "image" => "alpine:3.4" }, location_fingerprint: 'test',
+ metadata_version: 'test', raw_metadata: 'test', uuid: SecureRandom.uuid)
+ vulnerabilities_findings.create!(
+ vulnerability_id: vulnerability_id,
+ project_id: project_id,
+ name: name,
+ severity: severity,
+ confidence: confidence,
+ report_type: report_type,
+ project_fingerprint: project_fingerprint,
+ scanner_id: scanner_id,
+ primary_identifier_id: primary_identifier_id,
+ location: location,
+ location_fingerprint: location_fingerprint,
+ metadata_version: metadata_version,
+ raw_metadata: raw_metadata,
+ uuid: uuid
+ )
+ end
+ # rubocop:enable Metrics/ParameterLists
+end
diff --git a/spec/migrations/20220107064845_populate_vulnerability_reads_spec.rb b/spec/migrations/20220107064845_populate_vulnerability_reads_spec.rb
new file mode 100644
index 00000000000..1338f826537
--- /dev/null
+++ b/spec/migrations/20220107064845_populate_vulnerability_reads_spec.rb
@@ -0,0 +1,106 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+require_migration!
+
+RSpec.describe PopulateVulnerabilityReads, :migration, feature_category: :vulnerability_management do
+ let!(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') }
+ let!(:user) { table(:users).create!(email: 'author@example.com', username: 'author', projects_limit: 10) }
+ let!(:project) { table(:projects).create!(namespace_id: namespace.id) }
+ let!(:scanner) { table(:vulnerability_scanners).create!(project_id: project.id, external_id: 'test 1', name: 'test scanner 1') }
+ let!(:background_migration_jobs) { table(:background_migration_jobs) }
+ let!(:vulnerabilities) { table(:vulnerabilities) }
+ let!(:vulnerability_reads) { table(:vulnerability_reads) }
+ let!(:vulnerabilities_findings) { table(:vulnerability_occurrences) }
+ let!(:vulnerability_issue_links) { table(:vulnerability_issue_links) }
+ let!(:vulnerability_ids) { [] }
+
+ before do
+ stub_const("#{described_class}::BATCH_SIZE", 1)
+ stub_const("#{described_class}::SUB_BATCH_SIZE", 1)
+
+ 5.times.each do |x|
+ vulnerability = create_vulnerability!(
+ project_id: project.id,
+ report_type: 7,
+ author_id: user.id
+ )
+ identifier = table(:vulnerability_identifiers).create!(
+ project_id: project.id,
+ external_type: 'uuid-v5',
+ external_id: 'uuid-v5',
+ fingerprint: Digest::SHA1.hexdigest(vulnerability.id.to_s),
+ name: 'Identifier for UUIDv5')
+
+ create_finding!(
+ vulnerability_id: vulnerability.id,
+ project_id: project.id,
+ scanner_id: scanner.id,
+ primary_identifier_id: identifier.id
+ )
+
+ vulnerability_ids << vulnerability.id
+ end
+ end
+
+ around do |example|
+ freeze_time { Sidekiq::Testing.fake! { example.run } }
+ end
+
+ it 'schedules background migrations' do
+ migrate!
+
+ expect(background_migration_jobs.count).to eq(5)
+ expect(background_migration_jobs.first.arguments).to match_array([vulnerability_ids.first, vulnerability_ids.first, 1])
+ expect(background_migration_jobs.second.arguments).to match_array([vulnerability_ids.second, vulnerability_ids.second, 1])
+ expect(background_migration_jobs.third.arguments).to match_array([vulnerability_ids.third, vulnerability_ids.third, 1])
+ expect(background_migration_jobs.fourth.arguments).to match_array([vulnerability_ids.fourth, vulnerability_ids.fourth, 1])
+ expect(background_migration_jobs.fifth.arguments).to match_array([vulnerability_ids.fifth, vulnerability_ids.fifth, 1])
+
+ expect(BackgroundMigrationWorker.jobs.size).to eq(5)
+ expect(described_class::MIGRATION_NAME).to be_scheduled_delayed_migration(2.minutes, vulnerability_ids.first, vulnerability_ids.first, 1)
+ expect(described_class::MIGRATION_NAME).to be_scheduled_delayed_migration(4.minutes, vulnerability_ids.second, vulnerability_ids.second, 1)
+ expect(described_class::MIGRATION_NAME).to be_scheduled_delayed_migration(6.minutes, vulnerability_ids.third, vulnerability_ids.third, 1)
+ expect(described_class::MIGRATION_NAME).to be_scheduled_delayed_migration(8.minutes, vulnerability_ids.fourth, vulnerability_ids.fourth, 1)
+ expect(described_class::MIGRATION_NAME).to be_scheduled_delayed_migration(10.minutes, vulnerability_ids.fifth, vulnerability_ids.fifth, 1)
+ end
+
+ private
+
+ def create_vulnerability!(project_id:, author_id:, title: 'test', severity: 7, confidence: 7, report_type: 0)
+ vulnerabilities.create!(
+ project_id: project_id,
+ author_id: author_id,
+ title: title,
+ severity: severity,
+ confidence: confidence,
+ report_type: report_type
+ )
+ end
+
+ # rubocop:disable Metrics/ParameterLists
+ def create_finding!(
+ vulnerability_id:, project_id:, scanner_id:, primary_identifier_id:, id: nil,
+ name: "test", severity: 7, confidence: 7, report_type: 0,
+ project_fingerprint: '123qweasdzxc', location_fingerprint: 'test',
+ metadata_version: 'test', raw_metadata: 'test', uuid: SecureRandom.uuid)
+ params = {
+ vulnerability_id: vulnerability_id,
+ project_id: project_id,
+ name: name,
+ severity: severity,
+ confidence: confidence,
+ report_type: report_type,
+ project_fingerprint: project_fingerprint,
+ scanner_id: scanner_id,
+ primary_identifier_id: primary_identifier_id,
+ location_fingerprint: location_fingerprint,
+ metadata_version: metadata_version,
+ raw_metadata: raw_metadata,
+ uuid: uuid
+ }
+ params[:id] = id unless id.nil?
+ vulnerabilities_findings.create!(params)
+ end
+ # rubocop:enable Metrics/ParameterLists
+end
diff --git a/spec/migrations/20220120094340_drop_position_from_security_findings_spec.rb b/spec/migrations/20220120094340_drop_position_from_security_findings_spec.rb
new file mode 100644
index 00000000000..1470f2b3cad
--- /dev/null
+++ b/spec/migrations/20220120094340_drop_position_from_security_findings_spec.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!('drop_position_from_security_findings')
+
+RSpec.describe DropPositionFromSecurityFindings, feature_category: :vulnerability_management do
+ let(:events) { table(:security_findings) }
+
+ it 'correctly migrates up and down' do
+ reversible_migration do |migration|
+ migration.before -> {
+ expect(events.column_names).to include('position')
+ }
+
+ migration.after -> {
+ events.reset_column_information
+ expect(events.column_names).not_to include('position')
+ }
+ end
+ end
+end
diff --git a/spec/migrations/20220124130028_dedup_runner_projects_spec.rb b/spec/migrations/20220124130028_dedup_runner_projects_spec.rb
new file mode 100644
index 00000000000..b9189cbae7f
--- /dev/null
+++ b/spec/migrations/20220124130028_dedup_runner_projects_spec.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe DedupRunnerProjects, :migration, :suppress_gitlab_schemas_validate_connection,
+ schema: 20220120085655, feature_category: :runner do
+ let(:namespaces) { table(:namespaces) }
+ let(:projects) { table(:projects) }
+ let(:runners) { table(:ci_runners) }
+ let(:runner_projects) { table(:ci_runner_projects) }
+
+ let!(:namespace) { namespaces.create!(name: 'foo', path: 'foo') }
+ let!(:project) { projects.create!(namespace_id: namespace.id) }
+ let!(:project_2) { projects.create!(namespace_id: namespace.id) }
+ let!(:runner) { runners.create!(runner_type: 'project_type') }
+ let!(:runner_2) { runners.create!(runner_type: 'project_type') }
+ let!(:runner_3) { runners.create!(runner_type: 'project_type') }
+
+ let!(:duplicated_runner_project_1) { runner_projects.create!(runner_id: runner.id, project_id: project.id) }
+ let!(:duplicated_runner_project_2) { runner_projects.create!(runner_id: runner.id, project_id: project.id) }
+ let!(:duplicated_runner_project_3) { runner_projects.create!(runner_id: runner_2.id, project_id: project_2.id) }
+ let!(:duplicated_runner_project_4) { runner_projects.create!(runner_id: runner_2.id, project_id: project_2.id) }
+
+ let!(:non_duplicated_runner_project) { runner_projects.create!(runner_id: runner_3.id, project_id: project.id) }
+
+ it 'deduplicates ci_runner_projects table' do
+ expect { migrate! }.to change { runner_projects.count }.from(5).to(3)
+ end
+
+ it 'merges `duplicated_runner_project_1` with `duplicated_runner_project_2`', :aggregate_failures do
+ migrate!
+
+ expect(runner_projects.where(id: duplicated_runner_project_1.id)).not_to(exist)
+
+ merged_runner_projects = runner_projects.find_by(id: duplicated_runner_project_2.id)
+
+ expect(merged_runner_projects).to be_present
+ expect(merged_runner_projects.created_at).to be_like_time(duplicated_runner_project_1.created_at)
+ expect(merged_runner_projects.created_at).to be_like_time(duplicated_runner_project_2.created_at)
+ end
+
+ it 'merges `duplicated_runner_project_3` with `duplicated_runner_project_4`', :aggregate_failures do
+ migrate!
+
+ expect(runner_projects.where(id: duplicated_runner_project_3.id)).not_to(exist)
+
+ merged_runner_projects = runner_projects.find_by(id: duplicated_runner_project_4.id)
+
+ expect(merged_runner_projects).to be_present
+ expect(merged_runner_projects.created_at).to be_like_time(duplicated_runner_project_3.created_at)
+ expect(merged_runner_projects.created_at).to be_like_time(duplicated_runner_project_4.created_at)
+ end
+
+ it 'does not change non duplicated records' do
+ expect { migrate! }.not_to change { non_duplicated_runner_project.reload.attributes }
+ end
+
+ it 'does nothing when there are no runner projects' do
+ runner_projects.delete_all
+
+ migrate!
+
+ expect(runner_projects.count).to eq(0)
+ end
+end
diff --git a/spec/migrations/20220128155251_remove_dangling_running_builds_spec.rb b/spec/migrations/20220128155251_remove_dangling_running_builds_spec.rb
new file mode 100644
index 00000000000..3abe173196f
--- /dev/null
+++ b/spec/migrations/20220128155251_remove_dangling_running_builds_spec.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!('remove_dangling_running_builds')
+
+RSpec.describe RemoveDanglingRunningBuilds, :suppress_gitlab_schemas_validate_connection,
+ feature_category: :continuous_integration do
+ let(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') }
+ let(:project) { table(:projects).create!(namespace_id: namespace.id) }
+ let(:runner) { table(:ci_runners).create!(runner_type: 1) }
+ let(:builds) { table(:ci_builds) }
+ let(:running_builds) { table(:ci_running_builds) }
+
+ let(:running_build) do
+ builds.create!(
+ name: 'test 1',
+ status: 'running',
+ project_id: project.id,
+ type: 'Ci::Build')
+ end
+
+ let(:failed_build) do
+ builds.create!(
+ name: 'test 2',
+ status: 'failed',
+ project_id: project.id,
+ type: 'Ci::Build')
+ end
+
+ let!(:running_metadata) do
+ running_builds.create!(
+ build_id: running_build.id,
+ project_id: project.id,
+ runner_id: runner.id,
+ runner_type:
+ runner.runner_type)
+ end
+
+ let!(:failed_metadata) do
+ running_builds.create!(
+ build_id: failed_build.id,
+ project_id: project.id,
+ runner_id: runner.id,
+ runner_type: runner.runner_type)
+ end
+
+ it 'removes failed builds' do
+ migrate!
+
+ expect(running_metadata.reload).to be_present
+ expect { failed_metadata.reload }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+end
diff --git a/spec/migrations/20220128155814_fix_approval_rules_code_owners_rule_type_index_spec.rb b/spec/migrations/20220128155814_fix_approval_rules_code_owners_rule_type_index_spec.rb
new file mode 100644
index 00000000000..3f3fdd0889d
--- /dev/null
+++ b/spec/migrations/20220128155814_fix_approval_rules_code_owners_rule_type_index_spec.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!('fix_approval_rules_code_owners_rule_type_index')
+
+RSpec.describe FixApprovalRulesCodeOwnersRuleTypeIndex, :migration, feature_category: :source_code_management do
+ let(:table_name) { :approval_merge_request_rules }
+ let(:index_name) { 'index_approval_rules_code_owners_rule_type' }
+
+ it 'correctly migrates up and down' do
+ reversible_migration do |migration|
+ migration.before -> {
+ expect(subject.index_exists_by_name?(table_name, index_name)).to be_truthy
+ }
+
+ migration.after -> {
+ expect(subject.index_exists_by_name?(table_name, index_name)).to be_truthy
+ }
+ end
+ end
+
+ context 'when the index already exists' do
+ before do
+ subject.add_concurrent_index table_name, :merge_request_id, where: 'rule_type = 2', name: index_name
+ end
+
+ it 'keeps the index' do
+ migrate!
+
+ expect(subject.index_exists_by_name?(table_name, index_name)).to be_truthy
+ end
+ end
+end
diff --git a/spec/migrations/20220202105733_delete_service_template_records_spec.rb b/spec/migrations/20220202105733_delete_service_template_records_spec.rb
new file mode 100644
index 00000000000..41762a3a5c3
--- /dev/null
+++ b/spec/migrations/20220202105733_delete_service_template_records_spec.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+require_migration!
+
+RSpec.describe DeleteServiceTemplateRecords, feature_category: :integrations do
+ let(:integrations) { table(:integrations) }
+ let(:chat_names) { table(:chat_names) }
+ let(:web_hooks) { table(:web_hooks) }
+ let(:slack_integrations) { table(:slack_integrations) }
+ let(:zentao_tracker_data) { table(:zentao_tracker_data) }
+ let(:jira_tracker_data) { table(:jira_tracker_data) }
+ let(:issue_tracker_data) { table(:issue_tracker_data) }
+
+ before do
+ template = integrations.create!(template: true)
+ chat_names.create!(service_id: template.id, user_id: 1, team_id: 1, chat_id: 1)
+ web_hooks.create!(service_id: template.id)
+ slack_integrations.create!(service_id: template.id, team_id: 1, team_name: 'team', alias: 'alias', user_id: 1)
+ zentao_tracker_data.create!(integration_id: template.id)
+ jira_tracker_data.create!(service_id: template.id)
+ issue_tracker_data.create!(service_id: template.id)
+
+ integrations.create!(template: false)
+ end
+
+ it 'deletes template records and associated data' do
+ expect { migrate! }
+ .to change { integrations.where(template: true).count }.from(1).to(0)
+ .and change { chat_names.count }.from(1).to(0)
+ .and change { web_hooks.count }.from(1).to(0)
+ .and change { slack_integrations.count }.from(1).to(0)
+ .and change { zentao_tracker_data.count }.from(1).to(0)
+ .and change { jira_tracker_data.count }.from(1).to(0)
+ .and change { issue_tracker_data.count }.from(1).to(0)
+ end
+
+ it 'does not delete non template records' do
+ expect { migrate! }
+ .not_to change { integrations.where(template: false).count }
+ end
+end
diff --git a/spec/migrations/20220204095121_backfill_namespace_statistics_with_dependency_proxy_size_spec.rb b/spec/migrations/20220204095121_backfill_namespace_statistics_with_dependency_proxy_size_spec.rb
new file mode 100644
index 00000000000..cbae5674d78
--- /dev/null
+++ b/spec/migrations/20220204095121_backfill_namespace_statistics_with_dependency_proxy_size_spec.rb
@@ -0,0 +1,64 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe BackfillNamespaceStatisticsWithDependencyProxySize, feature_category: :dependency_proxy do
+ let!(:groups) { table(:namespaces) }
+ let!(:group1) { groups.create!(id: 10, name: 'test1', path: 'test1', type: 'Group') }
+ let!(:group2) { groups.create!(id: 20, name: 'test2', path: 'test2', type: 'Group') }
+ let!(:group3) { groups.create!(id: 30, name: 'test3', path: 'test3', type: 'Group') }
+ let!(:group4) { groups.create!(id: 40, name: 'test4', path: 'test4', type: 'Group') }
+
+ let!(:dependency_proxy_blobs) { table(:dependency_proxy_blobs) }
+ let!(:dependency_proxy_manifests) { table(:dependency_proxy_manifests) }
+
+ let!(:group1_manifest) { create_manifest(10, 10) }
+ let!(:group2_manifest) { create_manifest(20, 20) }
+ let!(:group3_manifest) { create_manifest(30, 30) }
+
+ let!(:group1_blob) { create_blob(10, 10) }
+ let!(:group2_blob) { create_blob(20, 20) }
+ let!(:group3_blob) { create_blob(30, 30) }
+
+ describe '#up' do
+ it 'correctly schedules background migrations' do
+ stub_const("#{described_class}::BATCH_SIZE", 2)
+
+ Sidekiq::Testing.fake! do
+ freeze_time do
+ migrate!
+
+ aggregate_failures do
+ expect(described_class::MIGRATION)
+ .to be_scheduled_migration([10, 30], ['dependency_proxy_size'])
+
+ expect(described_class::MIGRATION)
+ .to be_scheduled_delayed_migration(2.minutes, [20], ['dependency_proxy_size'])
+
+ expect(BackgroundMigrationWorker.jobs.size).to eq(2)
+ end
+ end
+ end
+ end
+ end
+
+ def create_manifest(group_id, size)
+ dependency_proxy_manifests.create!(
+ group_id: group_id,
+ size: size,
+ file_name: 'test-file',
+ file: 'test',
+ digest: 'abc123'
+ )
+ end
+
+ def create_blob(group_id, size)
+ dependency_proxy_blobs.create!(
+ group_id: group_id,
+ size: size,
+ file_name: 'test-file',
+ file: 'test'
+ )
+ end
+end
diff --git a/spec/migrations/20220204194347_encrypt_integration_properties_spec.rb b/spec/migrations/20220204194347_encrypt_integration_properties_spec.rb
new file mode 100644
index 00000000000..5e728bb396c
--- /dev/null
+++ b/spec/migrations/20220204194347_encrypt_integration_properties_spec.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+require_migration!
+
+RSpec.describe EncryptIntegrationProperties, :migration, schema: 20220204193000, feature_category: :integrations do
+ subject(:migration) { described_class.new }
+
+ let(:integrations) { table(:integrations) }
+
+ before do
+ stub_const("#{described_class.name}::BATCH_SIZE", 2)
+ end
+
+ it 'correctly schedules background migrations', :aggregate_failures do
+ # update required
+ record1 = integrations.create!(properties: some_props)
+ record2 = integrations.create!(properties: some_props)
+ record3 = integrations.create!(properties: some_props)
+ record4 = integrations.create!(properties: nil)
+ record5 = integrations.create!(properties: nil)
+
+ Sidekiq::Testing.fake! do
+ freeze_time do
+ migrate!
+
+ expect(described_class::MIGRATION).to be_scheduled_migration(record1.id, record2.id)
+ expect(described_class::MIGRATION).to be_scheduled_migration(record3.id, record4.id)
+ expect(described_class::MIGRATION).to be_scheduled_migration(record5.id, record5.id)
+
+ expect(BackgroundMigrationWorker.jobs.size).to eq(3)
+ end
+ end
+ end
+
+ def some_props
+ { iid: generate(:iid), url: generate(:url), username: generate(:username) }.to_json
+ end
+end
diff --git a/spec/migrations/20220208080921_schedule_migrate_personal_namespace_project_maintainer_to_owner_spec.rb b/spec/migrations/20220208080921_schedule_migrate_personal_namespace_project_maintainer_to_owner_spec.rb
new file mode 100644
index 00000000000..89583d1050b
--- /dev/null
+++ b/spec/migrations/20220208080921_schedule_migrate_personal_namespace_project_maintainer_to_owner_spec.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe ScheduleMigratePersonalNamespaceProjectMaintainerToOwner, feature_category: :subgroups do
+ let!(:migration) { described_class::MIGRATION }
+
+ describe '#up' do
+ it 'schedules background jobs for each batch of members' do
+ migrate!
+
+ expect(migration).to have_scheduled_batched_migration(
+ table_name: :members,
+ column_name: :id,
+ interval: described_class::INTERVAL
+ )
+ end
+ end
+end
diff --git a/spec/migrations/20220211214605_update_integrations_trigger_type_new_on_insert_null_safe_spec.rb b/spec/migrations/20220211214605_update_integrations_trigger_type_new_on_insert_null_safe_spec.rb
new file mode 100644
index 00000000000..8a6a542bc5e
--- /dev/null
+++ b/spec/migrations/20220211214605_update_integrations_trigger_type_new_on_insert_null_safe_spec.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+require_migration!
+
+RSpec.describe UpdateIntegrationsTriggerTypeNewOnInsertNullSafe, :migration, feature_category: :integrations do
+ let(:integrations) { table(:integrations) }
+
+ before do
+ migrate!
+ end
+
+ it 'leaves defined values alone' do
+ record = integrations.create!(type: 'XService', type_new: 'Integrations::Y')
+
+ expect(integrations.find(record.id)).to have_attributes(type: 'XService', type_new: 'Integrations::Y')
+ end
+
+ it 'keeps type_new synchronized with type' do
+ record = integrations.create!(type: 'AbcService', type_new: nil)
+
+ expect(integrations.find(record.id)).to have_attributes(
+ type: 'AbcService',
+ type_new: 'Integrations::Abc'
+ )
+ end
+
+ it 'keeps type synchronized with type_new' do
+ record = integrations.create!(type: nil, type_new: 'Integrations::Abc')
+
+ expect(integrations.find(record.id)).to have_attributes(
+ type: 'AbcService',
+ type_new: 'Integrations::Abc'
+ )
+ end
+end
diff --git a/spec/migrations/20220213103859_remove_integrations_type_spec.rb b/spec/migrations/20220213103859_remove_integrations_type_spec.rb
new file mode 100644
index 00000000000..8f6d9b0d9b5
--- /dev/null
+++ b/spec/migrations/20220213103859_remove_integrations_type_spec.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+require_migration!
+
+RSpec.describe RemoveIntegrationsType, :migration, feature_category: :integrations do
+ subject(:migration) { described_class.new }
+
+ let(:integrations) { table(:integrations) }
+ let(:bg_migration) { instance_double(bg_migration_class) }
+
+ before do
+ stub_const("#{described_class.name}::BATCH_SIZE", 2)
+ end
+
+ it 'performs remaining background migrations', :aggregate_failures do
+ # Already migrated
+ integrations.create!(type: 'SlackService', type_new: 'Integrations::Slack')
+ # update required
+ record1 = integrations.create!(type: 'SlackService')
+ record2 = integrations.create!(type: 'JiraService')
+ record3 = integrations.create!(type: 'SlackService')
+
+ migrate!
+
+ expect(record1.reload.type_new).to eq 'Integrations::Slack'
+ expect(record2.reload.type_new).to eq 'Integrations::Jira'
+ expect(record3.reload.type_new).to eq 'Integrations::Slack'
+ end
+end
diff --git a/spec/migrations/20220222192524_create_not_null_constraint_releases_tag_spec.rb b/spec/migrations/20220222192524_create_not_null_constraint_releases_tag_spec.rb
new file mode 100644
index 00000000000..b8a37dcd6d9
--- /dev/null
+++ b/spec/migrations/20220222192524_create_not_null_constraint_releases_tag_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+require 'spec_helper'
+require_migration!
+
+RSpec.describe CreateNotNullConstraintReleasesTag, feature_category: :release_orchestration do
+ let!(:releases) { table(:releases) }
+ let!(:migration) { described_class.new }
+
+ before do
+ allow(migration).to receive(:transaction_open?).and_return(false)
+ allow(migration).to receive(:with_lock_retries).and_yield
+ end
+
+ it 'adds a check constraint to tags' do
+ constraint = releases.connection.check_constraints(:releases).find { |constraint| constraint.expression == "tag IS NOT NULL" }
+ expect(constraint).to be_nil
+
+ migration.up
+
+ constraint = releases.connection.check_constraints(:releases).find { |constraint| constraint.expression == "tag IS NOT NULL" }
+ expect(constraint).to be_a(ActiveRecord::ConnectionAdapters::CheckConstraintDefinition)
+ end
+end
diff --git a/spec/migrations/20220222192525_remove_null_releases_spec.rb b/spec/migrations/20220222192525_remove_null_releases_spec.rb
new file mode 100644
index 00000000000..ce42dea077d
--- /dev/null
+++ b/spec/migrations/20220222192525_remove_null_releases_spec.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+require_migration!
+
+RSpec.describe RemoveNullReleases, feature_category: :release_orchestration do
+ let(:releases) { table(:releases) }
+
+ before do
+ # we need to migrate to before previous migration so an invalid record can be created
+ migrate!
+ migration_context.down(previous_migration(3).version)
+
+ releases.create!(tag: 'good', name: 'good release', released_at: Time.now)
+ releases.create!(tag: nil, name: 'bad release', released_at: Time.now)
+ end
+
+ it 'deletes template records and associated data' do
+ expect { migrate! }
+ .to change { releases.count }.from(2).to(1)
+ end
+end
diff --git a/spec/migrations/20220223124428_schedule_merge_topics_with_same_name_spec.rb b/spec/migrations/20220223124428_schedule_merge_topics_with_same_name_spec.rb
new file mode 100644
index 00000000000..425f622581b
--- /dev/null
+++ b/spec/migrations/20220223124428_schedule_merge_topics_with_same_name_spec.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe ScheduleMergeTopicsWithSameName, feature_category: :projects do
+ let(:topics) { table(:topics) }
+
+ describe '#up' do
+ before do
+ stub_const("#{described_class}::BATCH_SIZE", 2)
+
+ topics.create!(name: 'topic1')
+ topics.create!(name: 'Topic2')
+ topics.create!(name: 'Topic3')
+ topics.create!(name: 'Topic4')
+ topics.create!(name: 'topic2')
+ topics.create!(name: 'topic3')
+ topics.create!(name: 'topic4')
+ topics.create!(name: 'TOPIC2')
+ topics.create!(name: 'topic5')
+ end
+
+ it 'schedules MergeTopicsWithSameName background jobs', :aggregate_failures do
+ Sidekiq::Testing.fake! do
+ freeze_time do
+ migrate!
+
+ expect(described_class::MIGRATION).to be_scheduled_delayed_migration(2.minutes, %w[topic2 topic3])
+ expect(described_class::MIGRATION).to be_scheduled_delayed_migration(4.minutes, %w[topic4])
+ expect(BackgroundMigrationWorker.jobs.size).to eq(2)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/migrations/20220305223212_add_security_training_providers_spec.rb b/spec/migrations/20220305223212_add_security_training_providers_spec.rb
new file mode 100644
index 00000000000..f67db3b68cd
--- /dev/null
+++ b/spec/migrations/20220305223212_add_security_training_providers_spec.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe AddSecurityTrainingProviders, :migration, feature_category: :vulnerability_management do
+ include MigrationHelpers::WorkItemTypesHelper
+
+ let!(:security_training_providers) { table(:security_training_providers) }
+
+ it 'creates default data' do
+ # Need to delete all as security training providers are seeded before entire test suite
+ security_training_providers.delete_all
+
+ reversible_migration do |migration|
+ migration.before -> {
+ expect(security_training_providers.count).to eq(0)
+ }
+
+ migration.after -> {
+ expect(security_training_providers.count).to eq(2)
+ }
+ end
+ end
+end
diff --git a/spec/migrations/20220307192610_remove_duplicate_project_tag_releases_spec.rb b/spec/migrations/20220307192610_remove_duplicate_project_tag_releases_spec.rb
new file mode 100644
index 00000000000..98e2ba4816b
--- /dev/null
+++ b/spec/migrations/20220307192610_remove_duplicate_project_tag_releases_spec.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe RemoveDuplicateProjectTagReleases, feature_category: :release_orchestration do
+ let(:namespaces) { table(:namespaces) }
+ let(:projects) { table(:projects) }
+ let(:users) { table(:users) }
+ let(:releases) { table(:releases) }
+
+ let(:namespace) { namespaces.create!(name: 'gitlab', path: 'gitlab-org') }
+ let(:project) { projects.create!(namespace_id: namespace.id, name: 'foo') }
+
+ let(:dup_releases) do
+ Array.new(4).fill do |i|
+ rel = releases.new(project_id: project.id, tag: "duplicate tag", released_at: (DateTime.now + i.days))
+ rel.save!(validate: false)
+ rel
+ end
+ end
+
+ let(:valid_release) do
+ releases.create!(
+ project_id: project.id,
+ tag: "valid tag",
+ released_at: DateTime.now
+ )
+ end
+
+ describe '#up' do
+ it "correctly removes duplicate tags from the same project" do
+ expect(dup_releases.length).to eq 4
+ expect(valid_release).not_to be nil
+ expect(releases.where(tag: 'duplicate tag').count).to eq 4
+ expect(releases.where(tag: 'valid tag').count).to eq 1
+
+ migrate!
+
+ expect(releases.where(tag: 'duplicate tag').count).to eq 1
+ expect(releases.where(tag: 'valid tag').count).to eq 1
+ expect(releases.all.map(&:tag)).to match_array ['valid tag', 'duplicate tag']
+ end
+ end
+end
diff --git a/spec/migrations/20220309084954_remove_leftover_external_pull_request_deletions_spec.rb b/spec/migrations/20220309084954_remove_leftover_external_pull_request_deletions_spec.rb
new file mode 100644
index 00000000000..8df9907643e
--- /dev/null
+++ b/spec/migrations/20220309084954_remove_leftover_external_pull_request_deletions_spec.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+require_migration!
+
+RSpec.describe RemoveLeftoverExternalPullRequestDeletions, feature_category: :cell do
+ let(:deleted_records) { table(:loose_foreign_keys_deleted_records) }
+
+ let(:pending_record1) { deleted_records.create!(id: 1, fully_qualified_table_name: 'public.external_pull_requests', primary_key_value: 1, status: 1) }
+ let(:pending_record2) { deleted_records.create!(id: 2, fully_qualified_table_name: 'public.external_pull_requests', primary_key_value: 2, status: 1) }
+ let(:other_pending_record1) { deleted_records.create!(id: 3, fully_qualified_table_name: 'public.projects', primary_key_value: 1, status: 1) }
+ let(:other_pending_record2) { deleted_records.create!(id: 4, fully_qualified_table_name: 'public.ci_builds', primary_key_value: 1, status: 1) }
+ let(:processed_record1) { deleted_records.create!(id: 5, fully_qualified_table_name: 'public.external_pull_requests', primary_key_value: 3, status: 2) }
+ let(:other_processed_record1) { deleted_records.create!(id: 6, fully_qualified_table_name: 'public.ci_builds', primary_key_value: 2, status: 2) }
+
+ let!(:persisted_ids_before) do
+ [
+ pending_record1,
+ pending_record2,
+ other_pending_record1,
+ other_pending_record2,
+ processed_record1,
+ other_processed_record1
+ ].map(&:id).sort
+ end
+
+ let!(:persisted_ids_after) do
+ [
+ other_pending_record1,
+ other_pending_record2,
+ processed_record1,
+ other_processed_record1
+ ].map(&:id).sort
+ end
+
+ def all_ids
+ deleted_records.all.map(&:id).sort
+ end
+
+ it 'deletes pending external_pull_requests records' do
+ expect { migrate! }.to change { all_ids }.from(persisted_ids_before).to(persisted_ids_after)
+ end
+end
diff --git a/spec/migrations/20220310141349_remove_dependency_list_usage_data_from_redis_spec.rb b/spec/migrations/20220310141349_remove_dependency_list_usage_data_from_redis_spec.rb
new file mode 100644
index 00000000000..5d9be79e768
--- /dev/null
+++ b/spec/migrations/20220310141349_remove_dependency_list_usage_data_from_redis_spec.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe RemoveDependencyListUsageDataFromRedis, :migration, :clean_gitlab_redis_shared_state,
+ feature_category: :dependency_management do
+ let(:key) { "DEPENDENCY_LIST_USAGE_COUNTER" }
+
+ describe "#up" do
+ it 'removes the hash from redis' do
+ with_redis do |redis|
+ redis.hincrby(key, 1, 1)
+ redis.hincrby(key, 2, 1)
+ end
+
+ expect { migrate! }.to change { with_redis { |r| r.hgetall(key) } }.from({ '1' => '1', '2' => '1' }).to({})
+ end
+ end
+
+ def with_redis(&block)
+ Gitlab::Redis::SharedState.with(&block)
+ end
+end
diff --git a/spec/migrations/backfill_all_project_namespaces_spec.rb b/spec/migrations/backfill_all_project_namespaces_spec.rb
new file mode 100644
index 00000000000..52fa46eea57
--- /dev/null
+++ b/spec/migrations/backfill_all_project_namespaces_spec.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe BackfillAllProjectNamespaces, :migration, feature_category: :subgroups do
+ let!(:migration) { described_class::MIGRATION }
+
+ let(:projects) { table(:projects) }
+ let(:namespaces) { table(:namespaces) }
+ let(:user_namespace) { namespaces.create!(name: 'user1', path: 'user1', visibility_level: 20, type: 'User') }
+ let(:parent_group1) { namespaces.create!(name: 'parent_group1', path: 'parent_group1', visibility_level: 20, type: 'Group') }
+ let!(:parent_group1_project) { projects.create!(name: 'parent_group1_project', path: 'parent_group1_project', namespace_id: parent_group1.id, visibility_level: 20) }
+ let!(:user_namespace_project) { projects.create!(name: 'user1_project', path: 'user1_project', namespace_id: user_namespace.id, visibility_level: 20) }
+
+ describe '#up' do
+ it 'schedules background jobs for each batch of namespaces' do
+ migrate!
+
+ expect(migration).to have_scheduled_batched_migration(
+ table_name: :projects,
+ column_name: :id,
+ job_arguments: [nil, 'up'],
+ interval: described_class::DELAY_INTERVAL
+ )
+ end
+ end
+
+ describe '#down' do
+ it 'deletes all batched migration records' do
+ migrate!
+ schema_migrate_down!
+
+ expect(migration).not_to have_scheduled_batched_migration
+ end
+ end
+end
diff --git a/spec/migrations/backfill_cycle_analytics_aggregations_spec.rb b/spec/migrations/backfill_cycle_analytics_aggregations_spec.rb
new file mode 100644
index 00000000000..47950f918c3
--- /dev/null
+++ b/spec/migrations/backfill_cycle_analytics_aggregations_spec.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe BackfillCycleAnalyticsAggregations, :migration, feature_category: :value_stream_management do
+ let(:migration) { described_class.new }
+
+ let(:aggregations) { table(:analytics_cycle_analytics_aggregations) }
+ let(:namespaces) { table(:namespaces) }
+ let(:group_value_streams) { table(:analytics_cycle_analytics_group_value_streams) }
+
+ context 'when there are value stream records' do
+ it 'inserts a record for each top-level namespace' do
+ group1 = namespaces.create!(path: 'aaa', name: 'aaa')
+ subgroup1 = namespaces.create!(path: 'bbb', name: 'bbb', parent_id: group1.id)
+ group2 = namespaces.create!(path: 'ccc', name: 'ccc')
+
+ namespaces.create!(path: 'ddd', name: 'ddd') # not used
+
+ group_value_streams.create!(name: 'for top level group', group_id: group2.id)
+ group_value_streams.create!(name: 'another for top level group', group_id: group2.id)
+
+ group_value_streams.create!(name: 'for subgroup', group_id: subgroup1.id)
+ group_value_streams.create!(name: 'another for subgroup', group_id: subgroup1.id)
+
+ migrate!
+
+ expect(aggregations.pluck(:group_id)).to match_array([group1.id, group2.id])
+ end
+ end
+
+ it 'does nothing' do
+ expect { migrate! }.not_to change { aggregations.count }
+ end
+end
diff --git a/spec/migrations/backfill_group_features_spec.rb b/spec/migrations/backfill_group_features_spec.rb
new file mode 100644
index 00000000000..1e7729a97d8
--- /dev/null
+++ b/spec/migrations/backfill_group_features_spec.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe BackfillGroupFeatures, :migration, feature_category: :feature_flags do
+ let(:migration) { described_class::MIGRATION }
+
+ describe '#up' do
+ it 'schedules background jobs for each batch of namespaces' do
+ migrate!
+
+ expect(migration).to have_scheduled_batched_migration(
+ table_name: :namespaces,
+ column_name: :id,
+ job_arguments: [described_class::BATCH_SIZE],
+ interval: described_class::INTERVAL,
+ batch_size: described_class::BATCH_SIZE
+ )
+ end
+ end
+
+ describe '#down' do
+ it 'deletes all batched migration records' do
+ migrate!
+ schema_migrate_down!
+
+ expect(migration).not_to have_scheduled_batched_migration
+ end
+ end
+end
diff --git a/spec/migrations/backfill_member_namespace_id_for_group_members_spec.rb b/spec/migrations/backfill_member_namespace_id_for_group_members_spec.rb
new file mode 100644
index 00000000000..892589dd770
--- /dev/null
+++ b/spec/migrations/backfill_member_namespace_id_for_group_members_spec.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe BackfillMemberNamespaceIdForGroupMembers, feature_category: :subgroups do
+ let!(:migration) { described_class::MIGRATION }
+
+ describe '#up' do
+ it 'schedules background jobs for each batch of group members' do
+ migrate!
+
+ expect(migration).to have_scheduled_batched_migration(
+ table_name: :members,
+ column_name: :id,
+ interval: described_class::INTERVAL
+ )
+ end
+ end
+
+ describe '#down' do
+ it 'deletes all batched migration records' do
+ migrate!
+ schema_migrate_down!
+
+ expect(migration).not_to have_scheduled_batched_migration
+ end
+ end
+end
diff --git a/spec/migrations/backfill_namespace_id_for_namespace_routes_spec.rb b/spec/migrations/backfill_namespace_id_for_namespace_routes_spec.rb
new file mode 100644
index 00000000000..627b18cd889
--- /dev/null
+++ b/spec/migrations/backfill_namespace_id_for_namespace_routes_spec.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe BackfillNamespaceIdForNamespaceRoutes, feature_category: :projects do
+ let!(:migration) { described_class::MIGRATION }
+
+ describe '#up' do
+ it 'schedules background jobs for each batch of routes' do
+ migrate!
+
+ expect(migration).to have_scheduled_batched_migration(
+ table_name: :routes,
+ column_name: :id,
+ interval: described_class::INTERVAL
+ )
+ end
+ end
+
+ describe '#down' do
+ it 'deletes all batched migration records' do
+ migrate!
+ schema_migrate_down!
+
+ expect(migration).not_to have_scheduled_batched_migration
+ end
+ end
+end
diff --git a/spec/migrations/backfill_project_namespaces_for_group_spec.rb b/spec/migrations/backfill_project_namespaces_for_group_spec.rb
new file mode 100644
index 00000000000..b21ed6e1aa2
--- /dev/null
+++ b/spec/migrations/backfill_project_namespaces_for_group_spec.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe BackfillProjectNamespacesForGroup, feature_category: :subgroups do
+ let!(:migration) { described_class::MIGRATION }
+
+ let(:projects) { table(:projects) }
+ let(:namespaces) { table(:namespaces) }
+ let(:parent_group1) { namespaces.create!(name: 'parent_group1', path: 'parent_group1', visibility_level: 20, type: 'Group') }
+ let!(:parent_group1_project) { projects.create!(name: 'parent_group1_project', path: 'parent_group1_project', namespace_id: parent_group1.id, visibility_level: 20) }
+
+ before do
+ allow(Gitlab).to receive(:com?).and_return(true)
+ end
+
+ describe '#up' do
+ before do
+ stub_const("BackfillProjectNamespacesForGroup::GROUP_ID", parent_group1.id)
+ end
+
+ it 'schedules background jobs for each batch of namespaces' do
+ migrate!
+
+ expect(migration).to have_scheduled_batched_migration(
+ table_name: :projects,
+ column_name: :id,
+ job_arguments: [described_class::GROUP_ID, 'up'],
+ interval: described_class::DELAY_INTERVAL
+ )
+ end
+ end
+
+ describe '#down' do
+ it 'deletes all batched migration records' do
+ migrate!
+ schema_migrate_down!
+
+ expect(migration).not_to have_scheduled_batched_migration
+ end
+ end
+end
diff --git a/spec/migrations/populate_audit_event_streaming_verification_token_spec.rb b/spec/migrations/populate_audit_event_streaming_verification_token_spec.rb
new file mode 100644
index 00000000000..e2c117903d4
--- /dev/null
+++ b/spec/migrations/populate_audit_event_streaming_verification_token_spec.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe PopulateAuditEventStreamingVerificationToken, feature_category: :audit_events do
+ let(:groups) { table(:namespaces) }
+ let(:destinations) { table(:audit_events_external_audit_event_destinations) }
+ let(:migration) { described_class.new }
+
+ let!(:group) { groups.create!(name: 'test-group', path: 'test-group') }
+ let!(:destination) { destinations.create!(namespace_id: group.id, destination_url: 'https://example.com/destination', verification_token: nil) }
+
+ describe '#up' do
+ it 'adds verification tokens to records created before the migration' do
+ expect do
+ migrate!
+ destination.reload
+ end.to change { destination.verification_token }.from(nil).to(a_string_matching(/\w{24}/))
+ end
+ end
+end
diff --git a/spec/migrations/recreate_index_security_ci_builds_on_name_and_id_parser_with_new_features_spec.rb b/spec/migrations/recreate_index_security_ci_builds_on_name_and_id_parser_with_new_features_spec.rb
new file mode 100644
index 00000000000..c7709764727
--- /dev/null
+++ b/spec/migrations/recreate_index_security_ci_builds_on_name_and_id_parser_with_new_features_spec.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+require_migration!
+
+RSpec.describe RecreateIndexSecurityCiBuildsOnNameAndIdParserWithNewFeatures, :migration, feature_category: :continuous_integration do
+ let(:db) { described_class.new }
+ let(:pg_class) { table(:pg_class) }
+ let(:pg_index) { table(:pg_index) }
+ let(:async_indexes) { table(:postgres_async_indexes) }
+
+ it 'recreates index' do
+ reversible_migration do |migration|
+ migration.before -> {
+ expect(async_indexes.where(name: described_class::OLD_INDEX_NAME).exists?).to be false
+ expect(db.index_exists?(described_class::TABLE, described_class::COLUMNS, name: described_class::OLD_INDEX_NAME)).to be true
+ expect(db.index_exists?(described_class::TABLE, described_class::COLUMNS, name: described_class::NEW_INDEX_NAME)).to be false
+ }
+
+ migration.after -> {
+ expect(async_indexes.where(name: described_class::OLD_INDEX_NAME).exists?).to be true
+ expect(db.index_exists?(described_class::TABLE, described_class::COLUMNS, name: described_class::OLD_INDEX_NAME)).to be false
+ expect(db.index_exists?(described_class::TABLE, described_class::COLUMNS, name: described_class::NEW_INDEX_NAME)).to be true
+ }
+ end
+ end
+end
diff --git a/spec/migrations/remove_not_null_contraint_on_title_from_sprints_spec.rb b/spec/migrations/remove_not_null_contraint_on_title_from_sprints_spec.rb
new file mode 100644
index 00000000000..91687d8d730
--- /dev/null
+++ b/spec/migrations/remove_not_null_contraint_on_title_from_sprints_spec.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+require_migration!
+
+RSpec.describe RemoveNotNullContraintOnTitleFromSprints, :migration, feature_category: :team_planning do
+ let(:migration) { described_class.new }
+ let(:namespaces) { table(:namespaces) }
+ let(:sprints) { table(:sprints) }
+ let(:iterations_cadences) { table(:iterations_cadences) }
+
+ let!(:group) { namespaces.create!(name: 'foo', path: 'foo') }
+ let!(:cadence) { iterations_cadences.create!(group_id: group.id, title: "cadence 1") }
+ let!(:iteration1) { sprints.create!(id: 1, title: 'a', group_id: group.id, iterations_cadence_id: cadence.id, start_date: Date.new(2021, 11, 1), due_date: Date.new(2021, 11, 5), iid: 1) }
+
+ describe '#down' do
+ it "removes null titles by setting them with ids" do
+ migration.up
+
+ iteration2 = sprints.create!(id: 2, title: nil, group_id: group.id, iterations_cadence_id: cadence.id, start_date: Date.new(2021, 12, 1), due_date: Date.new(2021, 12, 5), iid: 2)
+
+ migration.down
+
+ expect(iteration1.reload.title).to eq 'a'
+ expect(iteration2.reload.title).to eq '2'
+ end
+ end
+end
diff --git a/spec/migrations/schedule_fix_incorrect_max_seats_used2_spec.rb b/spec/migrations/schedule_fix_incorrect_max_seats_used2_spec.rb
new file mode 100644
index 00000000000..26764f855b7
--- /dev/null
+++ b/spec/migrations/schedule_fix_incorrect_max_seats_used2_spec.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe ScheduleFixIncorrectMaxSeatsUsed2, :migration, feature_category: :purchase do
+ let(:migration_name) { described_class::MIGRATION.to_s.demodulize }
+
+ describe '#up' do
+ it 'schedules a job on Gitlab.com' do
+ allow(Gitlab).to receive(:com?).and_return(true)
+
+ Sidekiq::Testing.fake! do
+ freeze_time do
+ migrate!
+
+ expect(migration_name).to be_scheduled_delayed_migration(1.hour, 'batch_2_for_start_date_before_02_aug_2021')
+ expect(BackgroundMigrationWorker.jobs.size).to eq(1)
+ end
+ end
+ end
+
+ it 'does not schedule any jobs when not Gitlab.com' do
+ allow(Gitlab).to receive(:com?).and_return(false)
+
+ Sidekiq::Testing.fake! do
+ migrate!
+
+ expect(migration_name).not_to be_scheduled_delayed_migration
+ expect(BackgroundMigrationWorker.jobs.size).to eq(0)
+ end
+ end
+ end
+end
diff --git a/spec/migrations/schedule_fix_incorrect_max_seats_used_spec.rb b/spec/migrations/schedule_fix_incorrect_max_seats_used_spec.rb
new file mode 100644
index 00000000000..194a1d39ad1
--- /dev/null
+++ b/spec/migrations/schedule_fix_incorrect_max_seats_used_spec.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe ScheduleFixIncorrectMaxSeatsUsed, :migration, feature_category: :purchase do
+ let(:migration) { described_class.new }
+
+ describe '#up' do
+ it 'schedules a job on Gitlab.com' do
+ allow(Gitlab).to receive(:com?).and_return(true)
+
+ expect(migration).to receive(:migrate_in).with(1.hour, 'FixIncorrectMaxSeatsUsed')
+
+ migration.up
+ end
+
+ it 'does not schedule any jobs when not Gitlab.com' do
+ allow(Gitlab::CurrentSettings).to receive(:com?).and_return(false)
+
+ expect(migration).not_to receive(:migrate_in)
+
+ migration.up
+ end
+ end
+end
diff --git a/spec/migrations/schedule_update_timelogs_null_spent_at_spec.rb b/spec/migrations/schedule_update_timelogs_null_spent_at_spec.rb
new file mode 100644
index 00000000000..99ee9e58f4e
--- /dev/null
+++ b/spec/migrations/schedule_update_timelogs_null_spent_at_spec.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe ScheduleUpdateTimelogsNullSpentAt, feature_category: :team_planning do
+ let!(:namespace) { table(:namespaces).create!(name: 'namespace', path: 'namespace') }
+ let!(:project) { table(:projects).create!(namespace_id: namespace.id) }
+ let!(:issue) { table(:issues).create!(project_id: project.id) }
+ let!(:merge_request) { table(:merge_requests).create!(target_project_id: project.id, source_branch: 'master', target_branch: 'feature') }
+ let!(:timelog1) { create_timelog!(merge_request_id: merge_request.id) }
+ let!(:timelog2) { create_timelog!(merge_request_id: merge_request.id) }
+ let!(:timelog3) { create_timelog!(merge_request_id: merge_request.id) }
+ let!(:timelog4) { create_timelog!(issue_id: issue.id) }
+ let!(:timelog5) { create_timelog!(issue_id: issue.id) }
+
+ before do
+ table(:timelogs).where.not(id: timelog3.id).update_all(spent_at: nil)
+ end
+
+ it 'correctly schedules background migrations' do
+ stub_const("#{described_class}::BATCH_SIZE", 2)
+
+ Sidekiq::Testing.fake! do
+ freeze_time do
+ migrate!
+
+ expect(described_class::MIGRATION)
+ .to be_scheduled_delayed_migration(2.minutes, timelog1.id, timelog2.id)
+
+ expect(described_class::MIGRATION)
+ .to be_scheduled_delayed_migration(4.minutes, timelog4.id, timelog5.id)
+
+ expect(BackgroundMigrationWorker.jobs.size).to eq(2)
+ end
+ end
+ end
+
+ private
+
+ def create_timelog!(**args)
+ table(:timelogs).create!(**args, time_spent: 1)
+ end
+end
diff --git a/spec/migrations/start_backfill_ci_queuing_tables_spec.rb b/spec/migrations/start_backfill_ci_queuing_tables_spec.rb
new file mode 100644
index 00000000000..0a189b58c94
--- /dev/null
+++ b/spec/migrations/start_backfill_ci_queuing_tables_spec.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe StartBackfillCiQueuingTables, :suppress_gitlab_schemas_validate_connection,
+ feature_category: :continuous_integration do
+ let(:namespaces) { table(:namespaces) }
+ let(:projects) { table(:projects) }
+ let(:builds) { table(:ci_builds) }
+
+ let!(:namespace) do
+ namespaces.create!(name: 'namespace1', path: 'namespace1')
+ end
+
+ let!(:project) do
+ projects.create!(namespace_id: namespace.id, name: 'test1', path: 'test1')
+ end
+
+ let!(:pending_build_1) do
+ builds.create!(status: :pending, name: 'test1', type: 'Ci::Build', project_id: project.id)
+ end
+
+ let!(:running_build) do
+ builds.create!(status: :running, name: 'test2', type: 'Ci::Build', project_id: project.id)
+ end
+
+ let!(:pending_build_2) do
+ builds.create!(status: :pending, name: 'test3', type: 'Ci::Build', project_id: project.id)
+ end
+
+ before do
+ stub_const("#{described_class.name}::BATCH_SIZE", 1)
+ end
+
+ it 'schedules jobs for builds that are pending' do
+ Sidekiq::Testing.fake! do
+ freeze_time do
+ migrate!
+
+ expect(described_class::MIGRATION).to be_scheduled_delayed_migration(
+ 2.minutes, pending_build_1.id, pending_build_1.id)
+ expect(described_class::MIGRATION).to be_scheduled_delayed_migration(
+ 4.minutes, pending_build_2.id, pending_build_2.id)
+ expect(BackgroundMigrationWorker.jobs.size).to eq(2)
+ end
+ end
+ end
+end
diff --git a/spec/migrations/update_application_settings_container_registry_exp_pol_worker_capacity_default_spec.rb b/spec/migrations/update_application_settings_container_registry_exp_pol_worker_capacity_default_spec.rb
new file mode 100644
index 00000000000..66da9e6653d
--- /dev/null
+++ b/spec/migrations/update_application_settings_container_registry_exp_pol_worker_capacity_default_spec.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe UpdateApplicationSettingsContainerRegistryExpPolWorkerCapacityDefault,
+ feature_category: :container_registry do
+ let(:settings) { table(:application_settings) }
+
+ context 'with no rows in the application_settings table' do
+ it 'does not insert a row' do
+ expect { migrate! }.to not_change { settings.count }
+ end
+ end
+
+ context 'with a row in the application_settings table' do
+ before do
+ settings.create!(container_registry_expiration_policies_worker_capacity: capacity)
+ end
+
+ context 'with container_registry_expiration_policy_worker_capacity set to a value different than 0' do
+ let(:capacity) { 1 }
+
+ it 'does not update the row' do
+ expect { migrate! }
+ .to not_change { settings.count }
+ .and not_change { settings.first.container_registry_expiration_policies_worker_capacity }
+ end
+ end
+
+ context 'with container_registry_expiration_policy_worker_capacity set to 0' do
+ let(:capacity) { 0 }
+
+ it 'updates the existing row' do
+ expect { migrate! }
+ .to not_change { settings.count }
+ .and change { settings.first.container_registry_expiration_policies_worker_capacity }.from(0).to(4)
+ end
+ end
+ end
+end
diff --git a/spec/migrations/update_application_settings_protected_paths_spec.rb b/spec/migrations/update_application_settings_protected_paths_spec.rb
new file mode 100644
index 00000000000..c2bd4e8727d
--- /dev/null
+++ b/spec/migrations/update_application_settings_protected_paths_spec.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe UpdateApplicationSettingsProtectedPaths, :aggregate_failures,
+ feature_category: :system_access do
+ subject(:migration) { described_class.new }
+
+ let!(:application_settings) { table(:application_settings) }
+ let!(:oauth_paths) { %w[/oauth/authorize /oauth/token] }
+ let!(:custom_paths) { %w[/foo /bar] }
+
+ let(:default_paths) { application_settings.column_defaults.fetch('protected_paths') }
+
+ before do
+ application_settings.create!(protected_paths: custom_paths)
+ application_settings.create!(protected_paths: custom_paths + oauth_paths)
+ application_settings.create!(protected_paths: custom_paths + oauth_paths.take(1))
+ end
+
+ describe '#up' do
+ before do
+ migrate!
+ application_settings.reset_column_information
+ end
+
+ it 'removes the OAuth paths from the default value and persisted records' do
+ expect(default_paths).not_to include(*oauth_paths)
+ expect(default_paths).to eq(described_class::NEW_DEFAULT_PROTECTED_PATHS)
+ expect(application_settings.all).to all(have_attributes(protected_paths: custom_paths))
+ end
+ end
+
+ describe '#down' do
+ before do
+ migrate!
+ schema_migrate_down!
+ end
+
+ it 'adds the OAuth paths to the default value and persisted records' do
+ expect(default_paths).to include(*oauth_paths)
+ expect(default_paths).to eq(described_class::OLD_DEFAULT_PROTECTED_PATHS)
+ expect(application_settings.all).to all(have_attributes(protected_paths: custom_paths + oauth_paths))
+ end
+ end
+end
diff --git a/spec/migrations/update_default_scan_method_of_dast_site_profile_spec.rb b/spec/migrations/update_default_scan_method_of_dast_site_profile_spec.rb
new file mode 100644
index 00000000000..15a8e79a610
--- /dev/null
+++ b/spec/migrations/update_default_scan_method_of_dast_site_profile_spec.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe UpdateDefaultScanMethodOfDastSiteProfile, feature_category: :dynamic_application_security_testing do
+ let(:namespaces) { table(:namespaces) }
+ let(:projects) { table(:projects) }
+ let(:dast_sites) { table(:dast_sites) }
+ let(:dast_site_profiles) { table(:dast_site_profiles) }
+
+ before do
+ namespace = namespaces.create!(name: 'test', path: 'test')
+ project = projects.create!(id: 12, namespace_id: namespace.id, name: 'gitlab', path: 'gitlab')
+ dast_site = dast_sites.create!(id: 1, url: 'https://www.gitlab.com', project_id: project.id)
+
+ dast_site_profiles.create!(
+ id: 1,
+ project_id: project.id,
+ dast_site_id: dast_site.id,
+ name: "#{FFaker::Product.product_name.truncate(192)} #{SecureRandom.hex(4)} - 0",
+ scan_method: 0,
+ target_type: 0
+ )
+
+ dast_site_profiles.create!(
+ id: 2,
+ project_id: project.id,
+ dast_site_id: dast_site.id,
+ name: "#{FFaker::Product.product_name.truncate(192)} #{SecureRandom.hex(4)} - 1",
+ scan_method: 0,
+ target_type: 1
+ )
+ end
+
+ it 'updates the scan_method to 1 for profiles with target_type 1' do
+ migrate!
+
+ expect(dast_site_profiles.where(scan_method: 1).count).to eq 1
+ expect(dast_site_profiles.where(scan_method: 0).count).to eq 1
+ end
+end
diff --git a/spec/migrations/update_invalid_member_states_spec.rb b/spec/migrations/update_invalid_member_states_spec.rb
new file mode 100644
index 00000000000..6ae4b9f3c0f
--- /dev/null
+++ b/spec/migrations/update_invalid_member_states_spec.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+require_migration!
+
+RSpec.describe UpdateInvalidMemberStates, feature_category: :subgroups do
+ let(:members) { table(:members) }
+ let(:groups) { table(:namespaces) }
+ let(:projects) { table(:projects) }
+ let(:users) { table(:users) }
+
+ before do
+ user = users.create!(first_name: 'Test', last_name: 'User', email: 'test@user.com', projects_limit: 1)
+ group = groups.create!(name: 'gitlab', path: 'gitlab-org')
+ project = projects.create!(namespace_id: group.id)
+
+ members.create!(state: 2, source_id: group.id, source_type: 'Group', type: 'GroupMember', user_id: user.id, access_level: 50, notification_level: 0)
+ members.create!(state: 2, source_id: project.id, source_type: 'Project', type: 'ProjectMember', user_id: user.id, access_level: 50, notification_level: 0)
+ members.create!(state: 1, source_id: group.id, source_type: 'Group', type: 'GroupMember', user_id: user.id, access_level: 50, notification_level: 0)
+ members.create!(state: 0, source_id: group.id, source_type: 'Group', type: 'GroupMember', user_id: user.id, access_level: 50, notification_level: 0)
+ end
+
+ it 'updates matching member record states' do
+ expect { migrate! }
+ .to change { members.where(state: 0).count }.from(1).to(3)
+ .and change { members.where(state: 2).count }.from(2).to(0)
+ .and change { members.where(state: 1).count }.by(0)
+ end
+end