diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-12-20 16:37:47 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-12-20 16:37:47 +0300 |
commit | aee0a117a889461ce8ced6fcf73207fe017f1d99 (patch) | |
tree | 891d9ef189227a8445d83f35c1b0fc99573f4380 /spec/support/shared_examples | |
parent | 8d46af3258650d305f53b819eabf7ab18d22f59e (diff) |
Add latest changes from gitlab-org/gitlab@14-6-stable-eev14.6.0-rc42
Diffstat (limited to 'spec/support/shared_examples')
38 files changed, 823 insertions, 243 deletions
diff --git a/spec/support/shared_examples/bulk_imports/common/pipelines/wiki_pipeline_examples.rb b/spec/support/shared_examples/bulk_imports/common/pipelines/wiki_pipeline_examples.rb index e8cc666605b..06800f7cded 100644 --- a/spec/support/shared_examples/bulk_imports/common/pipelines/wiki_pipeline_examples.rb +++ b/spec/support/shared_examples/bulk_imports/common/pipelines/wiki_pipeline_examples.rb @@ -9,16 +9,18 @@ RSpec.shared_examples 'wiki pipeline imports a wiki for an entity' do let(:extracted_data) { BulkImports::Pipeline::ExtractedData.new(data: {}) } - context 'successfully imports wiki for an entity' do - subject { described_class.new(context) } + subject { described_class.new(context) } - before do - allow_next_instance_of(BulkImports::Common::Extractors::GraphqlExtractor) do |extractor| - allow(extractor).to receive(:extract).and_return(extracted_data) - end + before do + allow_next_instance_of(BulkImports::Common::Extractors::GraphqlExtractor) do |extractor| + allow(extractor).to receive(:extract).and_return(extracted_data) end + end + context 'when wiki exists' do it 'imports new wiki into destination project' do + expect(subject).to receive(:source_wiki_exists?).and_return(true) + expect_next_instance_of(Gitlab::GitalyClient::RepositoryService) do |repository_service| url = "https://oauth2:token@gitlab.example/#{entity.source_full_path}.wiki.git" expect(repository_service).to receive(:fetch_remote).with(url, any_args).and_return 0 @@ -27,5 +29,16 @@ RSpec.shared_examples 'wiki pipeline imports a wiki for an entity' do subject.run end end + + context 'when wiki does not exist' do + it 'does not import wiki' do + expect(subject).to receive(:source_wiki_exists?).and_return(false) + + expect(parent.wiki).not_to receive(:ensure_repository) + expect(parent.wiki.repository).not_to receive(:ensure_repository) + + expect { subject.run }.not_to raise_error + end + end end end diff --git a/spec/support/shared_examples/ci/create_pipeline_service_shared_examples.rb b/spec/support/shared_examples/ci/create_pipeline_service_shared_examples.rb new file mode 100644 index 00000000000..a72ce320e90 --- /dev/null +++ b/spec/support/shared_examples/ci/create_pipeline_service_shared_examples.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'pipelines are created without N+1 SQL queries' do + before do + # warm up + stub_ci_pipeline_yaml_file(config1) + execute_service + end + + it 'avoids N+1 queries', :aggregate_failures, :request_store, :use_sql_query_cache do + control = ActiveRecord::QueryRecorder.new(skip_cached: false) do + stub_ci_pipeline_yaml_file(config1) + + pipeline = execute_service.payload + + expect(pipeline).to be_created_successfully + end + + expect do + stub_ci_pipeline_yaml_file(config2) + + pipeline = execute_service.payload + + expect(pipeline).to be_created_successfully + end.not_to exceed_all_query_limit(control).with_threshold(accepted_n_plus_ones) + end +end diff --git a/spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb b/spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb index 0ffa32dec9e..46fc2cbdc9b 100644 --- a/spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb +++ b/spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb @@ -58,11 +58,12 @@ RSpec.shared_examples 'a GitHub-ish import controller: GET new' do end RSpec.shared_examples 'a GitHub-ish import controller: GET status' do + let(:repo_fake) { Struct.new(:id, :login, :full_name, :name, :owner, keyword_init: true) } let(:new_import_url) { public_send("new_import_#{provider}_url") } let(:user) { create(:user) } - let(:repo) { OpenStruct.new(login: 'vim', full_name: 'asd/vim', name: 'vim', owner: { login: 'owner' }) } - let(:org) { OpenStruct.new(login: 'company') } - let(:org_repo) { OpenStruct.new(login: 'company', full_name: 'company/repo', name: 'repo', owner: { login: 'owner' }) } + let(:repo) { repo_fake.new(login: 'vim', full_name: 'asd/vim', name: 'vim', owner: { login: 'owner' }) } + let(:org) { double('org', login: 'company') } + let(:org_repo) { repo_fake.new(login: 'company', full_name: 'company/repo', name: 'repo', owner: { login: 'owner' }) } before do assign_session_token(provider) @@ -72,7 +73,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: GET status' do project = create(:project, import_type: provider, namespace: user.namespace, import_status: :finished, import_source: 'example/repo') group = create(:group) group.add_owner(user) - stub_client(repos: [repo, org_repo], orgs: [org], org_repos: [org_repo], each_page: [OpenStruct.new(objects: [repo, org_repo])].to_enum) + stub_client(repos: [repo, org_repo], orgs: [org], org_repos: [org_repo], each_page: [double('client', objects: [repo, org_repo])].to_enum) get :status, format: :json @@ -125,7 +126,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: GET status' do end context 'when filtering' do - let(:repo_2) { OpenStruct.new(login: 'emacs', full_name: 'asd/emacs', name: 'emacs', owner: { login: 'owner' }) } + let(:repo_2) { repo_fake.new(login: 'emacs', full_name: 'asd/emacs', name: 'emacs', owner: { login: 'owner' }) } let(:project) { create(:project, import_type: provider, namespace: user.namespace, import_status: :finished, import_source: 'example/repo') } let(:group) { create(:group) } let(:repos) { [repo, repo_2, org_repo] } @@ -133,7 +134,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: GET status' do before do group.add_owner(user) client = stub_client(repos: repos, orgs: [org], org_repos: [org_repo]) - allow(client).to receive(:each_page).and_return([OpenStruct.new(objects: repos)].to_enum) + allow(client).to receive(:each_page).and_return([double('client', objects: repos)].to_enum) # GitHub controller has filtering done using GitHub Search API stub_feature_flags(remove_legacy_github_client: false) end @@ -172,7 +173,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: GET status' do repos = [build(:project, name: 2, path: 'test')] client = stub_client(repos: repos) - allow(client).to receive(:each_page).and_return([OpenStruct.new(objects: repos)].to_enum) + allow(client).to receive(:each_page).and_return([double('client', objects: repos)].to_enum) end it 'does not raise an error' do @@ -189,13 +190,14 @@ end RSpec.shared_examples 'a GitHub-ish import controller: POST create' do let(:user) { create(:user) } let(:provider_username) { user.username } - let(:provider_user) { OpenStruct.new(login: provider_username) } + let(:provider_user) { double('user', login: provider_username) } let(:project) { create(:project, import_type: provider, import_status: :finished, import_source: "#{provider_username}/vim") } let(:provider_repo) do - OpenStruct.new( + double( + 'provider', name: 'vim', full_name: "#{provider_username}/vim", - owner: OpenStruct.new(login: provider_username) + owner: double('owner', login: provider_username) ) end @@ -265,10 +267,9 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do end context "when the repository owner is not the provider user" do - let(:other_username) { "someone_else" } + let(:provider_username) { "someone_else" } before do - provider_repo.owner = OpenStruct.new(login: other_username) assign_session_token(provider) end @@ -277,8 +278,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do context "when the namespace is owned by the GitLab user" do before do - user.username = other_username - user.save! + user.update!(username: provider_username) end it "takes the existing namespace" do @@ -292,7 +292,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do context "when the namespace is not owned by the GitLab user" do it "creates a project using user's namespace" do - create(:user, username: other_username) + create(:user, username: provider_username) expect(Gitlab::LegacyGithubImport::ProjectCreator) .to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, type: provider, **access_params) diff --git a/spec/support/shared_examples/controllers/repositories/git_http_controller_shared_examples.rb b/spec/support/shared_examples/controllers/repositories/git_http_controller_shared_examples.rb index 00a0fb7e4c5..3a7588a5cc9 100644 --- a/spec/support/shared_examples/controllers/repositories/git_http_controller_shared_examples.rb +++ b/spec/support/shared_examples/controllers/repositories/git_http_controller_shared_examples.rb @@ -50,7 +50,8 @@ RSpec.shared_examples Repositories::GitHttpController do context 'with authorized user' do before do - request.headers.merge! auth_env(user.username, user.password, nil) + password = user.try(:password) || user.try(:token) + request.headers.merge! auth_env(user.username, password, nil) end it 'returns 200' do @@ -71,9 +72,10 @@ RSpec.shared_examples Repositories::GitHttpController do it 'adds user info to the logs' do get :info_refs, params: params - expect(log_data).to include('username' => user.username, - 'user_id' => user.id, - 'meta.user' => user.username) + user_log_data = { 'username' => user.username, 'user_id' => user.id } + user_log_data['meta.user'] = user.username if user.is_a?(User) + + expect(log_data).to include(user_log_data) end end end diff --git a/spec/support/shared_examples/controllers/unique_visits_shared_examples.rb b/spec/support/shared_examples/controllers/unique_visits_shared_examples.rb index 30914e61df0..ac7680f7ddb 100644 --- a/spec/support/shared_examples/controllers/unique_visits_shared_examples.rb +++ b/spec/support/shared_examples/controllers/unique_visits_shared_examples.rb @@ -6,15 +6,23 @@ RSpec.shared_examples 'tracking unique visits' do |method| let(:request_params) { {} } it 'tracks unique visit if the format is HTML' do - expect(Gitlab::UsageDataCounters::HLLRedisCounter) - .to receive(:track_event).with(target_id, values: kind_of(String)) + ids = target_id.instance_of?(String) ? [target_id] : target_id + + ids.each do |id| + expect(Gitlab::UsageDataCounters::HLLRedisCounter) + .to receive(:track_event).with(id, values: kind_of(String)) + end get method, params: request_params, format: :html end it 'tracks unique visit if DNT is not enabled' do - expect(Gitlab::UsageDataCounters::HLLRedisCounter) - .to receive(:track_event).with(target_id, values: kind_of(String)) + ids = target_id.instance_of?(String) ? [target_id] : target_id + + ids.each do |id| + expect(Gitlab::UsageDataCounters::HLLRedisCounter) + .to receive(:track_event).with(id, values: kind_of(String)) + end stub_do_not_track('0') diff --git a/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb b/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb index 30710e43357..1cb52c07069 100644 --- a/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb +++ b/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb @@ -299,7 +299,7 @@ RSpec.shared_examples 'wiki controller actions' do expect(response.headers['Content-Disposition']).to match(/^inline/) expect(response.headers[Gitlab::Workhorse::DETECT_HEADER]).to eq('true') expect(response.cache_control[:public]).to be(false) - expect(response.headers['Cache-Control']).to eq('private, no-store') + expect(response.headers['Cache-Control']).to eq('max-age=60, private') end end end diff --git a/spec/support/shared_examples/csp.rb b/spec/support/shared_examples/csp.rb index c4a8c7df898..9143d0f4720 100644 --- a/spec/support/shared_examples/csp.rb +++ b/spec/support/shared_examples/csp.rb @@ -28,7 +28,7 @@ RSpec.shared_examples 'setting CSP' do |rule_name| context 'when feature is enabled' do it "appends to #{rule_name}" do - is_expected.to eql("#{rule_name} #{default_csp_values} #{whitelisted_url}") + is_expected.to eql("#{rule_name} #{default_csp_values} #{allowlisted_url}") end end @@ -46,7 +46,7 @@ RSpec.shared_examples 'setting CSP' do |rule_name| context 'when feature is enabled' do it "uses default-src values in #{rule_name}" do - is_expected.to eql("default-src #{default_csp_values}; #{rule_name} #{default_csp_values} #{whitelisted_url}") + is_expected.to eql("default-src #{default_csp_values}; #{rule_name} #{default_csp_values} #{allowlisted_url}") end end @@ -64,7 +64,7 @@ RSpec.shared_examples 'setting CSP' do |rule_name| context 'when feature is enabled' do it "uses default-src values in #{rule_name}" do - is_expected.to eql("font-src #{default_csp_values}; #{rule_name} #{whitelisted_url}") + is_expected.to eql("font-src #{default_csp_values}; #{rule_name} #{allowlisted_url}") end end diff --git a/spec/support/shared_examples/features/page_description_shared_examples.rb b/spec/support/shared_examples/features/page_description_shared_examples.rb index 81653220b4c..e3ea36633d1 100644 --- a/spec/support/shared_examples/features/page_description_shared_examples.rb +++ b/spec/support/shared_examples/features/page_description_shared_examples.rb @@ -7,3 +7,13 @@ RSpec.shared_examples 'page meta description' do |expected_description| end end end + +RSpec.shared_examples 'default brand title page meta description' do + include AppearancesHelper + + it 'renders the page with description, og:description, and twitter:description meta tags with the default brand title', :aggregate_failures do + %w(name='description' property='og:description' property='twitter:description').each do |selector| + expect(page).to have_selector("meta[#{selector}][content='#{default_brand_title}']", visible: false) + end + end +end diff --git a/spec/support/shared_examples/features/sidebar/sidebar_due_date_shared_examples.rb b/spec/support/shared_examples/features/sidebar/sidebar_due_date_shared_examples.rb new file mode 100644 index 00000000000..345dfbce423 --- /dev/null +++ b/spec/support/shared_examples/features/sidebar/sidebar_due_date_shared_examples.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'date sidebar widget' do + context 'editing due date' do + let(:due_date_value) { find('[data-testid="sidebar-due-date"] [data-testid="sidebar-date-value"]') } + + around do |example| + freeze_time { example.run } + end + + it 'displays "None" when there is no due date' do + expect(due_date_value.text).to have_content 'None' + end + + it 'updates due date' do + page.within('[data-testid="sidebar-due-date"]') do + today = Date.today.day + + click_button 'Edit' + + click_button today.to_s + + wait_for_requests + + expect(page).to have_content(today.to_s(:medium)) + expect(due_date_value.text).to have_content Time.current.strftime('%b %-d, %Y') + end + end + end +end diff --git a/spec/support/shared_examples/features/sidebar/sidebar_milestone_shared_examples.rb b/spec/support/shared_examples/features/sidebar/sidebar_milestone_shared_examples.rb new file mode 100644 index 00000000000..da730240e8e --- /dev/null +++ b/spec/support/shared_examples/features/sidebar/sidebar_milestone_shared_examples.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'milestone sidebar widget' do + context 'editing milestone' do + let_it_be(:milestone_expired) { create(:milestone, project: project, title: 'Foo - expired', due_date: 5.days.ago) } + let_it_be(:milestone_no_duedate) { create(:milestone, project: project, title: 'Foo - No due date') } + let_it_be(:milestone1) { create(:milestone, project: project, title: 'Milestone-1', due_date: 20.days.from_now) } + let_it_be(:milestone2) { create(:milestone, project: project, title: 'Milestone-2', due_date: 15.days.from_now) } + let_it_be(:milestone3) { create(:milestone, project: project, title: 'Milestone-3', due_date: 10.days.from_now) } + + let(:milestone_widget) { find('[data-testid="sidebar-milestones"]') } + + before do + within(milestone_widget) do + click_button 'Edit' + end + + wait_for_all_requests + end + + it 'shows milestones list in the dropdown' do + # 5 milestones + "No milestone" = 6 items + expect(milestone_widget.find('.gl-new-dropdown-contents')).to have_selector('li.gl-new-dropdown-item', count: 6) + end + + it 'shows expired milestone at the bottom of the list and milestone due earliest at the top of the list', :aggregate_failures do + within(milestone_widget, '.gl-new-dropdown-contents') do + expect(page.find('li:last-child')).to have_content milestone_expired.title + + [milestone3, milestone2, milestone1, milestone_no_duedate].each_with_index do |m, i| + expect(page.all('li.gl-new-dropdown-item')[i + 1]).to have_content m.title + end + end + end + + it 'adds a milestone' do + within(milestone_widget) do + click_button milestone1.title + + wait_for_requests + + page.within('[data-testid="select-milestone"]') do + expect(page).to have_content(milestone1.title) + end + end + end + + it 'removes a milestone' do + within(milestone_widget) do + click_button "No milestone" + + wait_for_requests + + page.within('[data-testid="select-milestone"]') do + expect(page).not_to have_content(milestone1.title) + end + end + end + end +end diff --git a/spec/support/shared_examples/features/sidebar_shared_examples.rb b/spec/support/shared_examples/features/sidebar_shared_examples.rb index d509d124de0..615f568420e 100644 --- a/spec/support/shared_examples/features/sidebar_shared_examples.rb +++ b/spec/support/shared_examples/features/sidebar_shared_examples.rb @@ -5,6 +5,7 @@ RSpec.shared_examples 'issue boards sidebar' do before do first_card.click + wait_for_requests end it 'shows sidebar when clicking issue' do @@ -41,6 +42,14 @@ RSpec.shared_examples 'issue boards sidebar' do end end + context 'editing issue milestone', :js do + it_behaves_like 'milestone sidebar widget' + end + + context 'editing issue due date', :js do + it_behaves_like 'date sidebar widget' + end + context 'in notifications subscription' do it 'displays notifications toggle', :aggregate_failures do page.within('[data-testid="sidebar-notifications"]') do diff --git a/spec/support/shared_examples/features/snippets_shared_examples.rb b/spec/support/shared_examples/features/snippets_shared_examples.rb index bd1a67f3bb5..c402333107c 100644 --- a/spec/support/shared_examples/features/snippets_shared_examples.rb +++ b/spec/support/shared_examples/features/snippets_shared_examples.rb @@ -20,7 +20,7 @@ RSpec.shared_examples 'paginated snippets' do |remote: false| end RSpec.shared_examples 'tabs with counts' do - let(:tabs) { page.all('.snippet-scope-menu li') } + let(:tabs) { page.all('.js-snippets-nav-tabs li') } it 'shows a tab for All snippets and count' do tab = tabs[0] diff --git a/spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb index 7ced8508a31..a456b76b324 100644 --- a/spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb +++ b/spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb @@ -138,11 +138,26 @@ RSpec.shared_examples 'User updates wiki page' do end context 'when using the content editor' do - before do - click_button 'Use the new editor' + context 'with feature flag on' do + before do + click_button 'Edit rich text' + end + + it_behaves_like 'edits content using the content editor' end - it_behaves_like 'edits content using the content editor' + context 'with feature flag off' do + before do + stub_feature_flags(wiki_switch_between_content_editor_raw_markdown: false) + visit(wiki_path(wiki)) + + click_link('Edit') + + click_button 'Use the new editor' + end + + it_behaves_like 'edits content using the content editor' + end end end diff --git a/spec/support/shared_examples/features/wiki/user_views_wiki_page_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_views_wiki_page_shared_examples.rb index 96df5a5f972..eec911f3b6f 100644 --- a/spec/support/shared_examples/features/wiki/user_views_wiki_page_shared_examples.rb +++ b/spec/support/shared_examples/features/wiki/user_views_wiki_page_shared_examples.rb @@ -161,7 +161,7 @@ RSpec.shared_examples 'User views a wiki page' do commit = wiki.commit visit wiki_page_path(wiki, wiki_page, version_id: commit, action: :diff) - expect(page).to have_content('by John Doe') + expect(page).to have_content('by Sidney Jones') expect(page).to have_content('updated home') expect(page).to have_content('Showing 1 changed file with 1 addition and 3 deletions') expect(page).to have_content('some link') @@ -174,7 +174,7 @@ RSpec.shared_examples 'User views a wiki page' do commit = wiki.commit('HEAD^') visit wiki_page_path(wiki, wiki_page, version_id: commit, action: :diff) - expect(page).to have_content('by John Doe') + expect(page).to have_content('by Sidney Jones') expect(page).to have_content('updated home') expect(page).to have_content('Showing 1 changed file with 1 addition and 3 deletions') expect(page).to have_content('some link') @@ -188,7 +188,7 @@ RSpec.shared_examples 'User views a wiki page' do commit = wiki.commit('HEAD^') visit wiki_page_path(wiki, wiki_page, version_id: commit, action: :diff) - expect(page).to have_content('by John Doe') + expect(page).to have_content('by Sidney Jones') expect(page).to have_content('created page: home') expect(page).to have_content('Showing 1 changed file with 4 additions and 0 deletions') expect(page).to have_content('Look at this') diff --git a/spec/support/shared_examples/lib/gitlab/background_migration/mentions_migration_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/background_migration/mentions_migration_shared_examples.rb deleted file mode 100644 index 7707e79386c..00000000000 --- a/spec/support/shared_examples/lib/gitlab/background_migration/mentions_migration_shared_examples.rb +++ /dev/null @@ -1,108 +0,0 @@ -# frozen_string_literal: true - -RSpec.shared_examples 'resource mentions migration' do |migration_class, resource_class_name| - it 'migrates resource mentions' do - join = migration_class::JOIN - conditions = migration_class::QUERY_CONDITIONS - resource_class = "#{Gitlab::BackgroundMigration::UserMentions::Models}::#{resource_class_name}".constantize - - expect do - subject.perform(resource_class_name, join, conditions, false, resource_class.minimum(:id), resource_class.maximum(:id)) - end.to change { user_mentions.count }.by(1) - - user_mention = user_mentions.last - expect(user_mention.mentioned_users_ids.sort).to eq(mentioned_users.pluck(:id).sort) - expect(user_mention.mentioned_groups_ids.sort).to eq([group.id]) - expect(user_mention.mentioned_groups_ids.sort).not_to include(inaccessible_group.id) - - # check that performing the same job twice does not fail and does not change counts - expect do - subject.perform(resource_class_name, join, conditions, false, resource_class.minimum(:id), resource_class.maximum(:id)) - end.to change { user_mentions.count }.by(0) - end -end - -RSpec.shared_examples 'resource notes mentions migration' do |migration_class, resource_class_name| - it 'migrates mentions from note' do - join = migration_class::JOIN - conditions = migration_class::QUERY_CONDITIONS - - # there are 5 notes for each noteable_type, but two do not have mentions and - # another one's noteable_id points to an inexistent resource - expect(notes.where(noteable_type: resource_class_name).count).to eq 5 - expect(user_mentions.count).to eq 0 - - expect do - subject.perform(resource_class_name, join, conditions, true, Note.minimum(:id), Note.maximum(:id)) - end.to change { user_mentions.count }.by(2) - - # check that the user_mention for regular note is created - user_mention = user_mentions.first - expect(Note.find(user_mention.note_id).system).to be false - expect(user_mention.mentioned_users_ids.sort).to eq(users.pluck(:id).sort) - expect(user_mention.mentioned_groups_ids.sort).to eq([group.id]) - expect(user_mention.mentioned_groups_ids.sort).not_to include(inaccessible_group.id) - - # check that the user_mention for system note is created - user_mention = user_mentions.second - expect(Note.find(user_mention.note_id).system).to be true - expect(user_mention.mentioned_users_ids.sort).to eq(users.pluck(:id).sort) - expect(user_mention.mentioned_groups_ids.sort).to eq([group.id]) - expect(user_mention.mentioned_groups_ids.sort).not_to include(inaccessible_group.id) - - # check that performing the same job twice does not fail and does not change counts - expect do - subject.perform(resource_class_name, join, conditions, true, Note.minimum(:id), Note.maximum(:id)) - end.to change { user_mentions.count }.by(0) - end -end - -RSpec.shared_examples 'schedules resource mentions migration' do |resource_class, is_for_notes| - before do - stub_const("#{described_class.name}::BATCH_SIZE", 1) - end - - it 'schedules background migrations' do - Sidekiq::Testing.fake! do - freeze_time do - resource_count = is_for_notes ? Note.count : resource_class.count - expect(resource_count).to eq 5 - - migrate! - - migration = described_class::MIGRATION - join = described_class::JOIN - conditions = described_class::QUERY_CONDITIONS - delay = described_class::DELAY - - expect(migration).to be_scheduled_delayed_migration(1 * delay, resource_class.name, join, conditions, is_for_notes, resource1.id, resource1.id) - expect(migration).to be_scheduled_delayed_migration(2 * delay, resource_class.name, join, conditions, is_for_notes, resource2.id, resource2.id) - expect(migration).to be_scheduled_delayed_migration(3 * delay, resource_class.name, join, conditions, is_for_notes, resource3.id, resource3.id) - expect(BackgroundMigrationWorker.jobs.size).to eq 3 - end - end - end -end - -RSpec.shared_examples 'resource migration not run' do |migration_class, resource_class_name| - it 'does not migrate mentions' do - join = migration_class::JOIN - conditions = migration_class::QUERY_CONDITIONS - resource_class = "#{Gitlab::BackgroundMigration::UserMentions::Models}::#{resource_class_name}".constantize - - expect do - subject.perform(resource_class_name, join, conditions, false, resource_class.minimum(:id), resource_class.maximum(:id)) - end.to change { user_mentions.count }.by(0) - end -end - -RSpec.shared_examples 'resource notes migration not run' do |migration_class, resource_class_name| - it 'does not migrate mentions' do - join = migration_class::JOIN - conditions = migration_class::QUERY_CONDITIONS - - expect do - subject.perform(resource_class_name, join, conditions, true, Note.minimum(:id), Note.maximum(:id)) - end.to change { user_mentions.count }.by(0) - end -end diff --git a/spec/support/shared_examples/lib/gitlab/cycle_analytics/event_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/cycle_analytics/event_shared_examples.rb index bd8bdd70ce5..bce889b454d 100644 --- a/spec/support/shared_examples/lib/gitlab/cycle_analytics/event_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/cycle_analytics/event_shared_examples.rb @@ -9,7 +9,7 @@ RSpec.shared_examples_for 'value stream analytics event' do it { expect(described_class.identifier).to be_a_kind_of(Symbol) } it { expect(instance.object_type.ancestors).to include(ApplicationRecord) } it { expect(instance).to respond_to(:timestamp_projection) } - it { expect(instance).to respond_to(:markdown_description) } + it { expect(instance).to respond_to(:html_description) } it { expect(instance.column_list).to be_a_kind_of(Array) } describe '#apply_query_customization' do diff --git a/spec/support/shared_examples/lib/gitlab/import_export/attributes_permitter_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/import_export/attributes_permitter_shared_examples.rb index 41d3d76b66b..03344584361 100644 --- a/spec/support/shared_examples/lib/gitlab/import_export/attributes_permitter_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/import_export/attributes_permitter_shared_examples.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true RSpec.shared_examples 'a permitted attribute' do |relation_sym, permitted_attributes, additional_attributes = []| - let(:prohibited_attributes) { %i[remote_url my_attributes my_ids token my_id test] } + let(:prohibited_attributes) { %w[remote_url my_attributes my_ids token my_id test] } let(:import_export_config) { Gitlab::ImportExport::Config.new.to_h } let(:project_relation_factory) { Gitlab::ImportExport::Project::RelationFactory } @@ -8,7 +8,7 @@ RSpec.shared_examples 'a permitted attribute' do |relation_sym, permitted_attrib let(:relation_hash) { (permitted_attributes + prohibited_attributes).map(&:to_s).zip([]).to_h } let(:relation_name) { project_relation_factory.overrides[relation_sym]&.to_sym || relation_sym } let(:relation_class) { project_relation_factory.relation_class(relation_name) } - let(:excluded_keys) { import_export_config.dig(:excluded_keys, relation_sym) || [] } + let(:excluded_keys) { (import_export_config.dig(:excluded_attributes, relation_sym) || []).map(&:to_s) } let(:cleaned_hash) do Gitlab::ImportExport::AttributeCleaner.new( @@ -18,7 +18,7 @@ RSpec.shared_examples 'a permitted attribute' do |relation_sym, permitted_attrib ).clean end - let(:permitted_hash) { subject.permit(relation_sym, relation_hash) } + let(:permitted_hash) { subject.permit(relation_sym, relation_hash).transform_keys { |k| k.to_s } } if described_class.new.permitted_attributes_defined?(relation_sym) it 'contains only attributes that are defined as permitted in the import/export config' do @@ -26,11 +26,11 @@ RSpec.shared_examples 'a permitted attribute' do |relation_sym, permitted_attrib end it 'does not contain attributes that would be cleaned with AttributeCleaner' do - expect(cleaned_hash.keys + additional_attributes.to_a).to include(*permitted_hash.keys) + expect(cleaned_hash.keys + additional_attributes.to_a.map(&:to_s)).to include(*permitted_hash.keys) end it 'does not contain prohibited attributes that are not related to given relation' do - expect(permitted_hash.keys).not_to include(*prohibited_attributes.map(&:to_s)) + expect(permitted_hash.keys).not_to include(*prohibited_attributes) end else it 'is disabled' do diff --git a/spec/support/shared_examples/lib/gitlab/redis/multi_store_feature_flags_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/redis/multi_store_feature_flags_shared_examples.rb new file mode 100644 index 00000000000..046c70bf779 --- /dev/null +++ b/spec/support/shared_examples/lib/gitlab/redis/multi_store_feature_flags_shared_examples.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'multi store feature flags' do |use_primary_and_secondary_stores, use_primary_store_as_default| + context "with feature flag :#{use_primary_and_secondary_stores} is enabled" do + before do + stub_feature_flags(use_primary_and_secondary_stores => true) + end + + it 'multi store is enabled' do + expect(subject.use_primary_and_secondary_stores?).to be true + end + end + + context "with feature flag :#{use_primary_and_secondary_stores} is disabled" do + before do + stub_feature_flags(use_primary_and_secondary_stores => false) + end + + it 'multi store is disabled' do + expect(subject.use_primary_and_secondary_stores?).to be false + end + end + + context "with feature flag :#{use_primary_store_as_default} is enabled" do + before do + stub_feature_flags(use_primary_store_as_default => true) + end + + it 'primary store is enabled' do + expect(subject.use_primary_store_as_default?).to be true + end + end + + context "with feature flag :#{use_primary_store_as_default} is disabled" do + before do + stub_feature_flags(use_primary_store_as_default => false) + end + + it 'primary store is disabled' do + expect(subject.use_primary_store_as_default?).to be false + end + end +end diff --git a/spec/support/shared_examples/loose_foreign_keys/have_loose_foreign_key.rb b/spec/support/shared_examples/loose_foreign_keys/have_loose_foreign_key.rb index 7ccd9533811..8f3a93de509 100644 --- a/spec/support/shared_examples/loose_foreign_keys/have_loose_foreign_key.rb +++ b/spec/support/shared_examples/loose_foreign_keys/have_loose_foreign_key.rb @@ -4,17 +4,12 @@ RSpec.shared_examples 'it has loose foreign keys' do let(:factory_name) { nil } let(:table_name) { described_class.table_name } let(:connection) { described_class.connection } - - it 'includes the LooseForeignKey module' do - expect(described_class.ancestors).to include(LooseForeignKey) - end - - it 'responds to #loose_foreign_key_definitions' do - expect(described_class).to respond_to(:loose_foreign_key_definitions) - end + let(:fully_qualified_table_name) { "#{connection.current_schema}.#{table_name}" } + let(:deleted_records) { LooseForeignKeys::DeletedRecord.where(fully_qualified_table_name: fully_qualified_table_name) } it 'has at least one loose foreign key definition' do - expect(described_class.loose_foreign_key_definitions.size).to be > 0 + definitions = Gitlab::Database::LooseForeignKeys.definitions_by_table[table_name] + expect(definitions.size).to be > 0 end it 'has the deletion trigger present' do @@ -32,9 +27,11 @@ RSpec.shared_examples 'it has loose foreign keys' do it 'records record deletions' do model = create(factory_name) # rubocop: disable Rails/SaveBang - model.destroy! - deleted_record = LooseForeignKeys::DeletedRecord.find_by(fully_qualified_table_name: "#{connection.current_schema}.#{table_name}", primary_key_value: model.id) + # using delete to avoid cross-database modification errors when associations with dependent option are present + model.delete + + deleted_record = deleted_records.find_by(primary_key_value: model.id) expect(deleted_record).not_to be_nil end @@ -42,11 +39,36 @@ RSpec.shared_examples 'it has loose foreign keys' do it 'cleans up record deletions' do model = create(factory_name) # rubocop: disable Rails/SaveBang - expect { model.destroy! }.to change { LooseForeignKeys::DeletedRecord.count }.by(1) + expect { model.delete }.to change { deleted_records.count }.by(1) LooseForeignKeys::ProcessDeletedRecordsService.new(connection: connection).execute - expect(LooseForeignKeys::DeletedRecord.status_pending.count).to be(0) - expect(LooseForeignKeys::DeletedRecord.status_processed.count).to be(1) + expect(deleted_records.status_pending.count).to be(0) + expect(deleted_records.status_processed.count).to be(1) + end +end + +RSpec.shared_examples 'cleanup by a loose foreign key' do + let(:foreign_key_definition) do + foreign_keys_for_parent = Gitlab::Database::LooseForeignKeys.definitions_by_table[parent.class.table_name] + foreign_keys_for_parent.find { |definition| definition.from_table == model.class.table_name } + end + + def find_model + model.class.find_by(id: model.id) + end + + it 'deletes the model' do + parent.delete + + expect(find_model).to be_present + + LooseForeignKeys::ProcessDeletedRecordsService.new(connection: model.connection).execute + + if foreign_key_definition.on_delete.eql?(:async_delete) + expect(find_model).not_to be_present + else + expect(find_model[foreign_key_definition.column]).to eq(nil) + end end end diff --git a/spec/support/shared_examples/mailers/notify_shared_examples.rb b/spec/support/shared_examples/mailers/notify_shared_examples.rb index e1f7a9030e2..20ed380fb18 100644 --- a/spec/support/shared_examples/mailers/notify_shared_examples.rb +++ b/spec/support/shared_examples/mailers/notify_shared_examples.rb @@ -161,6 +161,12 @@ RSpec.shared_examples 'it should not have Gmail Actions links' do end end +RSpec.shared_examples 'it should show Gmail Actions Join now link' do + it_behaves_like 'it should have Gmail Actions links' + + it { is_expected.to have_body_text('Join now') } +end + RSpec.shared_examples 'it should show Gmail Actions View Issue link' do it_behaves_like 'it should have Gmail Actions links' diff --git a/spec/support/shared_examples/metrics/active_record_subscriber_shared_examples.rb b/spec/support/shared_examples/metrics/active_record_subscriber_shared_examples.rb index c06083ba952..6e8c340582a 100644 --- a/spec/support/shared_examples/metrics/active_record_subscriber_shared_examples.rb +++ b/spec/support/shared_examples/metrics/active_record_subscriber_shared_examples.rb @@ -1,7 +1,11 @@ # frozen_string_literal: true RSpec.shared_examples 'store ActiveRecord info in RequestStore' do |db_role| - let(:db_config_name) { ::Gitlab::Database.db_config_names.first } + let(:db_config_name) do + db_config_name = ::Gitlab::Database.db_config_names.first + db_config_name += "_replica" if db_role == :secondary + db_config_name + end let(:expected_payload_defaults) do result = {} @@ -39,15 +43,15 @@ RSpec.shared_examples 'store ActiveRecord info in RequestStore' do |db_role| db_write_count: record_write_query ? 1 : 0, db_cached_count: record_cached_query ? 1 : 0, db_primary_cached_count: record_cached_query ? 1 : 0, - "db_primary_#{db_config_name}_cached_count": record_cached_query ? 1 : 0, + "db_#{db_config_name}_cached_count": record_cached_query ? 1 : 0, db_primary_count: record_query ? 1 : 0, - "db_primary_#{db_config_name}_count": record_query ? 1 : 0, + "db_#{db_config_name}_count": record_query ? 1 : 0, db_primary_duration_s: record_query ? 0.002 : 0.0, - "db_primary_#{db_config_name}_duration_s": record_query ? 0.002 : 0.0, + "db_#{db_config_name}_duration_s": record_query ? 0.002 : 0.0, db_primary_wal_count: record_wal_query ? 1 : 0, - "db_primary_#{db_config_name}_wal_count": record_wal_query ? 1 : 0, + "db_#{db_config_name}_wal_count": record_wal_query ? 1 : 0, db_primary_wal_cached_count: record_wal_query && record_cached_query ? 1 : 0, - "db_primary_#{db_config_name}_wal_cached_count": record_wal_query && record_cached_query ? 1 : 0 + "db_#{db_config_name}_wal_cached_count": record_wal_query && record_cached_query ? 1 : 0 }) elsif db_role == :replica transform_hash(expected_payload_defaults, { @@ -55,15 +59,15 @@ RSpec.shared_examples 'store ActiveRecord info in RequestStore' do |db_role| db_write_count: record_write_query ? 1 : 0, db_cached_count: record_cached_query ? 1 : 0, db_replica_cached_count: record_cached_query ? 1 : 0, - "db_replica_#{db_config_name}_cached_count": record_cached_query ? 1 : 0, + "db_#{db_config_name}_cached_count": record_cached_query ? 1 : 0, db_replica_count: record_query ? 1 : 0, - "db_replica_#{db_config_name}_count": record_query ? 1 : 0, + "db_#{db_config_name}_count": record_query ? 1 : 0, db_replica_duration_s: record_query ? 0.002 : 0.0, - "db_replica_#{db_config_name}_duration_s": record_query ? 0.002 : 0.0, + "db_#{db_config_name}_duration_s": record_query ? 0.002 : 0.0, db_replica_wal_count: record_wal_query ? 1 : 0, - "db_replica_#{db_config_name}_wal_count": record_wal_query ? 1 : 0, + "db_#{db_config_name}_wal_count": record_wal_query ? 1 : 0, db_replica_wal_cached_count: record_wal_query && record_cached_query ? 1 : 0, - "db_replica_#{db_config_name}_wal_cached_count": record_wal_query && record_cached_query ? 1 : 0 + "db_#{db_config_name}_wal_cached_count": record_wal_query && record_cached_query ? 1 : 0 }) else transform_hash(expected_payload_defaults, { @@ -71,15 +75,15 @@ RSpec.shared_examples 'store ActiveRecord info in RequestStore' do |db_role| db_write_count: record_write_query ? 1 : 0, db_cached_count: record_cached_query ? 1 : 0, db_primary_cached_count: 0, - "db_primary_#{db_config_name}_cached_count": 0, + "db_#{db_config_name}_cached_count": 0, db_primary_count: 0, - "db_primary_#{db_config_name}_count": 0, + "db_#{db_config_name}_count": 0, db_primary_duration_s: 0.0, - "db_primary_#{db_config_name}_duration_s": 0.0, + "db_#{db_config_name}_duration_s": 0.0, db_primary_wal_count: 0, - "db_primary_#{db_config_name}_wal_count": 0, + "db_#{db_config_name}_wal_count": 0, db_primary_wal_cached_count: 0, - "db_primary_#{db_config_name}_wal_cached_count": 0 + "db_#{db_config_name}_wal_cached_count": 0 }) end @@ -105,7 +109,11 @@ RSpec.shared_examples 'store ActiveRecord info in RequestStore' do |db_role| end RSpec.shared_examples 'record ActiveRecord metrics in a metrics transaction' do |db_role| - let(:db_config_name) { ::Gitlab::Database.db_config_name(ApplicationRecord.retrieve_connection) } + let(:db_config_name) do + db_config_name = ::Gitlab::Database.db_config_names.first + db_config_name += "_replica" if db_role == :secondary + db_config_name + end it 'increments only db counters' do if record_query diff --git a/spec/support/shared_examples/models/atomic_internal_id_shared_examples.rb b/spec/support/shared_examples/models/atomic_internal_id_shared_examples.rb index 03f565e0aac..fe85daa7235 100644 --- a/spec/support/shared_examples/models/atomic_internal_id_shared_examples.rb +++ b/spec/support/shared_examples/models/atomic_internal_id_shared_examples.rb @@ -80,15 +80,22 @@ RSpec.shared_examples 'AtomicInternalId' do |validate_presence: true| it 'calls InternalId.generate_next and sets internal id attribute' do iid = rand(1..1000) - expect(InternalId).to receive(:generate_next).with(instance, scope_attrs, usage, any_args).and_return(iid) + # Need to do this before evaluating instance otherwise it gets set + # already in factory + allow(InternalId).to receive(:generate_next).and_return(iid) + subject expect(read_internal_id).to eq(iid) + + expect(InternalId).to have_received(:generate_next).with(instance, scope_attrs, usage, any_args) end it 'does not overwrite an existing internal id' do write_internal_id(4711) - expect { subject }.not_to change { read_internal_id } + allow_cross_database_modification_within_transaction(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/347091') do + expect { subject }.not_to change { read_internal_id } + end end context 'when the instance has an internal ID set' do @@ -101,6 +108,7 @@ RSpec.shared_examples 'AtomicInternalId' do |validate_presence: true| .to receive(:track_greatest) .with(instance, scope_attrs, usage, internal_id, any_args) .and_return(internal_id) + subject end end @@ -110,7 +118,11 @@ RSpec.shared_examples 'AtomicInternalId' do |validate_presence: true| context 'when the internal id has been changed' do context 'when the internal id is automatically set' do it 'clears it on the instance' do - expect_iid_to_be_set_and_rollback + write_internal_id(nil) + + allow_cross_database_modification_within_transaction(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/347091') do + expect_iid_to_be_set_and_rollback + end expect(read_internal_id).to be_nil end @@ -120,7 +132,9 @@ RSpec.shared_examples 'AtomicInternalId' do |validate_presence: true| it 'does not clear it on the instance' do write_internal_id(100) - expect_iid_to_be_set_and_rollback + allow_cross_database_modification_within_transaction(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/347091') do + expect_iid_to_be_set_and_rollback + end expect(read_internal_id).not_to be_nil end diff --git a/spec/support/shared_examples/models/chat_integration_shared_examples.rb b/spec/support/shared_examples/models/chat_integration_shared_examples.rb index 72659dd5f3b..e6b270c6188 100644 --- a/spec/support/shared_examples/models/chat_integration_shared_examples.rb +++ b/spec/support/shared_examples/models/chat_integration_shared_examples.rb @@ -71,7 +71,7 @@ RSpec.shared_examples "chat integration" do |integration_name| it "does not call #{integration_name} API" do result = subject.execute(sample_data) - expect(result).to be(false) + expect(result).to be_falsy expect(WebMock).not_to have_requested(:post, webhook_url) end end @@ -113,7 +113,7 @@ RSpec.shared_examples "chat integration" do |integration_name| context "with protected branch" do before do - create(:protected_branch, project: project, name: "a-protected-branch") + create(:protected_branch, :create_branch_on_repository, project: project, name: "a-protected-branch") end let(:sample_data) do @@ -309,7 +309,7 @@ RSpec.shared_examples "chat integration" do |integration_name| context "with protected branch" do before do - create(:protected_branch, project: project, name: "a-protected-branch") + create(:protected_branch, :create_branch_on_repository, project: project, name: "a-protected-branch") end let(:sample_data) do @@ -355,5 +355,11 @@ RSpec.shared_examples "chat integration" do |integration_name| end end end + + context 'deployment events' do + let(:sample_data) { Gitlab::DataBuilder::Deployment.build(create(:deployment), Time.now) } + + it_behaves_like "untriggered #{integration_name} integration" + end end end diff --git a/spec/support/shared_examples/models/concerns/integrations/slack_mattermost_notifier_shared_examples.rb b/spec/support/shared_examples/models/concerns/integrations/slack_mattermost_notifier_shared_examples.rb index 2d4c0b60f2b..ad15f82be5e 100644 --- a/spec/support/shared_examples/models/concerns/integrations/slack_mattermost_notifier_shared_examples.rb +++ b/spec/support/shared_examples/models/concerns/integrations/slack_mattermost_notifier_shared_examples.rb @@ -305,7 +305,7 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |service_name| context 'on a protected branch' do before do - create(:protected_branch, project: project, name: 'a-protected-branch') + create(:protected_branch, :create_branch_on_repository, project: project, name: 'a-protected-branch') end let(:data) do @@ -347,7 +347,7 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |service_name| context 'on a protected branch with protected branches defined using wildcards' do before do - create(:protected_branch, project: project, name: '*-stable') + create(:protected_branch, :create_branch_on_repository, repository_branch_name: '1-stable', project: project, name: '*-stable') end let(:data) do @@ -560,7 +560,7 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |service_name| context 'on a protected branch' do before do - create(:protected_branch, project: project, name: 'a-protected-branch') + create(:protected_branch, :create_branch_on_repository, project: project, name: 'a-protected-branch') end let(:pipeline) do @@ -590,7 +590,7 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |service_name| context 'on a protected branch with protected branches defined usin wildcards' do before do - create(:protected_branch, project: project, name: '*-stable') + create(:protected_branch, :create_branch_on_repository, repository_branch_name: '1-stable', project: project, name: '*-stable') end let(:pipeline) do diff --git a/spec/support/shared_examples/models/member_shared_examples.rb b/spec/support/shared_examples/models/member_shared_examples.rb index a2909c66e22..d5d137922eb 100644 --- a/spec/support/shared_examples/models/member_shared_examples.rb +++ b/spec/support/shared_examples/models/member_shared_examples.rb @@ -301,10 +301,6 @@ RSpec.shared_examples_for "member creation" do end context 'when `tasks_to_be_done` and `tasks_project_id` are passed' do - before do - stub_experiments(invite_members_for_task: true) - end - it 'creates a member_task with the correct attributes', :aggregate_failures do task_project = source.is_a?(Group) ? create(:project, group: source) : source described_class.new(source, user, :developer, tasks_to_be_done: %w(ci code), tasks_project_id: task_project.id).execute @@ -397,10 +393,6 @@ RSpec.shared_examples_for "bulk member creation" do end context 'when `tasks_to_be_done` and `tasks_project_id` are passed' do - before do - stub_experiments(invite_members_for_task: true) - end - it 'creates a member_task with the correct attributes', :aggregate_failures do task_project = source.is_a?(Group) ? create(:project, group: source) : source members = described_class.add_users(source, [user1], :developer, tasks_to_be_done: %w(ci code), tasks_project_id: task_project.id) diff --git a/spec/support/shared_examples/models/packages/debian/component_file_shared_example.rb b/spec/support/shared_examples/models/packages/debian/component_file_shared_example.rb index f08ee820463..23026167b19 100644 --- a/spec/support/shared_examples/models/packages/debian/component_file_shared_example.rb +++ b/spec/support/shared_examples/models/packages/debian/component_file_shared_example.rb @@ -23,7 +23,7 @@ RSpec.shared_examples 'Debian Component File' do |container_type, can_freeze| let_it_be(:component_file_other_file_md5, freeze: can_freeze) { create("debian_#{container_type}_component_file", component: component1_1, architecture: architecture1_1, file_md5: 'other_md5') } let_it_be(:component_file_other_file_sha256, freeze: can_freeze) { create("debian_#{container_type}_component_file", component: component1_1, architecture: architecture1_1, file_sha256: 'other_sha256') } let_it_be(:component_file_other_container, freeze: can_freeze) { create("debian_#{container_type}_component_file", component: component2_1, architecture: architecture2_1) } - let_it_be_with_refind(:component_file_with_file_type_source) { create("debian_#{container_type}_component_file", :source, component: component1_1) } + let_it_be_with_refind(:component_file_with_file_type_sources) { create("debian_#{container_type}_component_file", :sources, component: component1_1) } let_it_be(:component_file_with_file_type_di_packages, freeze: can_freeze) { create("debian_#{container_type}_component_file", :di_packages, component: component1_1, architecture: architecture1_1) } subject { component_file_with_architecture } @@ -43,8 +43,8 @@ RSpec.shared_examples 'Debian Component File' do |container_type, can_freeze| it { is_expected.to belong_to(:architecture).class_name("Packages::Debian::#{container_type.capitalize}Architecture").inverse_of(:files) } end - context 'with :source file_type' do - subject { component_file_with_file_type_source } + context 'with :sources file_type' do + subject { component_file_with_file_type_sources } it { is_expected.to belong_to(:architecture).class_name("Packages::Debian::#{container_type.capitalize}Architecture").inverse_of(:files).optional } end @@ -66,8 +66,8 @@ RSpec.shared_examples 'Debian Component File' do |container_type, can_freeze| it { is_expected.to validate_presence_of(:architecture) } end - context 'with :source file_type' do - subject { component_file_with_file_type_source } + context 'with :sources file_type' do + subject { component_file_with_file_type_sources } it { is_expected.to validate_absence_of(:architecture) } end @@ -135,10 +135,10 @@ RSpec.shared_examples 'Debian Component File' do |container_type, can_freeze| end describe '.with_file_type' do - subject { described_class.with_file_type(:source) } + subject { described_class.with_file_type(:sources) } it do - expect(subject.to_a).to contain_exactly(component_file_with_file_type_source) + expect(subject.to_a).to contain_exactly(component_file_with_file_type_sources) end end @@ -214,9 +214,9 @@ RSpec.shared_examples 'Debian Component File' do |container_type, can_freeze| end context 'with a Source file_type' do - subject { component_file_with_file_type_source.relative_path } + subject { component_file_with_file_type_sources.relative_path } - it { is_expected.to eq("#{component1_1.name}/source/Source") } + it { is_expected.to eq("#{component1_1.name}/source/Sources") } end context 'with a DI Packages file_type' do diff --git a/spec/support/shared_examples/namespaces/traversal_examples.rb b/spec/support/shared_examples/namespaces/traversal_examples.rb index ac6a843663f..73e22b97abc 100644 --- a/spec/support/shared_examples/namespaces/traversal_examples.rb +++ b/spec/support/shared_examples/namespaces/traversal_examples.rb @@ -205,6 +205,58 @@ RSpec.shared_examples 'namespace traversal' do end end + shared_examples '#ancestors_upto' do + let(:parent) { create(:group) } + let(:child) { create(:group, parent: parent) } + let(:child2) { create(:group, parent: child) } + + it 'returns all ancestors when no namespace is given' do + expect(child2.ancestors_upto).to contain_exactly(child, parent) + end + + it 'includes ancestors upto but excluding the given ancestor' do + expect(child2.ancestors_upto(parent)).to contain_exactly(child) + end + + context 'with asc hierarchy_order' do + it 'returns the correct ancestor ids' do + expect(child2.ancestors_upto(hierarchy_order: :asc)).to eq([child, parent]) + end + end + + context 'with desc hierarchy_order' do + it 'returns the correct ancestor ids' do + expect(child2.ancestors_upto(hierarchy_order: :desc)).to eq([parent, child]) + end + end + + describe '#recursive_self_and_ancestor_ids' do + it 'is equivalent to ancestors_upto' do + recursive_result = child2.recursive_ancestors_upto(parent) + linear_result = child2.ancestors_upto(parent) + expect(linear_result).to match_array recursive_result + end + + it 'makes a recursive query' do + expect { child2.recursive_ancestors_upto.try(:load) }.to make_queries_matching(/WITH RECURSIVE/) + end + end + end + + describe '#ancestors_upto' do + context 'with use_traversal_ids_for_ancestors_upto enabled' do + include_examples '#ancestors_upto' + end + + context 'with use_traversal_ids_for_ancestors_upto disabled' do + before do + stub_feature_flags(use_traversal_ids_for_ancestors_upto: false) + end + + include_examples '#ancestors_upto' + end + end + describe '#descendants' do let!(:another_group) { create(:group) } let!(:another_group_nested) { create(:group, parent: another_group) } diff --git a/spec/support/shared_examples/namespaces/traversal_scope_examples.rb b/spec/support/shared_examples/namespaces/traversal_scope_examples.rb index 4c09c1c2a3b..3d52ed30c62 100644 --- a/spec/support/shared_examples/namespaces/traversal_scope_examples.rb +++ b/spec/support/shared_examples/namespaces/traversal_scope_examples.rb @@ -213,6 +213,12 @@ RSpec.shared_examples 'namespace traversal scopes' do it { is_expected.to contain_exactly(deep_nested_group_1, deep_nested_group_2) } end + + context 'with offset and limit' do + subject { described_class.where(id: [group_1, group_2]).offset(1).limit(1).self_and_descendants } + + it { is_expected.to contain_exactly(group_2, nested_group_2, deep_nested_group_2) } + end end describe '.self_and_descendants' do @@ -242,6 +248,19 @@ RSpec.shared_examples 'namespace traversal scopes' do it { is_expected.to contain_exactly(deep_nested_group_1.id, deep_nested_group_2.id) } end + + context 'with offset and limit' do + subject do + described_class + .where(id: [group_1, group_2]) + .limit(1) + .offset(1) + .self_and_descendant_ids + .pluck(:id) + end + + it { is_expected.to contain_exactly(group_2.id, nested_group_2.id, deep_nested_group_2.id) } + end end describe '.self_and_descendant_ids' do diff --git a/spec/support/shared_examples/requests/api/composer_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/composer_packages_shared_examples.rb index e45be21f152..9f4fdcf7ba1 100644 --- a/spec/support/shared_examples/requests/api/composer_packages_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/composer_packages_shared_examples.rb @@ -173,3 +173,65 @@ RSpec.shared_examples 'rejects Composer access with unknown project id' do end end end + +RSpec.shared_examples 'Composer access with deploy tokens' do + shared_examples 'a deploy token for Composer GET requests' do + context 'with deploy token headers' do + let(:headers) { basic_auth_header(deploy_token.username, deploy_token.token) } + + before do + group.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE) + end + + context 'valid token' do + it_behaves_like 'returning response status', :success + end + + context 'invalid token' do + let(:headers) { basic_auth_header(deploy_token.username, 'bar') } + + it_behaves_like 'returning response status', :not_found + end + end + end + + context 'group deploy token' do + let(:deploy_token) { deploy_token_for_group } + + it_behaves_like 'a deploy token for Composer GET requests' + end + + context 'project deploy token' do + let(:deploy_token) { deploy_token_for_project } + + it_behaves_like 'a deploy token for Composer GET requests' + end +end + +RSpec.shared_examples 'Composer publish with deploy tokens' do + shared_examples 'a deploy token for Composer publish requests' do + let(:headers) { basic_auth_header(deploy_token.username, deploy_token.token) } + + context 'valid token' do + it_behaves_like 'returning response status', :success + end + + context 'invalid token' do + let(:headers) { basic_auth_header(deploy_token.username, 'bar') } + + it_behaves_like 'returning response status', :unauthorized + end + end + + context 'group deploy token' do + let(:deploy_token) { deploy_token_for_group } + + it_behaves_like 'a deploy token for Composer publish requests' + end + + context 'group deploy token' do + let(:deploy_token) { deploy_token_for_project } + + it_behaves_like 'a deploy token for Composer publish requests' + end +end diff --git a/spec/support/shared_examples/requests/api/conan_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/conan_packages_shared_examples.rb index 20606ae942d..71f3a0235be 100644 --- a/spec/support/shared_examples/requests/api/conan_packages_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/conan_packages_shared_examples.rb @@ -178,6 +178,54 @@ RSpec.shared_examples 'rejects invalid recipe' do end end +RSpec.shared_examples 'handling empty values for username and channel' do + using RSpec::Parameterized::TableSyntax + + let(:recipe_path) { "#{package.name}/#{package.version}/#{package_username}/#{channel}" } + + where(:username, :channel, :status) do + 'username' | 'channel' | :ok + 'username' | '_' | :bad_request + '_' | 'channel' | :bad_request_or_not_found + '_' | '_' | :ok_or_not_found + end + + with_them do + let(:package_username) do + if username == 'username' + package.conan_metadatum.package_username + else + username + end + end + + before do + project.add_maintainer(user) # avoid any permission issue + end + + it 'returns the correct status code' do |example| + project_level = example.full_description.include?('api/v4/projects') + + expected_status = case status + when :ok_or_not_found + project_level ? :ok : :not_found + when :bad_request_or_not_found + project_level ? :bad_request : :not_found + else + status + end + + if expected_status == :ok + package.conan_metadatum.update!(package_username: package_username, package_channel: channel) + end + + subject + + expect(response).to have_gitlab_http_status(expected_status) + end + end +end + RSpec.shared_examples 'rejects invalid file_name' do |invalid_file_name| let(:file_name) { invalid_file_name } @@ -300,6 +348,7 @@ RSpec.shared_examples 'recipe snapshot endpoint' do it_behaves_like 'rejects invalid recipe' it_behaves_like 'rejects recipe for invalid project' it_behaves_like 'empty recipe for not found package' + it_behaves_like 'handling empty values for username and channel' context 'with existing package' do it 'returns a hash of files with their md5 hashes' do @@ -324,6 +373,7 @@ RSpec.shared_examples 'package snapshot endpoint' do it_behaves_like 'rejects invalid recipe' it_behaves_like 'rejects recipe for invalid project' it_behaves_like 'empty recipe for not found package' + it_behaves_like 'handling empty values for username and channel' context 'with existing package' do it 'returns a hash of md5 values for the files' do @@ -344,12 +394,14 @@ RSpec.shared_examples 'recipe download_urls endpoint' do it_behaves_like 'rejects invalid recipe' it_behaves_like 'rejects recipe for invalid project' it_behaves_like 'recipe download_urls' + it_behaves_like 'handling empty values for username and channel' end RSpec.shared_examples 'package download_urls endpoint' do it_behaves_like 'rejects invalid recipe' it_behaves_like 'rejects recipe for invalid project' it_behaves_like 'package download_urls' + it_behaves_like 'handling empty values for username and channel' end RSpec.shared_examples 'recipe upload_urls endpoint' do @@ -362,6 +414,7 @@ RSpec.shared_examples 'recipe upload_urls endpoint' do it_behaves_like 'rejects invalid recipe' it_behaves_like 'rejects invalid upload_url params' + it_behaves_like 'handling empty values for username and channel' it 'returns a set of upload urls for the files requested' do subject @@ -423,6 +476,7 @@ RSpec.shared_examples 'package upload_urls endpoint' do it_behaves_like 'rejects invalid recipe' it_behaves_like 'rejects invalid upload_url params' + it_behaves_like 'handling empty values for username and channel' it 'returns a set of upload urls for the files requested' do expected_response = { @@ -458,6 +512,7 @@ RSpec.shared_examples 'delete package endpoint' do let(:recipe_path) { package.conan_recipe_path } it_behaves_like 'rejects invalid recipe' + it_behaves_like 'handling empty values for username and channel' it 'returns unauthorized for users without valid permission' do subject @@ -568,12 +623,14 @@ RSpec.shared_examples 'recipe file download endpoint' do it_behaves_like 'a public project with packages' it_behaves_like 'an internal project with packages' it_behaves_like 'a private project with packages' + it_behaves_like 'handling empty values for username and channel' end RSpec.shared_examples 'package file download endpoint' do it_behaves_like 'a public project with packages' it_behaves_like 'an internal project with packages' it_behaves_like 'a private project with packages' + it_behaves_like 'handling empty values for username and channel' context 'tracking the conan_package.tgz download' do let(:package_file) { package.package_files.find_by(file_name: ::Packages::Conan::FileMetadatum::PACKAGE_BINARY) } @@ -598,6 +655,7 @@ RSpec.shared_examples 'workhorse authorize endpoint' do it_behaves_like 'rejects invalid recipe' it_behaves_like 'rejects invalid file_name', 'conanfile.py.git%2fgit-upload-pack' it_behaves_like 'workhorse authorization' + it_behaves_like 'handling empty values for username and channel' end RSpec.shared_examples 'workhorse recipe file upload endpoint' do @@ -619,6 +677,7 @@ RSpec.shared_examples 'workhorse recipe file upload endpoint' do it_behaves_like 'rejects invalid file_name', 'conanfile.py.git%2fgit-upload-pack' it_behaves_like 'uploads a package file' it_behaves_like 'creates build_info when there is a job' + it_behaves_like 'handling empty values for username and channel' end RSpec.shared_examples 'workhorse package file upload endpoint' do @@ -640,6 +699,7 @@ RSpec.shared_examples 'workhorse package file upload endpoint' do it_behaves_like 'rejects invalid file_name', 'conaninfo.txttest' it_behaves_like 'uploads a package file' it_behaves_like 'creates build_info when there is a job' + it_behaves_like 'handling empty values for username and channel' context 'tracking the conan_package.tgz upload' do let(:file_name) { ::Packages::Conan::FileMetadatum::PACKAGE_BINARY } diff --git a/spec/support/shared_examples/requests/api/graphql/mutations/snippets_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/mutations/snippets_shared_examples.rb index 62dbac3fd4d..8bffd1f71e9 100644 --- a/spec/support/shared_examples/requests/api/graphql/mutations/snippets_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/graphql/mutations/snippets_shared_examples.rb @@ -18,19 +18,19 @@ RSpec.shared_examples 'snippet edit usage data counters' do end end - context 'when user is not sessionless' do + context 'when user is not sessionless', :clean_gitlab_redis_sessions do before do session_id = Rack::Session::SessionId.new('6919a6f1bb119dd7396fadc38fd18d0d') session_hash = { 'warden.user.user.key' => [[current_user.id], current_user.encrypted_password[0, 29]] } - Gitlab::Redis::SharedState.with do |redis| + Gitlab::Redis::Sessions.with do |redis| redis.set("session:gitlab:#{session_id.private_id}", Marshal.dump(session_hash)) end cookies[Gitlab::Application.config.session_options[:key]] = session_id.public_id end - it 'tracks usage data actions', :clean_gitlab_redis_shared_state do + it 'tracks usage data actions', :clean_gitlab_redis_sessions do expect(::Gitlab::UsageDataCounters::EditorUniqueCounter).to receive(:track_snippet_editor_edit_action) post_graphql_mutation(mutation) diff --git a/spec/support/shared_examples/requests/api/graphql/packages/group_and_project_packages_list_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/packages/group_and_project_packages_list_shared_examples.rb index 367c6d4fa3a..882c79cb03f 100644 --- a/spec/support/shared_examples/requests/api/graphql/packages/group_and_project_packages_list_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/graphql/packages/group_and_project_packages_list_shared_examples.rb @@ -55,7 +55,7 @@ RSpec.shared_examples 'group and project packages query' do end it 'deals with metadata' do - expect(target_shas).to contain_exactly(composer_metadatum.target_sha) + expect(target_shas.compact).to contain_exactly(composer_metadatum.target_sha) end it 'returns the count of the packages' do diff --git a/spec/support/shared_examples/requests/api/issuable_participants_examples.rb b/spec/support/shared_examples/requests/api/issuable_participants_examples.rb index 673d7741017..c5e5803c0a7 100644 --- a/spec/support/shared_examples/requests/api/issuable_participants_examples.rb +++ b/spec/support/shared_examples/requests/api/issuable_participants_examples.rb @@ -28,4 +28,34 @@ RSpec.shared_examples 'issuable participants endpoint' do expect(response).to have_gitlab_http_status(:not_found) end + + context 'with a confidential note' do + let!(:note) do + create( + :note, + :confidential, + project: project, + noteable: entity, + author: create(:user) + ) + end + + it 'returns a full list of participants' do + get api("/projects/#{project.id}/#{area}/#{entity.iid}/participants", user) + + expect(response).to have_gitlab_http_status(:ok) + participant_ids = json_response.map { |el| el['id'] } + expect(participant_ids).to match_array([entity.author_id, note.author_id]) + end + + context 'when user cannot see a confidential note' do + it 'returns a limited list of participants' do + get api("/projects/#{project.id}/#{area}/#{entity.iid}/participants", create(:user)) + + expect(response).to have_gitlab_http_status(:ok) + participant_ids = json_response.map { |el| el['id'] } + expect(participant_ids).to match_array([entity.author_id]) + end + end + end end diff --git a/spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb index 19677e92001..8d6d85732be 100644 --- a/spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb @@ -41,19 +41,6 @@ RSpec.shared_examples 'handling get metadata requests' do |scope: :project| # query count can slightly change between the examples so we're using a custom threshold expect { get(url, headers: headers) }.not_to exceed_query_limit(control).with_threshold(4) end - - context 'with packages_npm_abbreviated_metadata disabled' do - before do - stub_feature_flags(packages_npm_abbreviated_metadata: false) - end - - it 'calls the presenter without including metadata' do - expect(::Packages::Npm::PackagePresenter) - .to receive(:new).with(anything, anything, include_metadata: false).and_call_original - - subject - end - end end shared_examples 'reject metadata request' do |status:| diff --git a/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb index 878cbc10a24..6568d51b90e 100644 --- a/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb @@ -391,7 +391,7 @@ RSpec.shared_examples 'rejects nuget access with invalid target id' do context 'with a target id with invalid integers' do using RSpec::Parameterized::TableSyntax - let(:target) { OpenStruct.new(id: id) } + let(:target) { double(id: id) } where(:id, :status) do '/../' | :bad_request @@ -411,7 +411,7 @@ end RSpec.shared_examples 'rejects nuget access with unknown target id' do context 'with an unknown target' do - let(:target) { OpenStruct.new(id: 1234567890) } + let(:target) { double(id: 1234567890) } context 'as anonymous' do it_behaves_like 'rejects nuget packages access', :anonymous, :unauthorized diff --git a/spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb index 06c51add438..aff086d1ba3 100644 --- a/spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb @@ -346,7 +346,8 @@ RSpec.shared_examples 'a pypi user namespace endpoint' do end with_them do - let_it_be_with_reload(:group) { create(:namespace) } + # only groups are supported, so this "group" is actually the wrong namespace type + let_it_be_with_reload(:group) { create(:user_namespace) } let(:headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, personal_access_token.token) } before do diff --git a/spec/support/shared_examples/services/packages/debian/generate_distribution_shared_examples.rb b/spec/support/shared_examples/services/packages/debian/generate_distribution_shared_examples.rb index c979fdc2bb0..7fd20fc3909 100644 --- a/spec/support/shared_examples/services/packages/debian/generate_distribution_shared_examples.rb +++ b/spec/support/shared_examples/services/packages/debian/generate_distribution_shared_examples.rb @@ -126,7 +126,7 @@ RSpec.shared_examples 'Generate Debian Distribution and component files' do SHA256: #{package_files[4].file_sha256} EOF - expected_main_source_content = <<~EOF + expected_main_sources_content = <<~EOF Package: #{package.name} Binary: sample-dev, libsample0, sample-udeb Version: #{package.version} @@ -158,7 +158,7 @@ RSpec.shared_examples 'Generate Debian Distribution and component files' do check_component_file(current_time.round, 'main', :di_packages, 'amd64', expected_main_amd64_di_content) check_component_file(current_time.round, 'main', :di_packages, 'arm64', nil) - check_component_file(current_time.round, 'main', :source, nil, expected_main_source_content) + check_component_file(current_time.round, 'main', :sources, nil, expected_main_sources_content) check_component_file(current_time.round, 'contrib', :packages, 'all', nil) check_component_file(current_time.round, 'contrib', :packages, 'amd64', nil) @@ -168,7 +168,7 @@ RSpec.shared_examples 'Generate Debian Distribution and component files' do check_component_file(current_time.round, 'contrib', :di_packages, 'amd64', nil) check_component_file(current_time.round, 'contrib', :di_packages, 'arm64', nil) - check_component_file(current_time.round, 'contrib', :source, nil, nil) + check_component_file(current_time.round, 'contrib', :sources, nil, nil) main_amd64_size = expected_main_amd64_content.length main_amd64_md5sum = Digest::MD5.hexdigest(expected_main_amd64_content) @@ -182,9 +182,9 @@ RSpec.shared_examples 'Generate Debian Distribution and component files' do main_amd64_di_md5sum = Digest::MD5.hexdigest(expected_main_amd64_di_content) main_amd64_di_sha256 = Digest::SHA256.hexdigest(expected_main_amd64_di_content) - main_source_size = expected_main_source_content.length - main_source_md5sum = Digest::MD5.hexdigest(expected_main_source_content) - main_source_sha256 = Digest::SHA256.hexdigest(expected_main_source_content) + main_sources_size = expected_main_sources_content.length + main_sources_md5sum = Digest::MD5.hexdigest(expected_main_sources_content) + main_sources_sha256 = Digest::SHA256.hexdigest(expected_main_sources_content) expected_release_content = <<~EOF Codename: unstable @@ -199,14 +199,14 @@ RSpec.shared_examples 'Generate Debian Distribution and component files' do d41d8cd98f00b204e9800998ecf8427e 0 contrib/debian-installer/binary-amd64/Packages d41d8cd98f00b204e9800998ecf8427e 0 contrib/binary-arm64/Packages d41d8cd98f00b204e9800998ecf8427e 0 contrib/debian-installer/binary-arm64/Packages - d41d8cd98f00b204e9800998ecf8427e 0 contrib/source/Source + d41d8cd98f00b204e9800998ecf8427e 0 contrib/source/Sources d41d8cd98f00b204e9800998ecf8427e 0 main/binary-all/Packages d41d8cd98f00b204e9800998ecf8427e 0 main/debian-installer/binary-all/Packages #{main_amd64_md5sum} #{main_amd64_size} main/binary-amd64/Packages #{main_amd64_di_md5sum} #{main_amd64_di_size} main/debian-installer/binary-amd64/Packages d41d8cd98f00b204e9800998ecf8427e 0 main/binary-arm64/Packages d41d8cd98f00b204e9800998ecf8427e 0 main/debian-installer/binary-arm64/Packages - #{main_source_md5sum} #{main_source_size} main/source/Source + #{main_sources_md5sum} #{main_sources_size} main/source/Sources SHA256: #{contrib_all_sha256} #{contrib_all_size} contrib/binary-all/Packages e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 contrib/debian-installer/binary-all/Packages @@ -214,14 +214,14 @@ RSpec.shared_examples 'Generate Debian Distribution and component files' do e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 contrib/debian-installer/binary-amd64/Packages e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 contrib/binary-arm64/Packages e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 contrib/debian-installer/binary-arm64/Packages - e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 contrib/source/Source + e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 contrib/source/Sources e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 main/binary-all/Packages e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 main/debian-installer/binary-all/Packages #{main_amd64_sha256} #{main_amd64_size} main/binary-amd64/Packages #{main_amd64_di_sha256} #{main_amd64_di_size} main/debian-installer/binary-amd64/Packages e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 main/binary-arm64/Packages e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 main/debian-installer/binary-arm64/Packages - #{main_source_sha256} #{main_source_size} main/source/Source + #{main_sources_sha256} #{main_sources_size} main/source/Sources EOF check_release_files(expected_release_content) diff --git a/spec/support/shared_examples/workers/background_migration_worker_shared_examples.rb b/spec/support/shared_examples/workers/background_migration_worker_shared_examples.rb new file mode 100644 index 00000000000..0d3e158d358 --- /dev/null +++ b/spec/support/shared_examples/workers/background_migration_worker_shared_examples.rb @@ -0,0 +1,212 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'it runs background migration jobs' do |tracking_database, metric_name| + describe 'defining the job attributes' do + it 'defines the data_consistency as always' do + expect(described_class.get_data_consistency).to eq(:always) + end + + it 'defines the retry count in sidekiq_options' do + expect(described_class.sidekiq_options['retry']).to eq(3) + end + + it 'defines the feature_category as database' do + expect(described_class.get_feature_category).to eq(:database) + end + + it 'defines the urgency as throttled' do + expect(described_class.get_urgency).to eq(:throttled) + end + + it 'defines the loggable_arguments' do + expect(described_class.loggable_arguments).to match_array([0, 1]) + end + end + + describe '.tracking_database' do + it 'does not raise an error' do + expect { described_class.tracking_database }.not_to raise_error + end + + it 'overrides the method to return the tracking database' do + expect(described_class.tracking_database).to eq(tracking_database) + end + end + + describe '.unhealthy_metric_name' do + it 'does not raise an error' do + expect { described_class.unhealthy_metric_name }.not_to raise_error + end + + it 'overrides the method to return the unhealthy metric name' do + expect(described_class.unhealthy_metric_name).to eq(metric_name) + end + end + + describe '.minimum_interval' do + it 'returns 2 minutes' do + expect(described_class.minimum_interval).to eq(2.minutes.to_i) + end + end + + describe '#perform' do + let(:worker) { described_class.new } + + before do + allow(worker).to receive(:jid).and_return(1) + allow(worker).to receive(:always_perform?).and_return(false) + + allow(Postgresql::ReplicationSlot).to receive(:lag_too_great?).and_return(false) + end + + it 'performs jobs using the coordinator for the worker' do + expect_next_instance_of(Gitlab::BackgroundMigration::JobCoordinator) do |coordinator| + allow(coordinator).to receive(:with_shared_connection).and_yield + + expect(coordinator.worker_class).to eq(described_class) + expect(coordinator).to receive(:perform).with('Foo', [10, 20]) + end + + worker.perform('Foo', [10, 20]) + end + + context 'when lease can be obtained' do + let(:coordinator) { double('job coordinator') } + + before do + allow(Gitlab::BackgroundMigration).to receive(:coordinator_for_database) + .with(tracking_database) + .and_return(coordinator) + + allow(coordinator).to receive(:with_shared_connection).and_yield + end + + it 'sets up the shared connection before checking replication' do + expect(coordinator).to receive(:with_shared_connection).and_yield.ordered + expect(Postgresql::ReplicationSlot).to receive(:lag_too_great?).and_return(false).ordered + + expect(coordinator).to receive(:perform).with('Foo', [10, 20]) + + worker.perform('Foo', [10, 20]) + end + + it 'performs a background migration' do + expect(coordinator).to receive(:perform).with('Foo', [10, 20]) + + worker.perform('Foo', [10, 20]) + end + + context 'when lease_attempts is 1' do + it 'performs a background migration' do + expect(coordinator).to receive(:perform).with('Foo', [10, 20]) + + worker.perform('Foo', [10, 20], 1) + end + end + + it 'can run scheduled job and retried job concurrently' do + expect(coordinator) + .to receive(:perform) + .with('Foo', [10, 20]) + .exactly(2).time + + worker.perform('Foo', [10, 20]) + worker.perform('Foo', [10, 20], described_class::MAX_LEASE_ATTEMPTS - 1) + end + + it 'sets the class that will be executed as the caller_id' do + expect(coordinator).to receive(:perform) do + expect(Gitlab::ApplicationContext.current).to include('meta.caller_id' => 'Foo') + end + + worker.perform('Foo', [10, 20]) + end + end + + context 'when lease not obtained (migration of same class was performed recently)' do + let(:timeout) { described_class.minimum_interval } + let(:lease_key) { "#{described_class.name}:Foo" } + let(:coordinator) { double('job coordinator') } + + before do + allow(Gitlab::BackgroundMigration).to receive(:coordinator_for_database) + .with(tracking_database) + .and_return(coordinator) + + allow(coordinator).to receive(:with_shared_connection).and_yield + + expect(coordinator).not_to receive(:perform) + + Gitlab::ExclusiveLease.new(lease_key, timeout: timeout).try_obtain + end + + it 'reschedules the migration and decrements the lease_attempts' do + expect(described_class) + .to receive(:perform_in) + .with(a_kind_of(Numeric), 'Foo', [10, 20], 4) + + worker.perform('Foo', [10, 20], 5) + end + + context 'when lease_attempts is 1' do + let(:lease_key) { "#{described_class.name}:Foo:retried" } + + it 'reschedules the migration and decrements the lease_attempts' do + expect(described_class) + .to receive(:perform_in) + .with(a_kind_of(Numeric), 'Foo', [10, 20], 0) + + worker.perform('Foo', [10, 20], 1) + end + end + + context 'when lease_attempts is 0' do + let(:lease_key) { "#{described_class.name}:Foo:retried" } + + it 'gives up performing the migration' do + expect(described_class).not_to receive(:perform_in) + expect(Sidekiq.logger).to receive(:warn).with( + class: 'Foo', + message: 'Job could not get an exclusive lease after several tries. Giving up.', + job_id: 1) + + worker.perform('Foo', [10, 20], 0) + end + end + end + + context 'when database is not healthy' do + before do + expect(Postgresql::ReplicationSlot).to receive(:lag_too_great?).and_return(true) + end + + it 'reschedules a migration if the database is not healthy' do + expect(described_class) + .to receive(:perform_in) + .with(a_kind_of(Numeric), 'Foo', [10, 20], 4) + + worker.perform('Foo', [10, 20]) + end + + it 'increments the unhealthy counter' do + counter = Gitlab::Metrics.counter(metric_name, 'msg') + + expect(described_class).to receive(:perform_in) + + expect { worker.perform('Foo', [10, 20]) }.to change { counter.get }.by(1) + end + + context 'when lease_attempts is 0' do + it 'gives up performing the migration' do + expect(described_class).not_to receive(:perform_in) + expect(Sidekiq.logger).to receive(:warn).with( + class: 'Foo', + message: 'Database was unhealthy after several tries. Giving up.', + job_id: 1) + + worker.perform('Foo', [10, 20], 0) + end + end + end + end +end |