diff options
Diffstat (limited to 'spec/support/shared_examples')
41 files changed, 1434 insertions, 505 deletions
diff --git a/spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb b/spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb index 9c8006ce4f1..cadc753513d 100644 --- a/spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb +++ b/spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb @@ -109,6 +109,18 @@ RSpec.shared_examples 'multiple issue boards' do assert_boards_nav_active end + + it 'switches current board back' do + in_boards_switcher_dropdown do + click_link board.name + end + + wait_for_requests + + page.within('.boards-switcher') do + expect(page).to have_content(board.name) + end + end end context 'unauthorized user' do 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 422282da4d8..a9c6da7bc2b 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 @@ -80,7 +80,6 @@ RSpec.shared_examples 'a GitHub-ish import controller: GET status' do expect(json_response.dig("imported_projects", 0, "id")).to eq(project.id) expect(json_response.dig("provider_repos", 0, "id")).to eq(repo.id) expect(json_response.dig("provider_repos", 1, "id")).to eq(org_repo.id) - expect(json_response.dig("namespaces", 0, "id")).to eq(group.id) end it "does not show already added project" do @@ -156,7 +155,6 @@ RSpec.shared_examples 'a GitHub-ish import controller: GET status' do expect(json_response.dig("imported_projects").count).to eq(0) expect(json_response.dig("provider_repos").count).to eq(1) expect(json_response.dig("provider_repos", 0, "id")).to eq(repo_2.id) - expect(json_response.dig("namespaces", 0, "id")).to eq(group.id) end it 'filters the list, ignoring the case of the name' do @@ -166,7 +164,6 @@ RSpec.shared_examples 'a GitHub-ish import controller: GET status' do expect(json_response.dig("imported_projects").count).to eq(0) expect(json_response.dig("provider_repos").count).to eq(1) expect(json_response.dig("provider_repos", 0, "id")).to eq(repo_2.id) - expect(json_response.dig("namespaces", 0, "id")).to eq(group.id) end context 'when user input contains html' do diff --git a/spec/support/shared_examples/controllers/import_controller_status_shared_examples.rb b/spec/support/shared_examples/controllers/import_controller_status_shared_examples.rb index ecb9abc5c46..b9ae0e23e26 100644 --- a/spec/support/shared_examples/controllers/import_controller_status_shared_examples.rb +++ b/spec/support/shared_examples/controllers/import_controller_status_shared_examples.rb @@ -18,7 +18,6 @@ RSpec.shared_examples 'import controller status' do expect(response).to have_gitlab_http_status(:ok) expect(json_response.dig("imported_projects", 0, "id")).to eq(project.id) expect(json_response.dig("provider_repos", 0, "id")).to eq(repo_id) - expect(json_response.dig("namespaces", 0, "id")).to eq(group.id) end it "does not show already added project" do 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 3f97c031e27..30914e61df0 100644 --- a/spec/support/shared_examples/controllers/unique_visits_shared_examples.rb +++ b/spec/support/shared_examples/controllers/unique_visits_shared_examples.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true RSpec.shared_examples 'tracking unique visits' do |method| + include TrackingHelpers + let(:request_params) { {} } it 'tracks unique visit if the format is HTML' do @@ -14,14 +16,15 @@ RSpec.shared_examples 'tracking unique visits' do |method| expect(Gitlab::UsageDataCounters::HLLRedisCounter) .to receive(:track_event).with(target_id, values: kind_of(String)) - request.headers['DNT'] = '0' + stub_do_not_track('0') get method, params: request_params, format: :html end it 'does not track unique visit if DNT is enabled' do expect(Gitlab::UsageDataCounters::HLLRedisCounter).not_to receive(:track_event) - request.headers['DNT'] = '1' + + stub_do_not_track('1') get method, params: request_params, format: :html end diff --git a/spec/support/shared_examples/features/dependency_proxy_shared_examples.rb b/spec/support/shared_examples/features/dependency_proxy_shared_examples.rb new file mode 100644 index 00000000000..d29c677a962 --- /dev/null +++ b/spec/support/shared_examples/features/dependency_proxy_shared_examples.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'a successful blob pull' do + it 'sends a file' do + expect(controller).to receive(:send_file).with(blob.file.path, {}) + + subject + end + + it 'returns Content-Disposition: attachment', :aggregate_failures do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(response.headers['Content-Disposition']).to match(/^attachment/) + end +end + +RSpec.shared_examples 'a successful manifest pull' do + it 'sends a file' do + expect(controller).to receive(:send_file).with(manifest.file.path, type: manifest.content_type) + + subject + end + + it 'returns Content-Disposition: attachment', :aggregate_failures do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(response.headers['Docker-Content-Digest']).to eq(manifest.digest) + expect(response.headers['Content-Length']).to eq(manifest.size) + expect(response.headers['Docker-Distribution-Api-Version']).to eq(DependencyProxy::DISTRIBUTION_API_VERSION) + expect(response.headers['Etag']).to eq("\"#{manifest.digest}\"") + expect(response.headers['Content-Disposition']).to match(/^attachment/) + end +end diff --git a/spec/support/shared_examples/features/discussion_comments_shared_example.rb b/spec/support/shared_examples/features/discussion_comments_shared_example.rb index ff2878f77b4..fb2e422559d 100644 --- a/spec/support/shared_examples/features/discussion_comments_shared_example.rb +++ b/spec/support/shared_examples/features/discussion_comments_shared_example.rb @@ -308,7 +308,7 @@ RSpec.shared_examples 'thread comments for issue, epic and merge request' do |re let(:reply_id) { find("#{comments_selector} .note:last-of-type", match: :first)['data-note-id'] } it 'can be replied to after resolving' do - find('button[data-qa-selector="resolve_discussion_button"]').click + find('button[data-qa-selector="resolve_discussion_button"]').click # rubocop:disable QA/SelectorUsage wait_for_requests refresh @@ -320,7 +320,7 @@ RSpec.shared_examples 'thread comments for issue, epic and merge request' do |re it 'shows resolved thread when toggled' do submit_reply('a') - find('button[data-qa-selector="resolve_discussion_button"]').click + find('button[data-qa-selector="resolve_discussion_button"]').click # rubocop:disable QA/SelectorUsage wait_for_requests expect(page).to have_selector(".note-row-#{note_id}", visible: true) diff --git a/spec/support/shared_examples/features/manage_applications_shared_examples.rb b/spec/support/shared_examples/features/manage_applications_shared_examples.rb new file mode 100644 index 00000000000..38bb87eaed2 --- /dev/null +++ b/spec/support/shared_examples/features/manage_applications_shared_examples.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'manage applications' do + let_it_be(:application_name) { 'application foo bar' } + let_it_be(:application_name_changed) { "#{application_name} changed" } + let_it_be(:application_redirect_uri) { 'https://foo.bar' } + + it 'allows user to manage applications' do + visit new_application_path + + expect(page).to have_content 'Add new application' + + fill_in :doorkeeper_application_name, with: application_name + fill_in :doorkeeper_application_redirect_uri, with: application_redirect_uri + check :doorkeeper_application_scopes_read_user + click_on 'Save application' + + validate_application(application_name, 'Yes') + + application = Doorkeeper::Application.find_by(name: application_name) + expect(page).to have_css("button[title=\"Copy secret\"][data-clipboard-text=\"#{application.secret}\"]", text: 'Copy') + + click_on 'Edit' + + application_name_changed = "#{application_name} changed" + + fill_in :doorkeeper_application_name, with: application_name_changed + uncheck :doorkeeper_application_confidential + click_on 'Save application' + + validate_application(application_name_changed, 'No') + + visit_applications_path + + page.within '.oauth-applications' do + click_on 'Destroy' + end + expect(page.find('.oauth-applications')).not_to have_content 'test_changed' + end + + context 'when scopes are blank' do + it 'returns an error' do + visit new_application_path + + expect(page).to have_content 'Add new application' + + fill_in :doorkeeper_application_name, with: application_name + fill_in :doorkeeper_application_redirect_uri, with: application_redirect_uri + click_on 'Save application' + + expect(page).to have_content("Scopes can't be blank") + end + end + + def visit_applications_path + visit defined?(applications_path) ? applications_path : new_application_path + end + + def validate_application(name, confidential) + aggregate_failures do + expect(page).to have_content name + expect(page).to have_content 'Application ID' + expect(page).to have_content 'Secret' + expect(page).to have_content "Confidential #{confidential}" + end + end +end diff --git a/spec/support/shared_examples/features/packages_shared_examples.rb b/spec/support/shared_examples/features/packages_shared_examples.rb index 9e88db2e1c0..96be30b9f1f 100644 --- a/spec/support/shared_examples/features/packages_shared_examples.rb +++ b/spec/support/shared_examples/features/packages_shared_examples.rb @@ -14,7 +14,7 @@ RSpec.shared_examples 'packages list' do |check_project_name: false| end def package_table_row(index) - page.all("#{packages_table_selector} > [data-qa-selector=\"package_row\"]")[index].text + page.all("#{packages_table_selector} > [data-qa-selector=\"package_row\"]")[index].text # rubocop:disable QA/SelectorUsage end end @@ -34,10 +34,8 @@ RSpec.shared_examples 'package details link' do |property| expect(page).to have_css('.packages-app h1[data-testid="title"]', text: package.name) - page.within(%Q([name="#{package.name}"])) do - expect(page).to have_content('Installation') - expect(page).to have_content('Registry setup') - end + expect(page).to have_content('Installation') + expect(page).to have_content('Registry setup') end end @@ -92,7 +90,7 @@ RSpec.shared_examples 'shared package sorting' do end def packages_table_selector - '[data-qa-selector="packages-table"]' + '[data-qa-selector="packages-table"]' # rubocop:disable QA/SelectorUsage end def click_sort_option(option, ascending) @@ -100,7 +98,7 @@ def click_sort_option(option, ascending) # Reset the sort direction click_button 'Sort direction' if page.has_selector?('svg[aria-label="Sorting Direction: Ascending"]', wait: 0) - find('button.dropdown-menu-toggle').click + find('button.gl-dropdown-toggle').click page.within('.dropdown-menu') do click_button option diff --git a/spec/support/shared_examples/features/protected_branches_access_control_ce_shared_examples.rb b/spec/support/shared_examples/features/protected_branches_access_control_ce_shared_examples.rb index 56154c7cd03..8212f14d6be 100644 --- a/spec/support/shared_examples/features/protected_branches_access_control_ce_shared_examples.rb +++ b/spec/support/shared_examples/features/protected_branches_access_control_ce_shared_examples.rb @@ -23,6 +23,7 @@ RSpec.shared_examples "protected branches > access control > CE" do end click_on_protect + wait_for_requests expect(ProtectedBranch.count).to eq(1) expect(ProtectedBranch.last.push_access_levels.map(&:access_level)).to eq([access_type_id]) diff --git a/spec/support/shared_examples/features/protected_branches_with_deploy_keys_examples.rb b/spec/support/shared_examples/features/protected_branches_with_deploy_keys_examples.rb index 28fe198c9c3..14142793a0d 100644 --- a/spec/support/shared_examples/features/protected_branches_with_deploy_keys_examples.rb +++ b/spec/support/shared_examples/features/protected_branches_with_deploy_keys_examples.rb @@ -23,7 +23,7 @@ RSpec.shared_examples 'Deploy keys with protected branches' do find(".js-allowed-to-push").click wait_for_requests - within('.qa-allowed-to-push-dropdown') do + within('.qa-allowed-to-push-dropdown') do # rubocop:disable QA/SelectorUsage dropdown_headers = page.all('.dropdown-header').map(&:text) expect(dropdown_headers).to contain_exactly(*all_dropdown_sections) @@ -38,7 +38,7 @@ RSpec.shared_examples 'Deploy keys with protected branches' do find(".js-allowed-to-merge").click wait_for_requests - within('.qa-allowed-to-merge-dropdown') do + within('.qa-allowed-to-merge-dropdown') do # rubocop:disable QA/SelectorUsage dropdown_headers = page.all('.dropdown-header').map(&:text) expect(dropdown_headers).to contain_exactly(*dropdown_sections_minus_deploy_keys) @@ -68,7 +68,7 @@ RSpec.shared_examples 'Deploy keys with protected branches' do find(".js-allowed-to-push").click wait_for_requests - within('.qa-allowed-to-push-dropdown') do + within('.qa-allowed-to-push-dropdown') do # rubocop:disable QA/SelectorUsage dropdown_headers = page.all('.dropdown-header').map(&:text) expect(dropdown_headers).to contain_exactly(*dropdown_sections_minus_deploy_keys) diff --git a/spec/support/shared_examples/features/rss_shared_examples.rb b/spec/support/shared_examples/features/rss_shared_examples.rb index 1b0d3f9605a..c7c2aeea358 100644 --- a/spec/support/shared_examples/features/rss_shared_examples.rb +++ b/spec/support/shared_examples/features/rss_shared_examples.rb @@ -9,7 +9,7 @@ end RSpec.shared_examples "it has an RSS button with current_user's feed token" do it "shows the RSS button with current_user's feed token" do expect(page) - .to have_css("a:has(.qa-rss-icon)[href*='feed_token=#{user.feed_token}']") + .to have_css("a:has(.qa-rss-icon)[href*='feed_token=#{user.feed_token}']") # rubocop:disable QA/SelectorUsage end end @@ -22,6 +22,6 @@ end RSpec.shared_examples "it has an RSS button without a feed token" do it "shows the RSS button without a feed token" do expect(page) - .to have_css("a:has(.qa-rss-icon):not([href*='feed_token'])") + .to have_css("a:has(.qa-rss-icon):not([href*='feed_token'])") # rubocop:disable QA/SelectorUsage end end diff --git a/spec/support/shared_examples/features/variable_list_shared_examples.rb b/spec/support/shared_examples/features/variable_list_shared_examples.rb index 997500415a9..52451839281 100644 --- a/spec/support/shared_examples/features/variable_list_shared_examples.rb +++ b/spec/support/shared_examples/features/variable_list_shared_examples.rb @@ -91,7 +91,7 @@ RSpec.shared_examples 'variable list' do end page.within('#add-ci-variable') do - find('[data-qa-selector="ci_variable_key_field"] input').set('new_key') + find('[data-qa-selector="ci_variable_key_field"] input').set('new_key') # rubocop:disable QA/SelectorUsage click_button('Update variable') end @@ -173,7 +173,7 @@ RSpec.shared_examples 'variable list' do click_button('Add variable') page.within('#add-ci-variable') do - find('[data-qa-selector="ci_variable_key_field"] input').set('empty_mask_key') + find('[data-qa-selector="ci_variable_key_field"] input').set('empty_mask_key') # rubocop:disable QA/SelectorUsage find('[data-testid="ci-variable-protected-checkbox"]').click find('[data-testid="ci-variable-masked-checkbox"]').click @@ -286,8 +286,8 @@ RSpec.shared_examples 'variable list' do wait_for_requests page.within('#add-ci-variable') do - find('[data-qa-selector="ci_variable_key_field"] input').set(key) - find('[data-qa-selector="ci_variable_value_field"]').set(value) if value.present? + find('[data-qa-selector="ci_variable_key_field"] input').set(key) # rubocop:disable QA/SelectorUsage + find('[data-qa-selector="ci_variable_value_field"]').set(value) if value.present? # rubocop:disable QA/SelectorUsage find('[data-testid="ci-variable-protected-checkbox"]').click if protected find('[data-testid="ci-variable-masked-checkbox"]').click if masked diff --git a/spec/support/shared_examples/features/wiki/user_views_asciidoc_page_with_includes_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_views_asciidoc_page_with_includes_shared_examples.rb index 3b2fda4e05b..6fdc5ecae73 100644 --- a/spec/support/shared_examples/features/wiki/user_views_asciidoc_page_with_includes_shared_examples.rb +++ b/spec/support/shared_examples/features/wiki/user_views_asciidoc_page_with_includes_shared_examples.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true RSpec.shared_examples 'User views AsciiDoc page with includes' do - let_it_be(:wiki_content_selector) { '[data-qa-selector=wiki_page_content]' } + let_it_be(:wiki_content_selector) { '[data-qa-selector=wiki_page_content]' } # rubocop:disable QA/SelectorUsage let!(:included_wiki_page) { create_wiki_page('included_page', content: 'Content from the included page')} let!(:wiki_page) { create_wiki_page('home', content: "Content from the main page.\ninclude::included_page.asciidoc[]") } diff --git a/spec/support/shared_examples/finders/security/jobs_finder_shared_examples.rb b/spec/support/shared_examples/finders/security/jobs_finder_shared_examples.rb index a332b213866..117b35201f6 100644 --- a/spec/support/shared_examples/finders/security/jobs_finder_shared_examples.rb +++ b/spec/support/shared_examples/finders/security/jobs_finder_shared_examples.rb @@ -68,20 +68,6 @@ RSpec.shared_examples ::Security::JobsFinder do |default_job_types| end end - context 'when using legacy CI build metadata config storage' do - before do - stub_feature_flags(ci_build_metadata_config: false) - end - - it_behaves_like 'JobsFinder core functionality' - end - - context 'when using the new CI build metadata config storage' do - before do - stub_feature_flags(ci_build_metadata_config: true) - end - - it_behaves_like 'JobsFinder core functionality' - end + it_behaves_like 'JobsFinder core functionality' end end diff --git a/spec/support/shared_examples/lib/gitlab/ci/reports/security/locations/locations_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/ci/reports/security/locations/locations_shared_examples.rb new file mode 100644 index 00000000000..3aa04a77e57 --- /dev/null +++ b/spec/support/shared_examples/lib/gitlab/ci/reports/security/locations/locations_shared_examples.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'vulnerability location' do + describe '#initialize' do + subject { described_class.new(**params) } + + context 'when all params are given' do + it 'initializes an instance' do + expect { subject }.not_to raise_error + + expect(subject).to have_attributes(**params) + end + end + + where(:param) do + mandatory_params + end + + with_them do + context "when param #{params[:param]} is missing" do + before do + params.delete(param) + end + + it 'raises an error' do + expect { subject }.to raise_error(ArgumentError) + end + end + end + end + + describe '#fingerprint' do + subject { described_class.new(**params).fingerprint } + + it "generates expected fingerprint" do + expect(subject).to eq(expected_fingerprint) + end + end + + describe '#fingerprint_path' do + subject { described_class.new(**params).fingerprint_path } + + it "generates expected fingerprint" do + expect(subject).to eq(expected_fingerprint_path) + end + end + + describe '#==' do + let(:location_1) { create(:ci_reports_security_locations_sast) } + let(:location_2) { create(:ci_reports_security_locations_sast) } + + subject { location_1 == location_2 } + + it "returns true when fingerprints are equal" do + allow(location_1).to receive(:fingerprint).and_return('fingerprint') + allow(location_2).to receive(:fingerprint).and_return('fingerprint') + + expect(subject).to eq(true) + end + + it "returns false when fingerprints are different" do + allow(location_1).to receive(:fingerprint).and_return('fingerprint') + allow(location_2).to receive(:fingerprint).and_return('another_fingerprint') + + expect(subject).to eq(false) + end + end +end diff --git a/spec/support/shared_examples/lib/gitlab/database/background_migration_job_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/database/background_migration_job_shared_examples.rb index 20f3270526e..7888ade56eb 100644 --- a/spec/support/shared_examples/lib/gitlab/database/background_migration_job_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/database/background_migration_job_shared_examples.rb @@ -21,3 +21,46 @@ RSpec.shared_examples 'marks background migration job records' do expect(jobs_updated).to eq(1) end end + +RSpec.shared_examples 'finalized background migration' do + it 'processed the scheduled sidekiq queue' do + queued = Sidekiq::ScheduledSet + .new + .select do |scheduled| + scheduled.klass == 'BackgroundMigrationWorker' && + scheduled.args.first == job_class_name + end + expect(queued.size).to eq(0) + end + + it 'processed the async sidekiq queue' do + queued = Sidekiq::Queue.new('BackgroundMigrationWorker') + .select { |scheduled| scheduled.klass == job_class_name } + expect(queued.size).to eq(0) + end + + include_examples 'removed tracked jobs', 'pending' +end + +RSpec.shared_examples 'finalized tracked background migration' do + include_examples 'finalized background migration' + include_examples 'removed tracked jobs', 'succeeded' +end + +RSpec.shared_examples 'removed tracked jobs' do |status| + it "removes '#{status}' tracked jobs" do + jobs = Gitlab::Database::BackgroundMigrationJob + .where(status: Gitlab::Database::BackgroundMigrationJob.statuses[status]) + .for_migration_class(job_class_name) + expect(jobs).to be_empty + end +end + +RSpec.shared_examples 'retained tracked jobs' do |status| + it "retains '#{status}' tracked jobs" do + jobs = Gitlab::Database::BackgroundMigrationJob + .where(status: Gitlab::Database::BackgroundMigrationJob.statuses[status]) + .for_migration_class(job_class_name) + expect(jobs).to be_present + end +end diff --git a/spec/support/shared_examples/lib/gitlab/database/cte_materialized_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/database/cte_materialized_shared_examples.rb index 88e6ffd15a8..a617342ff8c 100644 --- a/spec/support/shared_examples/lib/gitlab/database/cte_materialized_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/database/cte_materialized_shared_examples.rb @@ -11,7 +11,7 @@ RSpec.shared_examples 'CTE with MATERIALIZED keyword examples' do context 'when PG version is <12' do it 'does not add MATERIALIZE keyword' do - allow(Gitlab::Database).to receive(:version).and_return('11.1') + allow(Gitlab::Database.main).to receive(:version).and_return('11.1') expect(query).to include(expected_query_block_without_materialized) end @@ -19,14 +19,14 @@ RSpec.shared_examples 'CTE with MATERIALIZED keyword examples' do context 'when PG version is >=12' do it 'adds MATERIALIZE keyword' do - allow(Gitlab::Database).to receive(:version).and_return('12.1') + allow(Gitlab::Database.main).to receive(:version).and_return('12.1') expect(query).to include(expected_query_block_with_materialized) end context 'when version is higher than 12' do it 'adds MATERIALIZE keyword' do - allow(Gitlab::Database).to receive(:version).and_return('15.1') + allow(Gitlab::Database.main).to receive(:version).and_return('15.1') expect(query).to include(expected_query_block_with_materialized) end diff --git a/spec/support/shared_examples/lib/gitlab/migration_helpers_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/migration_helpers_shared_examples.rb index 72d672fd36c..69a1f7ad11e 100644 --- a/spec/support/shared_examples/lib/gitlab/migration_helpers_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/migration_helpers_shared_examples.rb @@ -14,10 +14,10 @@ RSpec.shared_examples 'performs validation' do |validation_option| it 'performs validation' do expect(model).to receive(:disable_statement_timeout).and_call_original expect(model).to receive(:statement_timeout_disabled?).and_return(false) - expect(model).to receive(:execute).with(/statement_timeout/) + expect(model).to receive(:execute).with(/SET statement_timeout TO/) expect(model).to receive(:execute).ordered.with(/NOT VALID/) expect(model).to receive(:execute).ordered.with(/VALIDATE CONSTRAINT/) - expect(model).to receive(:execute).ordered.with(/RESET ALL/) + expect(model).to receive(:execute).ordered.with(/RESET statement_timeout/) model.add_concurrent_foreign_key(*args, **options.merge(validation_option)) end diff --git a/spec/support/shared_examples/lib/gitlab/sql/set_operator_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/sql/set_operator_shared_examples.rb index 8d758ed1655..ead8b174d46 100644 --- a/spec/support/shared_examples/lib/gitlab/sql/set_operator_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/sql/set_operator_shared_examples.rb @@ -22,7 +22,7 @@ RSpec.shared_examples 'SQL set operator' do |operator_keyword| end it 'skips Model.none segments' do - empty_relation = User.none + empty_relation = User.none.select(:id) set_operator = described_class.new([empty_relation, relation_1, relation_2]) expect {User.where("users.id IN (#{set_operator.to_sql})").to_a}.not_to raise_error @@ -44,6 +44,17 @@ RSpec.shared_examples 'SQL set operator' do |operator_keyword| end end + context 'when uneven select values are used' do + let(:relation_1) { User.where(email: 'alice@example.com').select(*User.column_names) } + let(:relation_2) { User.where(email: 'bob@example.com') } + + it 'raises error' do + expect do + described_class.new([relation_1, relation_2]) + end.to raise_error /Relations with uneven select values were passed/ + end + end + describe 'remove_order parameter' do let(:scopes) do [ diff --git a/spec/support/shared_examples/helpers/groups_shared_examples.rb b/spec/support/shared_examples/lib/menus_shared_examples.rb index 9c74d25b31f..2c2cb362b07 100644 --- a/spec/support/shared_examples/helpers/groups_shared_examples.rb +++ b/spec/support/shared_examples/lib/menus_shared_examples.rb @@ -1,30 +1,16 @@ # frozen_string_literal: true -# This shared_example requires the following variables: -# - current_user -# - group -# - type, the issuable type (ie :issues, :merge_requests) -# - count_service, the Service used by the specified issuable type - -RSpec.shared_examples 'cached issuables count' do - subject { helper.cached_issuables_count(group, type: type) } - - before do - allow(helper).to receive(:current_user) { current_user } - allow(count_service).to receive(:new).and_call_original - end +RSpec.shared_examples_for 'pill_count formatted results' do + let(:count_service) { raise NotImplementedError } - it 'calls the correct service class' do - subject - expect(count_service).to have_received(:new).with(group, current_user) - end + subject(:pill_count) { menu.pill_count } it 'returns all digits for count value under 1000' do allow_next_instance_of(count_service) do |service| allow(service).to receive(:count).and_return(999) end - expect(subject).to eq('999') + expect(pill_count).to eq('999') end it 'returns truncated digits for count value over 1000' do @@ -32,7 +18,7 @@ RSpec.shared_examples 'cached issuables count' do allow(service).to receive(:count).and_return(2300) end - expect(subject).to eq('2.3k') + expect(pill_count).to eq('2.3k') end it 'returns truncated digits for count value over 10000' do @@ -40,7 +26,7 @@ RSpec.shared_examples 'cached issuables count' do allow(service).to receive(:count).and_return(12560) end - expect(subject).to eq('12.6k') + expect(pill_count).to eq('12.6k') end it 'returns truncated digits for count value over 100000' do @@ -48,6 +34,6 @@ RSpec.shared_examples 'cached issuables count' do allow(service).to receive(:count).and_return(112560) end - expect(subject).to eq('112.6k') + expect(pill_count).to eq('112.6k') end end 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 a84658780b9..c6d6ff6bc1d 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,106 +1,126 @@ # 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(:expected_payload_defaults) do + metrics = + ::Gitlab::Metrics::Subscribers::ActiveRecord.load_balancing_metric_counter_keys + + ::Gitlab::Metrics::Subscribers::ActiveRecord.load_balancing_metric_duration_keys + + ::Gitlab::Metrics::Subscribers::ActiveRecord.db_counter_keys + + metrics.each_with_object({}) do |key, result| + result[key] = 0 + end + end + + def transform_hash(hash, another_hash) + another_hash.each do |key, value| + raise "Unexpected key: #{key}" unless hash[key] + end + + hash.merge(another_hash) + end + it 'prevents db counters from leaking to the next transaction' do 2.times do Gitlab::WithRequestStore.with_request_store do subscriber.sql(event) - connection = event.payload[:connection] - - if db_role == :primary - expected = { - db_count: record_query ? 1 : 0, - 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_count: record_query ? 1 : 0, - db_primary_duration_s: record_query ? 0.002 : 0, - db_replica_cached_count: 0, - db_replica_count: 0, - db_replica_duration_s: 0.0, - db_primary_wal_count: record_wal_query ? 1 : 0, - db_primary_wal_cached_count: record_wal_query && record_cached_query ? 1 : 0, - db_replica_wal_cached_count: 0, - db_replica_wal_count: 0 - } - expected[:"db_primary_#{::Gitlab::Database.dbname(connection)}_duration_s"] = 0.002 if record_query - elsif db_role == :replica - expected = { - db_count: record_query ? 1 : 0, - db_write_count: record_write_query ? 1 : 0, - db_cached_count: record_cached_query ? 1 : 0, - db_primary_cached_count: 0, - db_primary_count: 0, - db_primary_duration_s: 0.0, - db_replica_cached_count: record_cached_query ? 1 : 0, - db_replica_count: record_query ? 1 : 0, - db_replica_duration_s: record_query ? 0.002 : 0, - db_replica_wal_count: record_wal_query ? 1 : 0, - db_replica_wal_cached_count: record_wal_query && record_cached_query ? 1 : 0, - db_primary_wal_cached_count: 0, - db_primary_wal_count: 0 - } - expected[:"db_replica_#{::Gitlab::Database.dbname(connection)}_duration_s"] = 0.002 if record_query - else - expected = { - db_count: record_query ? 1 : 0, - db_write_count: record_write_query ? 1 : 0, - db_cached_count: record_cached_query ? 1 : 0 - } - end + + expected = if db_role == :primary + transform_hash(expected_payload_defaults, { + db_count: record_query ? 1 : 0, + 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_primary_count: record_query ? 1 : 0, + "db_primary_#{db_config_name}_count": record_query ? 1 : 0, + db_primary_duration_s: record_query ? 0.002 : 0, + "db_primary_#{db_config_name}_duration_s": record_query ? 0.002 : 0, + db_primary_wal_count: record_wal_query ? 1 : 0, + "db_primary_#{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 + }) + elsif db_role == :replica + transform_hash(expected_payload_defaults, { + db_count: record_query ? 1 : 0, + 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_replica_count: record_query ? 1 : 0, + "db_replica_#{db_config_name}_count": record_query ? 1 : 0, + db_replica_duration_s: record_query ? 0.002 : 0, + "db_replica_#{db_config_name}_duration_s": record_query ? 0.002 : 0, + db_replica_wal_count: record_wal_query ? 1 : 0, + "db_replica_#{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 + }) + else + { + db_count: record_query ? 1 : 0, + db_write_count: record_write_query ? 1 : 0, + db_cached_count: record_cached_query ? 1 : 0 + } + end expect(described_class.db_counter_payload).to eq(expected) end end end - context 'when multiple_database_metrics is disabled' do + context 'when the GITLAB_MULTIPLE_DATABASE_METRICS env var is disabled' do before do - stub_feature_flags(multiple_database_metrics: false) + stub_env('GITLAB_MULTIPLE_DATABASE_METRICS', nil) end it 'does not include per database metrics' do Gitlab::WithRequestStore.with_request_store do subscriber.sql(event) - connection = event.payload[:connection] - expect(described_class.db_counter_payload).not_to include(:"db_replica_#{::Gitlab::Database.dbname(connection)}_duration_s") + expect(described_class.db_counter_payload).not_to include(:"db_replica_#{db_config_name}_duration_s") + expect(described_class.db_counter_payload).not_to include(:"db_replica_#{db_config_name}_count") end end end end RSpec.shared_examples 'record ActiveRecord metrics in a metrics transaction' do |db_role| + let(:db_config_name) { ::Gitlab::Database.db_config_name(ApplicationRecord.connection) } + it 'increments only db counters' do if record_query - expect(transaction).to receive(:increment).with(:gitlab_transaction_db_count_total, 1) - expect(transaction).to receive(:increment).with("gitlab_transaction_db_#{db_role}_count_total".to_sym, 1) if db_role + expect(transaction).to receive(:increment).with(:gitlab_transaction_db_count_total, 1, { db_config_name: db_config_name }) + expect(transaction).to receive(:increment).with("gitlab_transaction_db_#{db_role}_count_total".to_sym, 1, { db_config_name: db_config_name }) if db_role else - expect(transaction).not_to receive(:increment).with(:gitlab_transaction_db_count_total, 1) - expect(transaction).not_to receive(:increment).with("gitlab_transaction_db_#{db_role}_count_total".to_sym, 1) if db_role + expect(transaction).not_to receive(:increment).with(:gitlab_transaction_db_count_total, 1, { db_config_name: db_config_name }) + expect(transaction).not_to receive(:increment).with("gitlab_transaction_db_#{db_role}_count_total".to_sym, 1, { db_config_name: db_config_name }) if db_role end if record_write_query - expect(transaction).to receive(:increment).with(:gitlab_transaction_db_write_count_total, 1) + expect(transaction).to receive(:increment).with(:gitlab_transaction_db_write_count_total, 1, { db_config_name: db_config_name }) else - expect(transaction).not_to receive(:increment).with(:gitlab_transaction_db_write_count_total, 1) + expect(transaction).not_to receive(:increment).with(:gitlab_transaction_db_write_count_total, 1, { db_config_name: db_config_name }) end if record_cached_query - expect(transaction).to receive(:increment).with(:gitlab_transaction_db_cached_count_total, 1) - expect(transaction).to receive(:increment).with("gitlab_transaction_db_#{db_role}_cached_count_total".to_sym, 1) if db_role + expect(transaction).to receive(:increment).with(:gitlab_transaction_db_cached_count_total, 1, { db_config_name: db_config_name }) + expect(transaction).to receive(:increment).with("gitlab_transaction_db_#{db_role}_cached_count_total".to_sym, 1, { db_config_name: db_config_name }) if db_role else - expect(transaction).not_to receive(:increment).with(:gitlab_transaction_db_cached_count_total, 1) - expect(transaction).not_to receive(:increment).with("gitlab_transaction_db_#{db_role}_cached_count_total".to_sym, 1) if db_role + expect(transaction).not_to receive(:increment).with(:gitlab_transaction_db_cached_count_total, 1, { db_config_name: db_config_name }) + expect(transaction).not_to receive(:increment).with("gitlab_transaction_db_#{db_role}_cached_count_total".to_sym, 1, { db_config_name: db_config_name }) if db_role end if record_wal_query if db_role - expect(transaction).to receive(:increment).with("gitlab_transaction_db_#{db_role}_wal_count_total".to_sym, 1) - expect(transaction).to receive(:increment).with("gitlab_transaction_db_#{db_role}_wal_cached_count_total".to_sym, 1) if record_cached_query + expect(transaction).to receive(:increment).with("gitlab_transaction_db_#{db_role}_wal_count_total".to_sym, 1, { db_config_name: db_config_name }) + expect(transaction).to receive(:increment).with("gitlab_transaction_db_#{db_role}_wal_cached_count_total".to_sym, 1, { db_config_name: db_config_name }) if record_cached_query end else - expect(transaction).not_to receive(:increment).with("gitlab_transaction_db_#{db_role}_wal_count_total".to_sym, 1) if db_role + expect(transaction).not_to receive(:increment).with("gitlab_transaction_db_#{db_role}_wal_count_total".to_sym, 1, { db_config_name: db_config_name }) if db_role end subscriber.sql(event) @@ -108,14 +128,34 @@ RSpec.shared_examples 'record ActiveRecord metrics in a metrics transaction' do it 'observes sql_duration metric' do if record_query - expect(transaction).to receive(:observe).with(:gitlab_sql_duration_seconds, 0.002) - expect(transaction).to receive(:observe).with("gitlab_sql_#{db_role}_duration_seconds".to_sym, 0.002) if db_role + expect(transaction).to receive(:observe).with(:gitlab_sql_duration_seconds, 0.002, { db_config_name: db_config_name }) + expect(transaction).to receive(:observe).with("gitlab_sql_#{db_role}_duration_seconds".to_sym, 0.002, { db_config_name: db_config_name }) if db_role else expect(transaction).not_to receive(:observe) end subscriber.sql(event) end + + context 'when the GITLAB_MULTIPLE_DATABASE_METRICS env var is disabled' do + before do + stub_env('GITLAB_MULTIPLE_DATABASE_METRICS', nil) + end + + it 'does not include db_config_name label' do + allow(transaction).to receive(:increment) do |*args| + labels = args[2] || {} + expect(labels).not_to include(:db_config_name) + end + + allow(transaction).to receive(:observe) do |*args| + labels = args[2] || {} + expect(labels).not_to include(:db_config_name) + end + + subscriber.sql(event) + end + end end RSpec.shared_examples 'record ActiveRecord metrics' do |db_role| diff --git a/spec/support/shared_examples/models/concerns/counter_attribute_shared_examples.rb b/spec/support/shared_examples/models/concerns/counter_attribute_shared_examples.rb index 99a09993900..f92ed3d7396 100644 --- a/spec/support/shared_examples/models/concerns/counter_attribute_shared_examples.rb +++ b/spec/support/shared_examples/models/concerns/counter_attribute_shared_examples.rb @@ -62,26 +62,6 @@ RSpec.shared_examples_for CounterAttribute do |counter_attributes| .to raise_error(ActiveModel::MissingAttributeError) end end - - context 'when feature flag is disabled' do - before do - stub_feature_flags(efficient_counter_attribute: false) - end - - it 'delegates to ActiveRecord update!' do - expect { subject } - .to change { model.reset.read_attribute(attribute) }.by(increment) - end - - it 'does not increment the counter in Redis' do - subject - - Gitlab::Redis::SharedState.with do |redis| - counter = redis.get(model.counter_key(attribute)) - expect(counter).to be_nil - end - end - end end end end diff --git a/spec/support/shared_examples/models/concerns/incident_management/escalatable_shared_examples.rb b/spec/support/shared_examples/models/concerns/incident_management/escalatable_shared_examples.rb new file mode 100644 index 00000000000..7b33a95bfa1 --- /dev/null +++ b/spec/support/shared_examples/models/concerns/incident_management/escalatable_shared_examples.rb @@ -0,0 +1,246 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'a model including Escalatable' do + # rubocop:disable Rails/SaveBang -- Usage of factory symbol as argument causes a false-positive + let_it_be(:escalatable_factory) { factory_from_class(described_class) } + let_it_be(:triggered_escalatable, reload: true) { create(escalatable_factory, :triggered) } + let_it_be(:acknowledged_escalatable, reload: true) { create(escalatable_factory, :acknowledged) } + let_it_be(:resolved_escalatable, reload: true) { create(escalatable_factory, :resolved) } + let_it_be(:ignored_escalatable, reload: true) { create(escalatable_factory, :ignored) } + + context 'validations' do + it { is_expected.to validate_presence_of(:status) } + + context 'when status is triggered' do + subject { triggered_escalatable } + + context 'when resolved_at is blank' do + it { is_expected.to be_valid } + end + + context 'when resolved_at is present' do + before do + triggered_escalatable.resolved_at = Time.current + end + + it { is_expected.to be_invalid } + end + end + + context 'when status is acknowledged' do + subject { acknowledged_escalatable } + + context 'when resolved_at is blank' do + it { is_expected.to be_valid } + end + + context 'when resolved_at is present' do + before do + acknowledged_escalatable.resolved_at = Time.current + end + + it { is_expected.to be_invalid } + end + end + + context 'when status is resolved' do + subject { resolved_escalatable } + + context 'when resolved_at is blank' do + before do + resolved_escalatable.resolved_at = nil + end + + it { is_expected.to be_invalid } + end + + context 'when resolved_at is present' do + it { is_expected.to be_valid } + end + end + + context 'when status is ignored' do + subject { ignored_escalatable } + + context 'when resolved_at is blank' do + it { is_expected.to be_valid } + end + + context 'when resolved_at is present' do + before do + ignored_escalatable.resolved_at = Time.current + end + + it { is_expected.to be_invalid } + end + end + end + + context 'scopes' do + let(:all_escalatables) { described_class.where(id: [triggered_escalatable, acknowledged_escalatable, ignored_escalatable, resolved_escalatable])} + + describe '.order_status' do + subject { all_escalatables.order_status(order) } + + context 'descending' do + let(:order) { :desc } + + # Downward arrow in UI always corresponds to default sort + it { is_expected.to eq([triggered_escalatable, acknowledged_escalatable, resolved_escalatable, ignored_escalatable]) } + end + + context 'ascending' do + let(:order) { :asc } + + it { is_expected.to eq([ignored_escalatable, resolved_escalatable, acknowledged_escalatable, triggered_escalatable]) } + end + end + end + + describe '.status_value' do + using RSpec::Parameterized::TableSyntax + + where(:status, :status_value) do + :triggered | 0 + :acknowledged | 1 + :resolved | 2 + :ignored | 3 + :unknown | nil + end + + with_them do + it 'returns status value by its name' do + expect(described_class.status_value(status)).to eq(status_value) + end + end + end + + describe '.status_name' do + using RSpec::Parameterized::TableSyntax + + where(:raw_status, :status) do + 0 | :triggered + 1 | :acknowledged + 2 | :resolved + 3 | :ignored + -1 | nil + end + + with_them do + it 'returns status name by its values' do + expect(described_class.status_name(raw_status)).to eq(status) + end + end + end + + describe '#trigger' do + subject { escalatable.trigger } + + context 'when escalatable is in triggered state' do + let(:escalatable) { triggered_escalatable } + + it 'does not change the escalatable status' do + expect { subject }.not_to change { escalatable.reload.status } + end + end + + context 'when escalatable is not in triggered state' do + let(:escalatable) { resolved_escalatable } + + it 'changes the escalatable status to triggered' do + expect { subject }.to change { escalatable.triggered? }.to(true) + end + + it 'resets resolved at' do + expect { subject }.to change { escalatable.reload.resolved_at }.to nil + end + end + end + + describe '#acknowledge' do + subject { escalatable.acknowledge } + + let(:escalatable) { resolved_escalatable } + + it 'changes the escalatable status to acknowledged' do + expect { subject }.to change { escalatable.acknowledged? }.to(true) + end + + it 'resets ended at' do + expect { subject }.to change { escalatable.reload.resolved_at }.to nil + end + end + + describe '#resolve' do + let!(:resolved_at) { Time.current } + + subject do + escalatable.resolved_at = resolved_at + escalatable.resolve + end + + context 'when escalatable is already resolved' do + let(:escalatable) { resolved_escalatable } + + it 'does not change the escalatable status' do + expect { subject }.not_to change { resolved_escalatable.reload.status } + end + end + + context 'when escalatable is not resolved' do + let(:escalatable) { triggered_escalatable } + + it 'changes escalatable status to "resolved"' do + expect { subject }.to change { escalatable.resolved? }.to(true) + end + end + end + + describe '#ignore' do + subject { escalatable.ignore } + + let(:escalatable) { resolved_escalatable } + + it 'changes the escalatable status to ignored' do + expect { subject }.to change { escalatable.ignored? }.to(true) + end + + it 'resets ended at' do + expect { subject }.to change { escalatable.reload.resolved_at }.to nil + end + end + + describe '#status_event_for' do + using RSpec::Parameterized::TableSyntax + + where(:for_status, :event) do + :triggered | :trigger + 'triggered' | :trigger + :acknowledged | :acknowledge + 'acknowledged' | :acknowledge + :resolved | :resolve + 'resolved' | :resolve + :ignored | :ignore + 'ignored' | :ignore + :unknown | nil + nil | nil + '' | nil + 1 | nil + end + + with_them do + let(:escalatable) { build(escalatable_factory) } + + it 'returns event by status name' do + expect(escalatable.status_event_for(for_status)).to eq(event) + end + end + end + + private + + def factory_from_class(klass) + klass.name.underscore.tr('/', '_') + end +end +# rubocop:enable Rails/SaveBang diff --git a/spec/support/shared_examples/models/cycle_analytics_stage_shared_examples.rb b/spec/support/shared_examples/models/cycle_analytics_stage_shared_examples.rb index cf38a583944..457ee49938f 100644 --- a/spec/support/shared_examples/models/cycle_analytics_stage_shared_examples.rb +++ b/spec/support/shared_examples/models/cycle_analytics_stage_shared_examples.rb @@ -13,6 +13,7 @@ RSpec.shared_examples 'value stream analytics stage' do describe 'associations' do it { is_expected.to belong_to(:end_event_label) } it { is_expected.to belong_to(:start_event_label) } + it { is_expected.to belong_to(:stage_event_hash) } end describe 'validation' do @@ -138,6 +139,67 @@ RSpec.shared_examples 'value stream analytics stage' do expect(stage_1.events_hash_code).not_to eq(stage_2.events_hash_code) end end + + # rubocop: disable Rails/SaveBang + describe '#event_hash' do + it 'associates the same stage event hash record' do + first = create(factory) + second = create(factory) + + expect(first.stage_event_hash_id).to eq(second.stage_event_hash_id) + end + + it 'does not introduce duplicated stage event hash records' do + expect do + create(factory) + create(factory) + end.to change { Analytics::CycleAnalytics::StageEventHash.count }.from(0).to(1) + end + + it 'creates different hash record for different event configurations' do + expect do + create(factory, start_event_identifier: :issue_created, end_event_identifier: :issue_stage_end) + create(factory, start_event_identifier: :merge_request_created, end_event_identifier: :merge_request_merged) + end.to change { Analytics::CycleAnalytics::StageEventHash.count }.from(0).to(2) + end + + context 'when the stage event hash changes' do + let(:stage) { create(factory, start_event_identifier: :issue_created, end_event_identifier: :issue_stage_end) } + + it 'deletes the old, unused stage event hash record' do + old_stage_event_hash = stage.stage_event_hash + + stage.update!(end_event_identifier: :issue_deployed_to_production) + + expect(stage.stage_event_hash_id).not_to eq(old_stage_event_hash.id) + + old_stage_event_hash_from_db = Analytics::CycleAnalytics::StageEventHash.find_by_id(old_stage_event_hash.id) + expect(old_stage_event_hash_from_db).to be_nil + end + + it 'does not delete used stage event hash record' do + other_stage = create(factory, start_event_identifier: :issue_created, end_event_identifier: :issue_stage_end) + + stage.update!(end_event_identifier: :issue_deployed_to_production) + + expect(stage.stage_event_hash_id).not_to eq(other_stage.stage_event_hash_id) + + old_stage_event_hash_from_db = Analytics::CycleAnalytics::StageEventHash.find_by_id(other_stage.stage_event_hash_id) + expect(old_stage_event_hash_from_db).not_to be_nil + end + end + + context 'when the stage events hash code does not change' do + it 'does not trigger extra query on save' do + stage = create(factory, start_event_identifier: :merge_request_created, end_event_identifier: :merge_request_merged) + + expect(Analytics::CycleAnalytics::StageEventHash).not_to receive(:record_id_by_hash_sha256) + + stage.update!(name: 'new title') + end + end + end + # rubocop: enable Rails/SaveBang end RSpec.shared_examples 'value stream analytics label based stage' do diff --git a/spec/support/shared_examples/models/mentionable_shared_examples.rb b/spec/support/shared_examples/models/mentionable_shared_examples.rb index 04630484964..07c5f730e95 100644 --- a/spec/support/shared_examples/models/mentionable_shared_examples.rb +++ b/spec/support/shared_examples/models/mentionable_shared_examples.rb @@ -207,7 +207,7 @@ RSpec.shared_examples 'an editable mentionable' do end RSpec.shared_examples 'mentions in description' do |mentionable_type| - describe 'when storing user mentions' do + shared_examples 'when storing user mentions' do before do mentionable.store_mentions! end @@ -238,10 +238,26 @@ RSpec.shared_examples 'mentions in description' do |mentionable_type| end end end + + context 'when store_mentions_without_subtransaction is enabled' do + before do + stub_feature_flags(store_mentions_without_subtransaction: true) + end + + it_behaves_like 'when storing user mentions' + end + + context 'when store_mentions_without_subtransaction is disabled' do + before do + stub_feature_flags(store_mentions_without_subtransaction: false) + end + + it_behaves_like 'when storing user mentions' + end end RSpec.shared_examples 'mentions in notes' do |mentionable_type| - context 'when mentionable notes contain mentions' do + shared_examples 'when mentionable notes contain mentions' do let(:user) { create(:user) } let(:user2) { create(:user) } let(:group) { create(:group) } @@ -261,6 +277,22 @@ RSpec.shared_examples 'mentions in notes' do |mentionable_type| expect(mentionable.referenced_groups(user)).to eq [group] end end + + context 'when store_mentions_without_subtransaction is enabled' do + before do + stub_feature_flags(store_mentions_without_subtransaction: true) + end + + it_behaves_like 'when mentionable notes contain mentions' + end + + context 'when store_mentions_without_subtransaction is disabled' do + before do + stub_feature_flags(store_mentions_without_subtransaction: false) + end + + it_behaves_like 'when mentionable notes contain mentions' + end end RSpec.shared_examples 'load mentions from DB' do |mentionable_type| @@ -278,7 +310,7 @@ RSpec.shared_examples 'load mentions from DB' do |mentionable_type| context 'when stored user mention contains ids of inexistent records' do before do - user_mention = note.send(:model_user_mention) + user_mention = note.user_mentions.first mention_ids = { mentioned_users_ids: user_mention.mentioned_users_ids.to_a << non_existing_record_id, mentioned_projects_ids: user_mention.mentioned_projects_ids.to_a << non_existing_record_id, @@ -302,7 +334,7 @@ RSpec.shared_examples 'load mentions from DB' do |mentionable_type| let(:group_member) { create(:group_member, user: create(:user), group: private_group) } before do - user_mention = note.send(:model_user_mention) + user_mention = note.user_mentions.first mention_ids = { mentioned_projects_ids: user_mention.mentioned_projects_ids.to_a << private_project.id, mentioned_groups_ids: user_mention.mentioned_groups_ids.to_a << private_group.id diff --git a/spec/support/shared_examples/models/packages/debian/distribution_shared_examples.rb b/spec/support/shared_examples/models/packages/debian/distribution_shared_examples.rb index 5459d17b1df..274fbae3dfd 100644 --- a/spec/support/shared_examples/models/packages/debian/distribution_shared_examples.rb +++ b/spec/support/shared_examples/models/packages/debian/distribution_shared_examples.rb @@ -128,10 +128,6 @@ RSpec.shared_examples 'Debian Distribution' do |factory, container, can_freeze| it { is_expected.not_to allow_value(12.hours.to_i).for(:valid_time_duration_seconds) } end - describe '#signing_keys' do - it { is_expected.to validate_absence_of(:signing_keys) } - end - describe '#file' do it { is_expected.not_to validate_presence_of(:file) } end @@ -141,7 +137,15 @@ RSpec.shared_examples 'Debian Distribution' do |factory, container, can_freeze| end describe '#file_signature' do - it { is_expected.to validate_absence_of(:file_signature) } + it { is_expected.not_to validate_absence_of(:file_signature) } + end + + describe '#signed_file' do + it { is_expected.not_to validate_presence_of(:signed_file) } + end + + describe '#signed_file_store' do + it { is_expected.to validate_presence_of(:signed_file_store) } end end diff --git a/spec/support/shared_examples/models/update_project_statistics_shared_examples.rb b/spec/support/shared_examples/models/update_project_statistics_shared_examples.rb index 7b591ad84d1..2e01de2ea84 100644 --- a/spec/support/shared_examples/models/update_project_statistics_shared_examples.rb +++ b/spec/support/shared_examples/models/update_project_statistics_shared_examples.rb @@ -22,116 +22,6 @@ RSpec.shared_examples 'UpdateProjectStatistics' do |with_counter_attribute| it { is_expected.to be_new_record } - context 'when feature flag efficient_counter_attribute is disabled' do - before do - stub_feature_flags(efficient_counter_attribute: false) - end - - context 'when creating' do - it 'updates the project statistics' do - delta0 = reload_stat - - subject.save! - - delta1 = reload_stat - - expect(delta1).to eq(delta0 + read_attribute) - expect(delta1).to be > delta0 - end - - it 'schedules a namespace statistics worker' do - expect(Namespaces::ScheduleAggregationWorker) - .to receive(:perform_async).once - - subject.save! - end - end - - context 'when updating' do - let(:delta) { 42 } - - before do - subject.save! - end - - it 'updates project statistics' do - expect(ProjectStatistics) - .to receive(:increment_statistic) - .and_call_original - - subject.write_attribute(statistic_attribute, read_attribute + delta) - - expect { subject.save! } - .to change { reload_stat } - .by(delta) - end - - it 'schedules a namespace statistics worker' do - expect(Namespaces::ScheduleAggregationWorker) - .to receive(:perform_async).once - - subject.write_attribute(statistic_attribute, read_attribute + delta) - subject.save! - end - - it 'avoids N + 1 queries' do - subject.write_attribute(statistic_attribute, read_attribute + delta) - - control_count = ActiveRecord::QueryRecorder.new do - subject.save! - end - - subject.write_attribute(statistic_attribute, read_attribute + delta) - - expect do - subject.save! - end.not_to exceed_query_limit(control_count) - end - end - - context 'when destroying' do - before do - subject.save! - end - - it 'updates the project statistics' do - delta0 = reload_stat - - subject.destroy! - - delta1 = reload_stat - - expect(delta1).to eq(delta0 - read_attribute) - expect(delta1).to be < delta0 - end - - it 'schedules a namespace statistics worker' do - expect(Namespaces::ScheduleAggregationWorker) - .to receive(:perform_async).once - - subject.destroy! - end - - context 'when it is destroyed from the project level' do - it 'does not update the project statistics' do - expect(ProjectStatistics) - .not_to receive(:increment_statistic) - - project.update!(pending_delete: true) - project.destroy! - end - - it 'does not schedule a namespace statistics worker' do - expect(Namespaces::ScheduleAggregationWorker) - .not_to receive(:perform_async) - - project.update!(pending_delete: true) - project.destroy! - end - end - end - end - def expect_flush_counter_increments_worker_performed expect(FlushCounterIncrementsWorker) .to receive(:perform_in) diff --git a/spec/support/shared_examples/namespaces/linear_traversal_examples.rb b/spec/support/shared_examples/namespaces/linear_traversal_examples.rb deleted file mode 100644 index 2fd90c36953..00000000000 --- a/spec/support/shared_examples/namespaces/linear_traversal_examples.rb +++ /dev/null @@ -1,23 +0,0 @@ -# frozen_string_literal: true - -# Traversal examples common to linear and recursive methods are in -# spec/support/shared_examples/namespaces/traversal_examples.rb - -RSpec.shared_examples 'linear namespace traversal' do - context 'when use_traversal_ids feature flag is enabled' do - before do - stub_feature_flags(use_traversal_ids: true) - end - - context 'scopes' do - describe '.as_ids' do - let_it_be(:namespace1) { create(:group) } - let_it_be(:namespace2) { create(:group) } - - subject { Namespace.where(id: [namespace1, namespace2]).as_ids.pluck(:id) } - - it { is_expected.to contain_exactly(namespace1.id, namespace2.id) } - end - end - end -end diff --git a/spec/support/shared_examples/namespaces/traversal_examples.rb b/spec/support/shared_examples/namespaces/traversal_examples.rb index f09634556c3..d126b242fb0 100644 --- a/spec/support/shared_examples/namespaces/traversal_examples.rb +++ b/spec/support/shared_examples/namespaces/traversal_examples.rb @@ -55,12 +55,34 @@ RSpec.shared_examples 'namespace traversal' do end describe '#ancestors' do - it 'returns the correct ancestors' do + before do # #reload is called to make sure traversal_ids are reloaded - expect(very_deep_nested_group.reload.ancestors).to contain_exactly(group, nested_group, deep_nested_group) - expect(deep_nested_group.reload.ancestors).to contain_exactly(group, nested_group) - expect(nested_group.reload.ancestors).to contain_exactly(group) - expect(group.reload.ancestors).to eq([]) + reload_models(group, nested_group, deep_nested_group, very_deep_nested_group) + end + + it 'returns the correct ancestors' do + expect(very_deep_nested_group.ancestors).to contain_exactly(group, nested_group, deep_nested_group) + expect(deep_nested_group.ancestors).to contain_exactly(group, nested_group) + expect(nested_group.ancestors).to contain_exactly(group) + expect(group.ancestors).to eq([]) + end + + context 'with asc hierarchy_order' do + it 'returns the correct ancestors' do + expect(very_deep_nested_group.ancestors(hierarchy_order: :asc)).to eq [deep_nested_group, nested_group, group] + expect(deep_nested_group.ancestors(hierarchy_order: :asc)).to eq [nested_group, group] + expect(nested_group.ancestors(hierarchy_order: :asc)).to eq [group] + expect(group.ancestors(hierarchy_order: :asc)).to eq([]) + end + end + + context 'with desc hierarchy_order' do + it 'returns the correct ancestors' do + expect(very_deep_nested_group.ancestors(hierarchy_order: :desc)).to eq [group, nested_group, deep_nested_group] + expect(deep_nested_group.ancestors(hierarchy_order: :desc)).to eq [group, nested_group] + expect(nested_group.ancestors(hierarchy_order: :desc)).to eq [group] + expect(group.ancestors(hierarchy_order: :desc)).to eq([]) + end end describe '#recursive_ancestors' do @@ -78,6 +100,24 @@ RSpec.shared_examples 'namespace traversal' do expect(group.ancestor_ids).to be_empty end + context 'with asc hierarchy_order' do + it 'returns the correct ancestor ids' do + expect(very_deep_nested_group.ancestor_ids(hierarchy_order: :asc)).to eq [deep_nested_group.id, nested_group.id, group.id] + expect(deep_nested_group.ancestor_ids(hierarchy_order: :asc)).to eq [nested_group.id, group.id] + expect(nested_group.ancestor_ids(hierarchy_order: :asc)).to eq [group.id] + expect(group.ancestor_ids(hierarchy_order: :asc)).to eq([]) + end + end + + context 'with desc hierarchy_order' do + it 'returns the correct ancestor ids' do + expect(very_deep_nested_group.ancestor_ids(hierarchy_order: :desc)).to eq [group.id, nested_group.id, deep_nested_group.id] + expect(deep_nested_group.ancestor_ids(hierarchy_order: :desc)).to eq [group.id, nested_group.id] + expect(nested_group.ancestor_ids(hierarchy_order: :desc)).to eq [group.id] + expect(group.ancestor_ids(hierarchy_order: :desc)).to eq([]) + end + end + describe '#recursive_ancestor_ids' do let_it_be(:groups) { [nested_group, deep_nested_group, very_deep_nested_group] } @@ -93,6 +133,24 @@ RSpec.shared_examples 'namespace traversal' do expect(group.self_and_ancestors).to contain_exactly(group) end + context 'with asc hierarchy_order' do + it 'returns the correct ancestors' do + expect(very_deep_nested_group.self_and_ancestors(hierarchy_order: :asc)).to eq [very_deep_nested_group, deep_nested_group, nested_group, group] + expect(deep_nested_group.self_and_ancestors(hierarchy_order: :asc)).to eq [deep_nested_group, nested_group, group] + expect(nested_group.self_and_ancestors(hierarchy_order: :asc)).to eq [nested_group, group] + expect(group.self_and_ancestors(hierarchy_order: :asc)).to eq([group]) + end + end + + context 'with desc hierarchy_order' do + it 'returns the correct ancestors' do + expect(very_deep_nested_group.self_and_ancestors(hierarchy_order: :desc)).to eq [group, nested_group, deep_nested_group, very_deep_nested_group] + expect(deep_nested_group.self_and_ancestors(hierarchy_order: :desc)).to eq [group, nested_group, deep_nested_group] + expect(nested_group.self_and_ancestors(hierarchy_order: :desc)).to eq [group, nested_group] + expect(group.self_and_ancestors(hierarchy_order: :desc)).to eq([group]) + end + end + describe '#recursive_self_and_ancestors' do let_it_be(:groups) { [nested_group, deep_nested_group, very_deep_nested_group] } @@ -108,6 +166,24 @@ RSpec.shared_examples 'namespace traversal' do expect(group.self_and_ancestor_ids).to contain_exactly(group.id) end + context 'with asc hierarchy_order' do + it 'returns the correct ancestor ids' do + expect(very_deep_nested_group.self_and_ancestor_ids(hierarchy_order: :asc)).to eq [very_deep_nested_group.id, deep_nested_group.id, nested_group.id, group.id] + expect(deep_nested_group.self_and_ancestor_ids(hierarchy_order: :asc)).to eq [deep_nested_group.id, nested_group.id, group.id] + expect(nested_group.self_and_ancestor_ids(hierarchy_order: :asc)).to eq [nested_group.id, group.id] + expect(group.self_and_ancestor_ids(hierarchy_order: :asc)).to eq([group.id]) + end + end + + context 'with desc hierarchy_order' do + it 'returns the correct ancestor ids' do + expect(very_deep_nested_group.self_and_ancestor_ids(hierarchy_order: :desc)).to eq [group.id, nested_group.id, deep_nested_group.id, very_deep_nested_group.id] + expect(deep_nested_group.self_and_ancestor_ids(hierarchy_order: :desc)).to eq [group.id, nested_group.id, deep_nested_group.id] + expect(nested_group.self_and_ancestor_ids(hierarchy_order: :desc)).to eq [group.id, nested_group.id] + expect(group.self_and_ancestor_ids(hierarchy_order: :desc)).to eq([group.id]) + end + end + describe '#recursive_self_and_ancestor_ids' do let_it_be(:groups) { [nested_group, deep_nested_group, very_deep_nested_group] } diff --git a/spec/support/shared_examples/namespaces/traversal_scope_examples.rb b/spec/support/shared_examples/namespaces/traversal_scope_examples.rb new file mode 100644 index 00000000000..4d328c03641 --- /dev/null +++ b/spec/support/shared_examples/namespaces/traversal_scope_examples.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'namespace traversal scopes' do + # Hierarchy 1 + let_it_be(:group_1) { create(:group) } + let_it_be(:nested_group_1) { create(:group, parent: group_1) } + let_it_be(:deep_nested_group_1) { create(:group, parent: nested_group_1) } + + # Hierarchy 2 + let_it_be(:group_2) { create(:group) } + let_it_be(:nested_group_2) { create(:group, parent: group_2) } + let_it_be(:deep_nested_group_2) { create(:group, parent: nested_group_2) } + + # All groups + let_it_be(:groups) do + [ + group_1, nested_group_1, deep_nested_group_1, + group_2, nested_group_2, deep_nested_group_2 + ] + end + + describe '.as_ids' do + subject { described_class.where(id: [group_1, group_2]).as_ids.pluck(:id) } + + it { is_expected.to contain_exactly(group_1.id, group_2.id) } + end + + describe '.without_sti_condition' do + subject { described_class.without_sti_condition } + + it { expect(subject.where_values_hash).not_to have_key(:type) } + end + + describe '.self_and_descendants' do + subject { described_class.where(id: [nested_group_1, nested_group_2]).self_and_descendants } + + it { is_expected.to contain_exactly(nested_group_1, deep_nested_group_1, nested_group_2, deep_nested_group_2) } + + context 'with duplicate descendants' do + subject { described_class.where(id: [group_1, group_2, nested_group_1]).self_and_descendants } + + it { is_expected.to match_array(groups) } + end + + context 'when include_self is false' do + subject { described_class.where(id: [nested_group_1, nested_group_2]).self_and_descendants(include_self: false) } + + it { is_expected.to contain_exactly(deep_nested_group_1, deep_nested_group_2) } + end + end + + describe '.self_and_descendant_ids' do + subject { described_class.where(id: [nested_group_1, nested_group_2]).self_and_descendant_ids.pluck(:id) } + + it { is_expected.to contain_exactly(nested_group_1.id, deep_nested_group_1.id, nested_group_2.id, deep_nested_group_2.id) } + + context 'when include_self is false' do + subject do + described_class + .where(id: [nested_group_1, nested_group_2]) + .self_and_descendant_ids(include_self: false) + .pluck(:id) + end + + it { is_expected.to contain_exactly(deep_nested_group_1.id, deep_nested_group_2.id) } + end + end +end diff --git a/spec/support/shared_examples/policies/project_policy_shared_examples.rb b/spec/support/shared_examples/policies/project_policy_shared_examples.rb index 013c9b61b99..a4243db6bc9 100644 --- a/spec/support/shared_examples/policies/project_policy_shared_examples.rb +++ b/spec/support/shared_examples/policies/project_policy_shared_examples.rb @@ -330,3 +330,18 @@ RSpec.shared_examples 'project policies as admin without admin mode' do end end end + +RSpec.shared_examples 'package access with repository disabled' do + context 'when repository is disabled' do + before do + project.project_feature.update!( + # Disable merge_requests and builds as well, since merge_requests and + # builds cannot have higher visibility than repository. + merge_requests_access_level: ProjectFeature::DISABLED, + builds_access_level: ProjectFeature::DISABLED, + repository_access_level: ProjectFeature::DISABLED) + end + + it { is_expected.to be_allowed(:read_package) } + end +end diff --git a/spec/support/shared_examples/requests/api/debian_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/debian_packages_shared_examples.rb index 1f68dd7a382..a3ed74085fb 100644 --- a/spec/support/shared_examples/requests/api/debian_packages_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/debian_packages_shared_examples.rb @@ -4,7 +4,7 @@ RSpec.shared_context 'Debian repository shared context' do |container_type, can_ include_context 'workhorse headers' before do - stub_feature_flags(debian_packages: true) + stub_feature_flags(debian_packages: true, debian_group_packages: true) end let_it_be(:private_container, freeze: can_freeze) { create(container_type, :private) } @@ -29,6 +29,8 @@ RSpec.shared_context 'Debian repository shared context' do |container_type, can_ let_it_be(:public_project) { create(:project, :public, group: public_container) } let_it_be(:private_project_distribution) { create(:debian_project_distribution, container: private_project, codename: 'existing-codename') } let_it_be(:public_project_distribution) { create(:debian_project_distribution, container: public_project, codename: 'existing-codename') } + + let(:project) { { private: private_project, public: public_project }[visibility_level] } else let_it_be(:private_project) { private_container } let_it_be(:public_project) { public_container } @@ -45,12 +47,8 @@ RSpec.shared_context 'Debian repository shared context' do |container_type, can_ let(:architecture) { { private: private_architecture, public: public_architecture }[visibility_level] } let(:component) { { private: private_component, public: public_component }[visibility_level] } let(:component_file) { { private: private_component_file, public: public_component_file }[visibility_level] } - - let(:source_package) { 'sample' } - let(:letter) { source_package[0..2] == 'lib' ? source_package[0..3] : source_package[0] } - let(:package_name) { 'libsample0' } - let(:package_version) { '1.2.3~alpha2' } - let(:file_name) { "#{package_name}_#{package_version}_#{architecture.name}.deb" } + let(:package) { { private: private_package, public: public_package }[visibility_level] } + let(:letter) { package.name[0..2] == 'lib' ? package.name[0..3] : package.name[0] } let(:method) { :get } @@ -94,6 +92,10 @@ RSpec.shared_context 'Debian repository shared context' do |container_type, can_ end end +RSpec.shared_context 'with file_name' do |file_name| + let(:file_name) { file_name } +end + RSpec.shared_context 'Debian repository auth headers' do |user_role, user_token, auth_method = :token| let(:token) { user_token ? personal_access_token.token : 'wrong' } 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 c15c59e1a1d..0390e60747f 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 @@ -46,6 +46,8 @@ RSpec.shared_examples 'handling get metadata requests' do |scope: :project| end shared_examples 'handling all conditions' do + include_context 'dependency proxy helpers context' + where(:auth, :package_name_type, :request_forward, :visibility, :user_role, :expected_result, :expected_status) do nil | :scoped_naming_convention | true | :public | nil | :accept | :ok nil | :scoped_naming_convention | false | :public | nil | :accept | :ok @@ -243,7 +245,7 @@ RSpec.shared_examples 'handling get metadata requests' do |scope: :project| project.send("add_#{user_role}", user) if user_role project.update!(visibility: visibility.to_s) package.update!(name: package_name) unless package_name == 'non-existing-package' - stub_application_setting(npm_package_requests_forwarding: request_forward) + allow_fetch_application_setting(attribute: "npm_package_requests_forwarding", return_value: request_forward) end example_name = "#{params[:expected_result]} metadata request" diff --git a/spec/support/shared_examples/requests/api/npm_packages_tags_shared_examples.rb b/spec/support/shared_examples/requests/api/npm_packages_tags_shared_examples.rb index e6b3dc74b74..86b6975bf9f 100644 --- a/spec/support/shared_examples/requests/api/npm_packages_tags_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/npm_packages_tags_shared_examples.rb @@ -10,9 +10,10 @@ end RSpec.shared_examples 'accept package tags request' do |status:| using RSpec::Parameterized::TableSyntax + include_context 'dependency proxy helpers context' before do - stub_application_setting(npm_package_requests_forwarding: false) + allow_fetch_application_setting(attribute: "npm_package_requests_forwarding", return_value: false) end context 'with valid package name' do 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 8a351226123..ed6d9ed43c8 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 @@ -228,6 +228,35 @@ RSpec.shared_examples 'pypi simple API endpoint' do it_behaves_like 'PyPI package versions', :developer, :success end + + context 'package request forward' do + include_context 'dependency proxy helpers context' + + where(:forward, :package_in_project, :shared_examples_name, :expected_status) do + true | true | 'PyPI package versions' | :success + true | false | 'process PyPI api request' | :redirect + false | true | 'PyPI package versions' | :success + false | false | 'process PyPI api request' | :not_found + end + + with_them do + let_it_be(:package) { create(:pypi_package, project: project, name: 'foobar') } + + let(:package_name) do + if package_in_project + 'foobar' + else + 'barfoo' + end + end + + before do + allow_fetch_application_setting(attribute: "pypi_package_requests_forwarding", return_value: forward) + end + + it_behaves_like params[:shared_examples_name], :reporter, params[:expected_status] + end + end end RSpec.shared_examples 'pypi file download endpoint' do diff --git a/spec/support/shared_examples/requests/api/time_tracking_shared_examples.rb b/spec/support/shared_examples/requests/api/time_tracking_shared_examples.rb index afc902dd184..104e91add8b 100644 --- a/spec/support/shared_examples/requests/api/time_tracking_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/time_tracking_shared_examples.rb @@ -128,17 +128,25 @@ RSpec.shared_examples 'time tracking endpoints' do |issuable_name| if issuable_name == 'merge_request' it 'calls update service with :use_specialized_service param' do - expect(::MergeRequests::UpdateService).to receive(:new).with(project: project, current_user: user, params: hash_including(use_specialized_service: true)) - - post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/add_spent_time", user), params: { duration: '2h' } + expect(::MergeRequests::UpdateService).to receive(:new).with( + project: project, + current_user: user, + params: hash_including( + use_specialized_service: true, + spend_time: hash_including(duration: 7200, summary: 'summary'))) + + post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/add_spent_time", user), params: { duration: '2h', summary: 'summary' } end end if issuable_name == 'issue' it 'calls update service without :use_specialized_service param' do - expect(::Issues::UpdateService).to receive(:new).with(project: project, current_user: user, params: hash_not_including(use_specialized_service: true)) + expect(::Issues::UpdateService).to receive(:new).with( + project: project, + current_user: user, + params: { spend_time: { duration: 3600, summary: 'summary', user_id: user.id } }) - post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/add_spent_time", user), params: { duration: '2h' } + post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/add_spent_time", user), params: { duration: '1h', summary: 'summary' } end end end diff --git a/spec/support/shared_examples/serializers/diff_file_entity_shared_examples.rb b/spec/support/shared_examples/serializers/diff_file_entity_shared_examples.rb index 7608f1c7f8a..32adf98969c 100644 --- a/spec/support/shared_examples/serializers/diff_file_entity_shared_examples.rb +++ b/spec/support/shared_examples/serializers/diff_file_entity_shared_examples.rb @@ -63,3 +63,19 @@ end RSpec.shared_examples 'diff file discussion entity' do it_behaves_like 'diff file base entity' end + +RSpec.shared_examples 'diff file with conflict_type' do + describe '#conflict_type' do + it 'returns nil by default' do + expect(subject[:conflict_type]).to be_nil + end + + context 'when there is matching conflict file' do + let(:options) { { conflicts: { diff_file.new_path => double(diff_lines_for_serializer: [], conflict_type: :both_modified) } } } + + it 'returns false' do + expect(subject[:conflict_type]).to eq(:both_modified) + end + end + end +end diff --git a/spec/support/shared_examples/services/boards/issues_move_service_shared_examples.rb b/spec/support/shared_examples/services/boards/issues_move_service_shared_examples.rb index 7d4fbeea0dc..d9b837258ce 100644 --- a/spec/support/shared_examples/services/boards/issues_move_service_shared_examples.rb +++ b/spec/support/shared_examples/services/boards/issues_move_service_shared_examples.rb @@ -100,8 +100,8 @@ RSpec.shared_examples 'issues move service' do |group| create(:labeled_issue, project: project, labels: [bug, development], assignees: [assignee]) end - it 'returns false' do - expect(described_class.new(parent, user, params).execute(issue)).to eq false + it 'returns nil' do + expect(described_class.new(parent, user, params).execute(issue)).to be_nil end it 'keeps issues labels' do diff --git a/spec/support/shared_examples/services/container_registry_auth_service_shared_examples.rb b/spec/support/shared_examples/services/container_registry_auth_service_shared_examples.rb index eafcbd77040..f6e25ee6647 100644 --- a/spec/support/shared_examples/services/container_registry_auth_service_shared_examples.rb +++ b/spec/support/shared_examples/services/container_registry_auth_service_shared_examples.rb @@ -69,6 +69,10 @@ RSpec.shared_examples 'a browsable' do end RSpec.shared_examples 'an accessible' do + before do + stub_feature_flags(container_registry_migration_phase1: false) + end + let(:access) do [{ 'type' => 'repository', 'name' => project.full_path, @@ -203,9 +207,7 @@ RSpec.shared_examples 'a container registry auth service' do end end - context 'for private project' do - let_it_be(:project) { create(:project) } - + shared_examples 'private project' do context 'allow to use scope-less authentication' do it_behaves_like 'a valid token' end @@ -345,8 +347,20 @@ RSpec.shared_examples 'a container registry auth service' do end end - context 'for public project' do - let_it_be(:project) { create(:project, :public) } + context 'for private project' do + let_it_be_with_reload(:project) { create(:project) } + + it_behaves_like 'private project' + end + + context 'for public project with private container registry' do + let_it_be_with_reload(:project) { create(:project, :public, :container_registry_private) } + + it_behaves_like 'private project' + end + + context 'for public project with container_registry `enabled`' do + let_it_be(:project) { create(:project, :public, :container_registry_enabled) } context 'allow anyone to pull images' do let(:current_params) do @@ -394,8 +408,8 @@ RSpec.shared_examples 'a container registry auth service' do end end - context 'for internal project' do - let_it_be(:project) { create(:project, :internal) } + context 'for internal project with container_registry `enabled`' do + let_it_be(:project) { create(:project, :internal, :container_registry_enabled) } context 'for internal user' do context 'allow anyone to pull images' do @@ -470,6 +484,12 @@ RSpec.shared_examples 'a container registry auth service' do end end end + + context 'for internal project with private container registry' do + let_it_be_with_reload(:project) { create(:project, :internal, :container_registry_private) } + + it_behaves_like 'private project' + end end context 'delete authorized as maintainer' do @@ -630,12 +650,8 @@ RSpec.shared_examples 'a container registry auth service' do end end - context 'for project with private container registry' do - let_it_be(:project, reload: true) { create(:project, :public) } - - before do - project.project_feature.update!(container_registry_access_level: ProjectFeature::PRIVATE) - end + context 'for public project with private container registry' do + let_it_be_with_reload(:project) { create(:project, :public, :container_registry_private) } it_behaves_like 'pullable for being team member' @@ -675,11 +691,7 @@ RSpec.shared_examples 'a container registry auth service' do end context 'for project without container registry' do - let_it_be(:project) { create(:project, :public, container_registry_enabled: false) } - - before do - project.update!(container_registry_enabled: false) - end + let_it_be_with_reload(:project) { create(:project, :public, :container_registry_disabled) } context 'disallow when pulling' do let(:current_params) do @@ -719,12 +731,16 @@ RSpec.shared_examples 'a container registry auth service' do context 'support for multiple scopes' do let_it_be(:internal_project) { create(:project, :internal) } let_it_be(:private_project) { create(:project, :private) } + let_it_be(:public_project) { create(:project, :public) } + let_it_be(:public_project_private_container_registry) { create(:project, :public, :container_registry_private) } let(:current_params) do { scopes: [ "repository:#{internal_project.full_path}:pull", - "repository:#{private_project.full_path}:pull" + "repository:#{private_project.full_path}:pull", + "repository:#{public_project.full_path}:pull", + "repository:#{public_project_private_container_registry.full_path}:pull" ] } end @@ -744,13 +760,19 @@ RSpec.shared_examples 'a container registry auth service' do 'actions' => ['pull'] }, { 'type' => 'repository', 'name' => private_project.full_path, + 'actions' => ['pull'] }, + { 'type' => 'repository', + 'name' => public_project.full_path, + 'actions' => ['pull'] }, + { 'type' => 'repository', + 'name' => public_project_private_container_registry.full_path, 'actions' => ['pull'] } ] end end end - context 'user only has access to internal project' do + context 'user only has access to internal and public projects' do let_it_be(:current_user) { create(:user) } it_behaves_like 'a browsable' do @@ -758,16 +780,35 @@ RSpec.shared_examples 'a container registry auth service' do [ { 'type' => 'repository', 'name' => internal_project.full_path, + 'actions' => ['pull'] }, + { 'type' => 'repository', + 'name' => public_project.full_path, 'actions' => ['pull'] } ] end end end - context 'anonymous access is rejected' do + context 'anonymous user has access only to public project' do let(:current_user) { nil } - it_behaves_like 'a forbidden' + it_behaves_like 'a browsable' do + let(:access) do + [ + { 'type' => 'repository', + 'name' => public_project.full_path, + 'actions' => ['pull'] } + ] + end + end + + context 'with no public container registry' do + before do + public_project.project_feature.update_column(:container_registry_access_level, ProjectFeature::PRIVATE) + end + + it_behaves_like 'a forbidden' + end end end @@ -796,8 +837,8 @@ RSpec.shared_examples 'a container registry auth service' do it_behaves_like 'a forbidden' end - context 'for public project' do - let_it_be(:project) { create(:project, :public) } + context 'for public project with container registry `enabled`' do + let_it_be_with_reload(:project) { create(:project, :public, :container_registry_enabled) } context 'when pulling and pushing' do let(:current_params) do @@ -818,6 +859,19 @@ RSpec.shared_examples 'a container registry auth service' do end end + context 'for public project with container registry `private`' do + let_it_be_with_reload(:project) { create(:project, :public, :container_registry_private) } + + context 'when pulling and pushing' do + let(:current_params) do + { scopes: ["repository:#{project.full_path}:pull,push"] } + end + + it_behaves_like 'a forbidden' + it_behaves_like 'not a container repository factory' + end + end + context 'for registry catalog' do let(:current_params) do { scopes: ["registry:catalog:*"] } @@ -830,15 +884,15 @@ RSpec.shared_examples 'a container registry auth service' do context 'for deploy tokens' do let(:current_params) do - { scopes: ["repository:#{project.full_path}:pull"] } + { scopes: ["repository:#{project.full_path}:pull"], deploy_token: deploy_token } end context 'when deploy token has read and write registry as scopes' do - let(:current_user) { create(:deploy_token, write_registry: true, projects: [project]) } + let(:deploy_token) { create(:deploy_token, write_registry: true, projects: [project]) } shared_examples 'able to login' do context 'registry provides read_container_image authentication_abilities' do - let(:current_params) { {} } + let(:current_params) { { deploy_token: deploy_token } } let(:authentication_abilities) { [:read_container_image] } it_behaves_like 'an authenticated' @@ -854,7 +908,7 @@ RSpec.shared_examples 'a container registry auth service' do context 'when pushing' do let(:current_params) do - { scopes: ["repository:#{project.full_path}:push"] } + { scopes: ["repository:#{project.full_path}:push"], deploy_token: deploy_token } end it_behaves_like 'a pushable' @@ -872,7 +926,7 @@ RSpec.shared_examples 'a container registry auth service' do context 'when pushing' do let(:current_params) do - { scopes: ["repository:#{project.full_path}:push"] } + { scopes: ["repository:#{project.full_path}:push"], deploy_token: deploy_token } end it_behaves_like 'a pushable' @@ -890,7 +944,25 @@ RSpec.shared_examples 'a container registry auth service' do context 'when pushing' do let(:current_params) do - { scopes: ["repository:#{project.full_path}:push"] } + { scopes: ["repository:#{project.full_path}:push"], deploy_token: deploy_token } + end + + it_behaves_like 'a pushable' + end + + it_behaves_like 'able to login' + end + + context 'for public project with private container registry' do + let_it_be_with_reload(:project) { create(:project, :public, :container_registry_private) } + + context 'when pulling' do + it_behaves_like 'a pullable' + end + + context 'when pushing' do + let(:current_params) do + { scopes: ["repository:#{project.full_path}:push"], deploy_token: deploy_token } end it_behaves_like 'a pushable' @@ -901,26 +973,26 @@ RSpec.shared_examples 'a container registry auth service' do end context 'when deploy token does not have read_registry scope' do - let(:current_user) { create(:deploy_token, projects: [project], read_registry: false) } + let(:deploy_token) do + create(:deploy_token, projects: [project], read_registry: false) + end shared_examples 'unable to login' do context 'registry provides no container authentication_abilities' do - let(:current_params) { {} } let(:authentication_abilities) { [] } it_behaves_like 'a forbidden' end context 'registry provides inapplicable container authentication_abilities' do - let(:current_params) { {} } let(:authentication_abilities) { [:download_code] } it_behaves_like 'a forbidden' end end - context 'for public project' do - let_it_be(:project) { create(:project, :public) } + context 'for public project with container registry `enabled`' do + let_it_be_with_reload(:project) { create(:project, :public, :container_registry_enabled) } context 'when pulling' do it_behaves_like 'a pullable' @@ -929,6 +1001,16 @@ RSpec.shared_examples 'a container registry auth service' do it_behaves_like 'unable to login' end + context 'for public project with container registry `private`' do + let_it_be_with_reload(:project) { create(:project, :public, :container_registry_private) } + + context 'when pulling' do + it_behaves_like 'an inaccessible' + end + + it_behaves_like 'unable to login' + end + context 'for internal project' do let_it_be(:project) { create(:project, :internal) } @@ -958,16 +1040,24 @@ RSpec.shared_examples 'a container registry auth service' do end context 'when deploy token is not related to the project' do - let_it_be(:current_user) { create(:deploy_token, read_registry: false) } + let_it_be(:deploy_token) { create(:deploy_token, read_registry: false) } - context 'for public project' do - let_it_be(:project) { create(:project, :public) } + context 'for public project with container registry `enabled`' do + let_it_be_with_reload(:project) { create(:project, :public, :container_registry_enabled) } context 'when pulling' do it_behaves_like 'a pullable' end end + context 'for public project with container registry `private`' do + let_it_be_with_reload(:project) { create(:project, :public, :container_registry_private) } + + context 'when pulling' do + it_behaves_like 'an inaccessible' + end + end + context 'for internal project' do let_it_be(:project) { create(:project, :internal) } @@ -986,14 +1076,20 @@ RSpec.shared_examples 'a container registry auth service' do end context 'when deploy token has been revoked' do - let(:current_user) { create(:deploy_token, :revoked, projects: [project]) } + let(:deploy_token) { create(:deploy_token, :revoked, projects: [project]) } - context 'for public project' do - let_it_be(:project) { create(:project, :public) } + context 'for public project with container registry `enabled`' do + let_it_be(:project) { create(:project, :public, :container_registry_enabled) } it_behaves_like 'a pullable' end + context 'for public project with container registry `private`' do + let_it_be(:project) { create(:project, :public, :container_registry_private) } + + it_behaves_like 'an inaccessible' + end + context 'for internal project' do let_it_be(:project) { create(:project, :internal) } diff --git a/spec/support/shared_examples/services/jira/requests/base_shared_examples.rb b/spec/support/shared_examples/services/jira/requests/base_shared_examples.rb new file mode 100644 index 00000000000..56a6d24d557 --- /dev/null +++ b/spec/support/shared_examples/services/jira/requests/base_shared_examples.rb @@ -0,0 +1,85 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'a service that handles Jira API errors' do + include AfterNextHelpers + using RSpec::Parameterized::TableSyntax + + where(:exception_class, :exception_message, :expected_message) do + Errno::ECONNRESET | '' | 'A connection error occurred' + Errno::ECONNREFUSED | '' | 'A connection error occurred' + Errno::ETIMEDOUT | '' | 'A timeout error occurred' + Timeout::Error | '' | 'A timeout error occurred' + URI::InvalidURIError | '' | 'The Jira API URL' + SocketError | '' | 'The Jira API URL' + OpenSSL::SSL::SSLError | 'foo' | 'An SSL error occurred while connecting to Jira: foo' + JIRA::HTTPError | 'Unauthorized' | 'The credentials for accessing Jira are not valid' + JIRA::HTTPError | 'Forbidden' | 'The credentials for accessing Jira are not allowed' + JIRA::HTTPError | 'Bad Request' | 'An error occurred while requesting data from Jira' + JIRA::HTTPError | 'Foo' | 'An error occurred while requesting data from Jira.' + JIRA::HTTPError | '{"errorMessages":["foo","bar"]}' | 'An error occurred while requesting data from Jira: foo and bar' + JIRA::HTTPError | '{"errorMessages":[""]}' | 'An error occurred while requesting data from Jira.' + end + + with_them do + it 'handles the error' do + stub_client_and_raise(exception_class, exception_message) + + expect(subject).to be_a(ServiceResponse) + expect(subject).to be_error + expect(subject.message).to include(expected_message) + end + end + + context 'when the JSON in JIRA::HTTPError is unsafe' do + before do + stub_client_and_raise(JIRA::HTTPError, error) + end + + context 'when JSON is malformed' do + let(:error) { '{"errorMessages":' } + + it 'returns the default error message' do + expect(subject.message).to eq('An error occurred while requesting data from Jira. Check your Jira integration configuration and try again.') + end + end + + context 'when JSON contains tags' do + let(:error) { '{"errorMessages":["<script>alert(true)</script>foo"]}' } + + it 'sanitizes it' do + expect(subject.message).to eq('An error occurred while requesting data from Jira: foo. Check your Jira integration configuration and try again.') + end + end + end + + it 'allows unknown exception classes to bubble' do + stub_client_and_raise(StandardError) + + expect { subject }.to raise_exception(StandardError) + end + + it 'logs the error' do + stub_client_and_raise(Timeout::Error, 'foo') + + expect(Gitlab::ProjectServiceLogger).to receive(:error).with( + hash_including( + client_url: be_present, + message: 'Error sending message', + service_class: described_class.name, + error: hash_including( + exception_class: Timeout::Error.name, + exception_message: 'foo', + exception_backtrace: be_present + ) + ) + ) + expect(subject).to be_error + end + + def stub_client_and_raise(exception_class, message = '') + # `JIRA::HTTPError` classes take a response from the JIRA API, rather than a `String`. + message = double(body: message) if exception_class == JIRA::HTTPError + + allow_next(JIRA::Client).to receive(:get).and_raise(exception_class, message) + end +end 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 9ffeba1b1d0..c979fdc2bb0 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 @@ -1,165 +1,259 @@ # frozen_string_literal: true RSpec.shared_examples 'Generate Debian Distribution and component files' do - let_it_be(:component_main) { create("debian_#{container_type}_component", distribution: distribution, name: 'main') } - let_it_be(:component_contrib) { create("debian_#{container_type}_component", distribution: distribution, name: 'contrib') } - - let_it_be(:architecture_all) { create("debian_#{container_type}_architecture", distribution: distribution, name: 'all') } - let_it_be(:architecture_amd64) { create("debian_#{container_type}_architecture", distribution: distribution, name: 'amd64') } - let_it_be(:architecture_arm64) { create("debian_#{container_type}_architecture", distribution: distribution, name: 'arm64') } - - let_it_be(:component_file1) { create("debian_#{container_type}_component_file", component: component_contrib, architecture: architecture_all, updated_at: '2020-01-24T08:00:00Z', file_sha256: 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855', file_md5: 'd41d8cd98f00b204e9800998ecf8427e', file_fixture: nil, size: 0) } # updated - let_it_be(:component_file2) { create("debian_#{container_type}_component_file", component: component_main, architecture: architecture_all, updated_at: '2020-01-24T09:00:00Z', file_sha256: 'a') } # destroyed - let_it_be(:component_file3) { create("debian_#{container_type}_component_file", component: component_main, architecture: architecture_amd64, updated_at: '2020-01-24T10:54:59Z', file_sha256: 'b') } # destroyed, 1 second before last generation - let_it_be(:component_file4) { create("debian_#{container_type}_component_file", component: component_contrib, architecture: architecture_all, updated_at: '2020-01-24T10:55:00Z', file_sha256: 'c') } # kept, last generation - let_it_be(:component_file5) { create("debian_#{container_type}_component_file", component: component_contrib, architecture: architecture_all, updated_at: '2020-01-24T10:55:00Z', file_sha256: 'd') } # kept, last generation - let_it_be(:component_file6) { create("debian_#{container_type}_component_file", component: component_contrib, architecture: architecture_amd64, updated_at: '2020-01-25T15:17:18Z', file_sha256: 'e') } # kept, less than 1 hour ago - - def check_component_file(release_date, component_name, component_file_type, architecture_name, expected_content) - component_file = distribution - .component_files - .with_component_name(component_name) - .with_file_type(component_file_type) - .with_architecture_name(architecture_name) - .order_updated_asc - .last - - expect(component_file).not_to be_nil - expect(component_file.updated_at).to eq(release_date) - - unless expected_content.nil? - component_file.file.use_file do |file_path| - expect(File.read(file_path)).to eq(expected_content) - end + def check_release_files(expected_release_content) + distribution.reload + + distribution.file.use_file do |file_path| + expect(File.read(file_path)).to eq(expected_release_content) + end + + expect(distribution.file_signature).to start_with("-----BEGIN PGP SIGNATURE-----\n") + expect(distribution.file_signature).to end_with("\n-----END PGP SIGNATURE-----\n") + + distribution.signed_file.use_file do |file_path| + expect(File.read(file_path)).to start_with("-----BEGIN PGP SIGNED MESSAGE-----\nHash: SHA512\n\n#{expected_release_content}-----BEGIN PGP SIGNATURE-----\n") + expect(File.read(file_path)).to end_with("\n-----END PGP SIGNATURE-----\n") end end - it 'generates Debian distribution and component files', :aggregate_failures do - current_time = Time.utc(2020, 01, 25, 15, 17, 18, 123456) - - travel_to(current_time) do - expect(Gitlab::ErrorTracking).not_to receive(:log_exception) - - initial_count = 6 - destroyed_count = 2 - # updated_count = 1 - created_count = 5 - - expect { subject } - .to not_change { Packages::Package.count } - .and not_change { Packages::PackageFile.count } - .and change { distribution.reload.updated_at }.to(current_time.round) - .and change { distribution.component_files.reset.count }.from(initial_count).to(initial_count - destroyed_count + created_count) - .and change { component_file1.reload.updated_at }.to(current_time.round) - - debs = package.package_files.with_debian_file_type(:deb).preload_debian_file_metadata.to_a - pool_prefix = "pool/unstable/#{project.id}/p/#{package.name}" - expected_main_amd64_content = <<~EOF - Package: libsample0 - Source: #{package.name} - Version: #{package.version} - Installed-Size: 7 - Maintainer: #{debs[0].debian_fields['Maintainer']} - Architecture: amd64 - Description: Some mostly empty lib - Used in GitLab tests. - . - Testing another paragraph. - Multi-Arch: same - Homepage: #{debs[0].debian_fields['Homepage']} - Section: libs - Priority: optional - Filename: #{pool_prefix}/libsample0_1.2.3~alpha2_amd64.deb - Size: 409600 - MD5sum: #{debs[0].file_md5} - SHA256: #{debs[0].file_sha256} - - Package: sample-dev - Source: #{package.name} (#{package.version}) - Version: 1.2.3~binary - Installed-Size: 7 - Maintainer: #{debs[1].debian_fields['Maintainer']} - Architecture: amd64 - Depends: libsample0 (= 1.2.3~binary) - Description: Some mostly empty development files - Used in GitLab tests. - . - Testing another paragraph. - Multi-Arch: same - Homepage: #{debs[1].debian_fields['Homepage']} - Section: libdevel - Priority: optional - Filename: #{pool_prefix}/sample-dev_1.2.3~binary_amd64.deb - Size: 409600 - MD5sum: #{debs[1].file_md5} - SHA256: #{debs[1].file_sha256} - EOF - - check_component_file(current_time.round, 'main', :packages, 'all', nil) - check_component_file(current_time.round, 'main', :packages, 'amd64', expected_main_amd64_content) - check_component_file(current_time.round, 'main', :packages, 'arm64', nil) - - check_component_file(current_time.round, 'contrib', :packages, 'all', nil) - check_component_file(current_time.round, 'contrib', :packages, 'amd64', nil) - check_component_file(current_time.round, 'contrib', :packages, 'arm64', nil) - - main_amd64_size = expected_main_amd64_content.length - main_amd64_md5sum = Digest::MD5.hexdigest(expected_main_amd64_content) - main_amd64_sha256 = Digest::SHA256.hexdigest(expected_main_amd64_content) - - contrib_all_size = component_file1.size - contrib_all_md5sum = component_file1.file_md5 - contrib_all_sha256 = component_file1.file_sha256 - - expected_release_content = <<~EOF - Codename: unstable - Date: Sat, 25 Jan 2020 15:17:18 +0000 - Valid-Until: Mon, 27 Jan 2020 15:17:18 +0000 - Architectures: all amd64 arm64 - Components: contrib main - MD5Sum: - #{contrib_all_md5sum} #{contrib_all_size} contrib/binary-all/Packages - d41d8cd98f00b204e9800998ecf8427e 0 contrib/binary-amd64/Packages - d41d8cd98f00b204e9800998ecf8427e 0 contrib/binary-arm64/Packages - d41d8cd98f00b204e9800998ecf8427e 0 main/binary-all/Packages - #{main_amd64_md5sum} #{main_amd64_size} main/binary-amd64/Packages - d41d8cd98f00b204e9800998ecf8427e 0 main/binary-arm64/Packages - SHA256: - #{contrib_all_sha256} #{contrib_all_size} contrib/binary-all/Packages - e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 contrib/binary-amd64/Packages - e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 contrib/binary-arm64/Packages - e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 main/binary-all/Packages - #{main_amd64_sha256} #{main_amd64_size} main/binary-amd64/Packages - e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 main/binary-arm64/Packages - EOF - - distribution.file.use_file do |file_path| - expect(File.read(file_path)).to eq(expected_release_content) + context 'with Debian components and architectures' do + let_it_be(:component_main) { create("debian_#{container_type}_component", distribution: distribution, name: 'main') } + let_it_be(:component_contrib) { create("debian_#{container_type}_component", distribution: distribution, name: 'contrib') } + + let_it_be(:architecture_all) { create("debian_#{container_type}_architecture", distribution: distribution, name: 'all') } + let_it_be(:architecture_amd64) { create("debian_#{container_type}_architecture", distribution: distribution, name: 'amd64') } + let_it_be(:architecture_arm64) { create("debian_#{container_type}_architecture", distribution: distribution, name: 'arm64') } + + let_it_be(:component_file1) { create("debian_#{container_type}_component_file", component: component_contrib, architecture: architecture_all, updated_at: '2020-01-24T08:00:00Z', file_sha256: 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855', file_md5: 'd41d8cd98f00b204e9800998ecf8427e', file_fixture: nil, size: 0) } # updated + let_it_be(:component_file2) { create("debian_#{container_type}_component_file", component: component_main, architecture: architecture_all, updated_at: '2020-01-24T09:00:00Z', file_sha256: 'a') } # destroyed + let_it_be(:component_file3) { create("debian_#{container_type}_component_file", component: component_main, architecture: architecture_amd64, updated_at: '2020-01-24T10:54:59Z', file_sha256: 'b') } # destroyed, 1 second before last generation + let_it_be(:component_file4) { create("debian_#{container_type}_component_file", component: component_contrib, architecture: architecture_all, updated_at: '2020-01-24T10:55:00Z', file_sha256: 'c') } # kept, last generation + let_it_be(:component_file5) { create("debian_#{container_type}_component_file", component: component_contrib, architecture: architecture_all, updated_at: '2020-01-24T10:55:00Z', file_sha256: 'd') } # kept, last generation + let_it_be(:component_file6) { create("debian_#{container_type}_component_file", component: component_contrib, architecture: architecture_amd64, updated_at: '2020-01-25T15:17:18Z', file_sha256: 'e') } # kept, less than 1 hour ago + + def check_component_file(release_date, component_name, component_file_type, architecture_name, expected_content) + component_file = distribution + .component_files + .with_component_name(component_name) + .with_file_type(component_file_type) + .with_architecture_name(architecture_name) + .order_updated_asc + .last + + expect(component_file).not_to be_nil + expect(component_file.updated_at).to eq(release_date) + + unless expected_content.nil? + component_file.file.use_file do |file_path| + expect(File.read(file_path)).to eq(expected_content) + end end end + + it 'generates Debian distribution and component files', :aggregate_failures do + current_time = Time.utc(2020, 01, 25, 15, 17, 18, 123456) + + travel_to(current_time) do + expect(Gitlab::ErrorTracking).not_to receive(:log_exception) + + components_count = 2 + architectures_count = 3 + + initial_count = 6 + destroyed_count = 2 + updated_count = 1 + created_count = components_count * (architectures_count * 2 + 1) - updated_count + + expect { subject } + .to not_change { Packages::Package.count } + .and not_change { Packages::PackageFile.count } + .and change { distribution.reload.updated_at }.to(current_time.round) + .and change { distribution.component_files.reset.count }.from(initial_count).to(initial_count - destroyed_count + created_count) + .and change { component_file1.reload.updated_at }.to(current_time.round) + + package_files = package.package_files.order(id: :asc).preload_debian_file_metadata.to_a + pool_prefix = 'pool/unstable' + pool_prefix += "/#{project.id}" if container_type == :group + pool_prefix += "/p/#{package.name}/#{package.version}" + expected_main_amd64_content = <<~EOF + Package: libsample0 + Source: #{package.name} + Version: #{package.version} + Installed-Size: 7 + Maintainer: #{package_files[2].debian_fields['Maintainer']} + Architecture: amd64 + Description: Some mostly empty lib + Used in GitLab tests. + . + Testing another paragraph. + Multi-Arch: same + Homepage: #{package_files[2].debian_fields['Homepage']} + Section: libs + Priority: optional + Filename: #{pool_prefix}/libsample0_1.2.3~alpha2_amd64.deb + Size: 409600 + MD5sum: #{package_files[2].file_md5} + SHA256: #{package_files[2].file_sha256} + + Package: sample-dev + Source: #{package.name} (#{package.version}) + Version: 1.2.3~binary + Installed-Size: 7 + Maintainer: #{package_files[3].debian_fields['Maintainer']} + Architecture: amd64 + Depends: libsample0 (= 1.2.3~binary) + Description: Some mostly empty development files + Used in GitLab tests. + . + Testing another paragraph. + Multi-Arch: same + Homepage: #{package_files[3].debian_fields['Homepage']} + Section: libdevel + Priority: optional + Filename: #{pool_prefix}/sample-dev_1.2.3~binary_amd64.deb + Size: 409600 + MD5sum: #{package_files[3].file_md5} + SHA256: #{package_files[3].file_sha256} + EOF + + expected_main_amd64_di_content = <<~EOF + Section: misc + Priority: extra + Filename: #{pool_prefix}/sample-udeb_1.2.3~alpha2_amd64.udeb + Size: 409600 + MD5sum: #{package_files[4].file_md5} + SHA256: #{package_files[4].file_sha256} + EOF + + expected_main_source_content = <<~EOF + Package: #{package.name} + Binary: sample-dev, libsample0, sample-udeb + Version: #{package.version} + Maintainer: #{package_files[1].debian_fields['Maintainer']} + Build-Depends: debhelper-compat (= 13) + Architecture: any + Standards-Version: 4.5.0 + Format: 3.0 (native) + Files: + #{package_files[1].file_md5} #{package_files[1].size} #{package_files[1].file_name} + d5ca476e4229d135a88f9c729c7606c9 864 sample_1.2.3~alpha2.tar.xz + Checksums-Sha256: + #{package_files[1].file_sha256} #{package_files[1].size} #{package_files[1].file_name} + 40e4682bb24a73251ccd7c7798c0094a649091e5625d6a14bcec9b4e7174f3da 864 sample_1.2.3~alpha2.tar.xz + Checksums-Sha1: + #{package_files[1].file_sha1} #{package_files[1].size} #{package_files[1].file_name} + c5cfc111ea924842a89a06d5673f07dfd07de8ca 864 sample_1.2.3~alpha2.tar.xz + Homepage: #{package_files[1].debian_fields['Homepage']} + Section: misc + Priority: extra + Directory: #{pool_prefix} + EOF + + check_component_file(current_time.round, 'main', :packages, 'all', nil) + check_component_file(current_time.round, 'main', :packages, 'amd64', expected_main_amd64_content) + check_component_file(current_time.round, 'main', :packages, 'arm64', nil) + + check_component_file(current_time.round, 'main', :di_packages, 'all', nil) + 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, 'contrib', :packages, 'all', nil) + check_component_file(current_time.round, 'contrib', :packages, 'amd64', nil) + check_component_file(current_time.round, 'contrib', :packages, 'arm64', nil) + + check_component_file(current_time.round, 'contrib', :di_packages, 'all', nil) + 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) + + main_amd64_size = expected_main_amd64_content.length + main_amd64_md5sum = Digest::MD5.hexdigest(expected_main_amd64_content) + main_amd64_sha256 = Digest::SHA256.hexdigest(expected_main_amd64_content) + + contrib_all_size = component_file1.size + contrib_all_md5sum = component_file1.file_md5 + contrib_all_sha256 = component_file1.file_sha256 + + main_amd64_di_size = expected_main_amd64_di_content.length + 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) + + expected_release_content = <<~EOF + Codename: unstable + Date: Sat, 25 Jan 2020 15:17:18 +0000 + Valid-Until: Mon, 27 Jan 2020 15:17:18 +0000 + Architectures: all amd64 arm64 + Components: contrib main + MD5Sum: + #{contrib_all_md5sum} #{contrib_all_size} contrib/binary-all/Packages + d41d8cd98f00b204e9800998ecf8427e 0 contrib/debian-installer/binary-all/Packages + d41d8cd98f00b204e9800998ecf8427e 0 contrib/binary-amd64/Packages + 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 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 + SHA256: + #{contrib_all_sha256} #{contrib_all_size} contrib/binary-all/Packages + e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 contrib/debian-installer/binary-all/Packages + e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 contrib/binary-amd64/Packages + 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 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 + EOF + + check_release_files(expected_release_content) + end + + create_list(:debian_package, 10, project: project, published_in: project_distribution) + control_count = ActiveRecord::QueryRecorder.new { subject2 }.count + + create_list(:debian_package, 10, project: project, published_in: project_distribution) + expect { subject3 }.not_to exceed_query_limit(control_count) + end end -end -RSpec.shared_examples 'Generate minimal Debian Distribution' do - it 'generates minimal distribution', :aggregate_failures do - travel_to(Time.utc(2020, 01, 25, 15, 17, 18, 123456)) do - expect(Gitlab::ErrorTracking).not_to receive(:log_exception) - - expect { subject } - .to not_change { Packages::Package.count } - .and not_change { Packages::PackageFile.count } - .and not_change { distribution.component_files.reset.count } - - expected_release_content = <<~EOF - Codename: unstable - Date: Sat, 25 Jan 2020 15:17:18 +0000 - Valid-Until: Mon, 27 Jan 2020 15:17:18 +0000 - MD5Sum: - SHA256: - EOF - - distribution.file.use_file do |file_path| - expect(File.read(file_path)).to eq(expected_release_content) + context 'without components and architectures' do + it 'generates minimal distribution', :aggregate_failures do + travel_to(Time.utc(2020, 01, 25, 15, 17, 18, 123456)) do + expect(Gitlab::ErrorTracking).not_to receive(:log_exception) + + expect { subject } + .to not_change { Packages::Package.count } + .and not_change { Packages::PackageFile.count } + .and not_change { distribution.component_files.reset.count } + + expected_release_content = <<~EOF + Codename: unstable + Date: Sat, 25 Jan 2020 15:17:18 +0000 + Valid-Until: Mon, 27 Jan 2020 15:17:18 +0000 + MD5Sum: + SHA256: + EOF + + check_release_files(expected_release_content) end end end |