diff options
Diffstat (limited to 'spec/support/shared_examples')
42 files changed, 1232 insertions, 475 deletions
diff --git a/spec/support/shared_examples/bulk_imports/common/pipelines/wiki_pipeline_examples.rb b/spec/support/shared_examples/bulk_imports/common/pipelines/wiki_pipeline_examples.rb new file mode 100644 index 00000000000..e8cc666605b --- /dev/null +++ b/spec/support/shared_examples/bulk_imports/common/pipelines/wiki_pipeline_examples.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'wiki pipeline imports a wiki for an entity' do + describe '#run' do + let_it_be(:bulk_import_configuration) { create(:bulk_import_configuration, bulk_import: bulk_import) } + + let_it_be(:tracker) { create(:bulk_import_tracker, entity: entity) } + let_it_be(:context) { BulkImports::Pipeline::Context.new(tracker) } + + let(:extracted_data) { BulkImports::Pipeline::ExtractedData.new(data: {}) } + + context 'successfully imports wiki for an entity' do + subject { described_class.new(context) } + + before do + allow_next_instance_of(BulkImports::Common::Extractors::GraphqlExtractor) do |extractor| + allow(extractor).to receive(:extract).and_return(extracted_data) + end + end + + it 'imports new wiki into destination project' do + expect_next_instance_of(Gitlab::GitalyClient::RepositoryService) do |repository_service| + url = "https://oauth2:token@gitlab.example/#{entity.source_full_path}.wiki.git" + expect(repository_service).to receive(:fetch_remote).with(url, any_args).and_return 0 + end + + subject.run + end + end + end +end diff --git a/spec/support/shared_examples/controllers/concerns/integrations_actions_shared_examples.rb b/spec/support/shared_examples/controllers/concerns/integrations/integrations_actions_shared_examples.rb index 748a3acf17b..a8aed0c1f0b 100644 --- a/spec/support/shared_examples/controllers/concerns/integrations_actions_shared_examples.rb +++ b/spec/support/shared_examples/controllers/concerns/integrations/integrations_actions_shared_examples.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -RSpec.shared_examples IntegrationsActions do +RSpec.shared_examples Integrations::Actions do let(:integration) do create(:datadog_integration, integration_attributes.merge( diff --git a/spec/support/shared_examples/controllers/create_notes_rate_limit_shared_examples.rb b/spec/support/shared_examples/controllers/create_notes_rate_limit_shared_examples.rb index 74a98c20383..8affe4ac8f5 100644 --- a/spec/support/shared_examples/controllers/create_notes_rate_limit_shared_examples.rb +++ b/spec/support/shared_examples/controllers/create_notes_rate_limit_shared_examples.rb @@ -6,39 +6,41 @@ # - request_full_path RSpec.shared_examples 'request exceeding rate limit' do - before do - stub_application_setting(notes_create_limit: 2) - 2.times { post :create, params: params } - end + context 'with rate limiter', :freeze_time, :clean_gitlab_redis_rate_limiting do + before do + stub_application_setting(notes_create_limit: 2) + 2.times { post :create, params: params } + end - it 'prevents from creating more notes', :request_store do - expect { post :create, params: params } - .to change { Note.count }.by(0) + it 'prevents from creating more notes' do + expect { post :create, params: params } + .to change { Note.count }.by(0) - expect(response).to have_gitlab_http_status(:too_many_requests) - expect(response.body).to eq(_('This endpoint has been requested too many times. Try again later.')) - end + expect(response).to have_gitlab_http_status(:too_many_requests) + expect(response.body).to eq(_('This endpoint has been requested too many times. Try again later.')) + end - it 'logs the event in auth.log' do - attributes = { - message: 'Application_Rate_Limiter_Request', - env: :notes_create_request_limit, - remote_ip: '0.0.0.0', - request_method: 'POST', - path: request_full_path, - user_id: user.id, - username: user.username - } + it 'logs the event in auth.log' do + attributes = { + message: 'Application_Rate_Limiter_Request', + env: :notes_create_request_limit, + remote_ip: '0.0.0.0', + request_method: 'POST', + path: request_full_path, + user_id: user.id, + username: user.username + } - expect(Gitlab::AuthLogger).to receive(:error).with(attributes).once - post :create, params: params - end + expect(Gitlab::AuthLogger).to receive(:error).with(attributes).once + post :create, params: params + end - it 'allows user in allow-list to create notes, even if the case is different' do - user.update_attribute(:username, user.username.titleize) - stub_application_setting(notes_create_limit_allowlist: ["#{user.username.downcase}"]) + it 'allows user in allow-list to create notes, even if the case is different' do + user.update_attribute(:username, user.username.titleize) + stub_application_setting(notes_create_limit_allowlist: ["#{user.username.downcase}"]) - post :create, params: params - expect(response).to have_gitlab_http_status(:found) + post :create, params: params + expect(response).to have_gitlab_http_status(:found) + end end end diff --git a/spec/support/shared_examples/features/2fa_shared_examples.rb b/spec/support/shared_examples/features/2fa_shared_examples.rb index ddc03e178ba..94c91556ea7 100644 --- a/spec/support/shared_examples/features/2fa_shared_examples.rb +++ b/spec/support/shared_examples/features/2fa_shared_examples.rb @@ -18,6 +18,7 @@ RSpec.shared_examples 'hardware device for 2fa' do |device_type| let(:user) { create(:user) } before do + stub_feature_flags(bootstrap_confirmation_modals: false) gitlab_sign_in(user) user.update_attribute(:otp_required_for_login, true) 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 index d29c677a962..5d1488502d2 100644 --- a/spec/support/shared_examples/features/dependency_proxy_shared_examples.rb +++ b/spec/support/shared_examples/features/dependency_proxy_shared_examples.rb @@ -26,7 +26,7 @@ RSpec.shared_examples 'a successful manifest pull' do subject expect(response).to have_gitlab_http_status(:ok) - expect(response.headers['Docker-Content-Digest']).to eq(manifest.digest) + expect(response.headers[DependencyProxy::Manifest::DIGEST_HEADER]).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}\"") diff --git a/spec/support/shared_examples/features/manage_applications_shared_examples.rb b/spec/support/shared_examples/features/manage_applications_shared_examples.rb index 0161899cb76..27d50c67f24 100644 --- a/spec/support/shared_examples/features/manage_applications_shared_examples.rb +++ b/spec/support/shared_examples/features/manage_applications_shared_examples.rb @@ -18,6 +18,7 @@ RSpec.shared_examples 'manage applications' do click_on 'Save application' validate_application(application_name, 'Yes') + expect(page).to have_link('Continue', href: index_path) application = Doorkeeper::Application.find_by(name: application_name) expect(page).to have_css("button[title=\"Copy secret\"][data-clipboard-text=\"#{application.secret}\"]", text: 'Copy') @@ -33,6 +34,7 @@ RSpec.shared_examples 'manage applications' do click_on 'Save application' validate_application(application_name_changed, 'No') + expect(page).not_to have_link('Continue') visit_applications_path diff --git a/spec/support/shared_examples/features/packages_shared_examples.rb b/spec/support/shared_examples/features/packages_shared_examples.rb index 96be30b9f1f..d14b4638ca5 100644 --- a/spec/support/shared_examples/features/packages_shared_examples.rb +++ b/spec/support/shared_examples/features/packages_shared_examples.rb @@ -21,10 +21,6 @@ end RSpec.shared_examples 'package details link' do |property| let(:package) { packages.first } - before do - stub_feature_flags(packages_details_one_column: false) - end - it 'navigates to the correct url' do page.within(packages_table_selector) do click_link package.name @@ -32,7 +28,7 @@ RSpec.shared_examples 'package details link' do |property| expect(page).to have_current_path(project_package_path(package.project, package)) - expect(page).to have_css('.packages-app h1[data-testid="title"]', text: package.name) + expect(page).to have_css('.packages-app h2[data-testid="title"]', text: package.name) expect(page).to have_content('Installation') expect(page).to have_content('Registry setup') @@ -94,16 +90,24 @@ def packages_table_selector end def click_sort_option(option, ascending) - page.within('.gl-sorting') do - # Reset the sort direction - click_button 'Sort direction' if page.has_selector?('svg[aria-label="Sorting Direction: Ascending"]', wait: 0) + wait_for_requests - find('button.gl-dropdown-toggle').click + # Reset the sort direction + if page.has_selector?('button[aria-label="Sorting Direction: Ascending"]', wait: 0) && !ascending + click_button 'Sort direction' - page.within('.dropdown-menu') do - click_button option - end + wait_for_requests + end + + find('button.gl-dropdown-toggle').click + + page.within('.dropdown-menu') do + click_button option + end + + if ascending + wait_for_requests - click_button 'Sort direction' if ascending + click_button 'Sort direction' end end diff --git a/spec/support/shared_examples/features/resolving_discussions_in_issues_shared_examples.rb b/spec/support/shared_examples/features/resolving_discussions_in_issues_shared_examples.rb index 6d44a6fde85..337b3f3cbd0 100644 --- a/spec/support/shared_examples/features/resolving_discussions_in_issues_shared_examples.rb +++ b/spec/support/shared_examples/features/resolving_discussions_in_issues_shared_examples.rb @@ -1,43 +1,29 @@ # frozen_string_literal: true RSpec.shared_examples 'creating an issue for a thread' do - it 'shows an issue with the title filled in' do + it 'shows an issue creation form' do + # Title field is filled in title_field = page.find_field('issue[title]') - expect(title_field.value).to include(merge_request.title) - end - it 'has a mention of the discussion in the description' do - description_field = page.find_field('issue[description]') + # Has a hidden field for the merge request + merge_request_field = find('#merge_request_to_resolve_discussions_of', visible: false) + expect(merge_request_field.value).to eq(merge_request.iid.to_s) + # Has a mention of the discussion in the description + description_field = page.find_field('issue[description]') expect(description_field.value).to include(discussion.first_note.note) end - it 'can create a new issue for the project' do + it 'creates a new issue for the project' do + # Actually creates an issue for the project expect { click_button 'Create issue' }.to change { project.issues.reload.size }.by(1) - end - - it 'resolves the discussion in the merge request' do - click_button 'Create issue' + # Resolves the discussion in the merge request discussion.first_note.reload - expect(discussion.resolved?).to eq(true) - end - - it 'shows a flash messaage after resolving a discussion' do - click_button 'Create issue' - - page.within '.flash-notice' do - # Only check for the word 'Resolved' since the spec might have resolved - # multiple discussions - expect(page).to have_content('Resolved') - end - end - - it 'has a hidden field for the merge request' do - merge_request_field = find('#merge_request_to_resolve_discussions_of', visible: false) - expect(merge_request_field.value).to eq(merge_request.iid.to_s) + # Issue title inludes MR title + expect(page).to have_content(%Q(Follow-up from "#{merge_request.title}")) end end diff --git a/spec/support/shared_examples/features/sidebar_shared_examples.rb b/spec/support/shared_examples/features/sidebar_shared_examples.rb index 5bfe929e957..d509d124de0 100644 --- a/spec/support/shared_examples/features/sidebar_shared_examples.rb +++ b/spec/support/shared_examples/features/sidebar_shared_examples.rb @@ -52,16 +52,17 @@ RSpec.shared_examples 'issue boards sidebar' do it 'shows toggle as on then as off as user toggles to subscribe and unsubscribe', :aggregate_failures do wait_for_requests + subscription_button = find('[data-testid="subscription-toggle"]') - click_button 'Notifications' + subscription_button.click - expect(page).to have_button('Notifications', class: 'is-checked') + expect(subscription_button).to have_css("button.is-checked") - click_button 'Notifications' + subscription_button.click wait_for_requests - expect(page).not_to have_button('Notifications', class: 'is-checked') + expect(subscription_button).to have_css("button:not(.is-checked)") end context 'when notifications have been disabled' do @@ -73,7 +74,7 @@ RSpec.shared_examples 'issue boards sidebar' do it 'displays a message that notifications have been disabled' do page.within('[data-testid="sidebar-notifications"]') do - expect(page).to have_button('Notifications', class: 'is-disabled') + expect(page).to have_selector('[data-testid="subscription-toggle"]', class: 'is-disabled') expect(page).to have_content('Disabled by project owner') end end diff --git a/spec/support/shared_examples/graphql/notes_creation_shared_examples.rb b/spec/support/shared_examples/graphql/notes_creation_shared_examples.rb index fb598b978f6..56b6dc682eb 100644 --- a/spec/support/shared_examples/graphql/notes_creation_shared_examples.rb +++ b/spec/support/shared_examples/graphql/notes_creation_shared_examples.rb @@ -66,20 +66,22 @@ RSpec.shared_examples 'a Note mutation when the given resource id is not for a N end RSpec.shared_examples 'a Note mutation when there are rate limit validation errors' do - before do - stub_application_setting(notes_create_limit: 3) - 3.times { post_graphql_mutation(mutation, current_user: current_user) } - end - - it_behaves_like 'a Note mutation that does not create a Note' - it_behaves_like 'a mutation that returns top-level errors', - errors: ['This endpoint has been requested too many times. Try again later.'] - - context 'when the user is in the allowlist' do + context 'with rate limiter', :freeze_time, :clean_gitlab_redis_rate_limiting do before do - stub_application_setting(notes_create_limit_allowlist: ["#{current_user.username}"]) + stub_application_setting(notes_create_limit: 3) + 3.times { post_graphql_mutation(mutation, current_user: current_user) } end - it_behaves_like 'a Note mutation that creates a Note' + it_behaves_like 'a Note mutation that does not create a Note' + it_behaves_like 'a mutation that returns top-level errors', + errors: ['This endpoint has been requested too many times. Try again later.'] + + context 'when the user is in the allowlist' do + before do + stub_application_setting(notes_create_limit_allowlist: ["#{current_user.username}"]) + end + + it_behaves_like 'a Note mutation that creates a Note' + end end end diff --git a/spec/support/shared_examples/lib/gitlab/ci/ci_trace_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/ci/ci_trace_shared_examples.rb index 8b4ecd7d5ae..a3c67210a4a 100644 --- a/spec/support/shared_examples/lib/gitlab/ci/ci_trace_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/ci/ci_trace_shared_examples.rb @@ -35,8 +35,8 @@ RSpec.shared_examples 'common trace features' do stub_feature_flags(gitlab_ci_archived_trace_consistent_reads: trace.job.project) end - it 'calls ::ApplicationRecord.sticking.unstick_or_continue_sticking' do - expect(::ApplicationRecord.sticking).to receive(:unstick_or_continue_sticking) + it 'calls ::Ci::Build.sticking.unstick_or_continue_sticking' do + expect(::Ci::Build.sticking).to receive(:unstick_or_continue_sticking) .with(described_class::LOAD_BALANCING_STICKING_NAMESPACE, trace.job.id) .and_call_original @@ -49,8 +49,8 @@ RSpec.shared_examples 'common trace features' do stub_feature_flags(gitlab_ci_archived_trace_consistent_reads: false) end - it 'does not call ::ApplicationRecord.sticking.unstick_or_continue_sticking' do - expect(::ApplicationRecord.sticking).not_to receive(:unstick_or_continue_sticking) + it 'does not call ::Ci::Build.sticking.unstick_or_continue_sticking' do + expect(::Ci::Build.sticking).not_to receive(:unstick_or_continue_sticking) trace.read { |stream| stream } end @@ -305,8 +305,8 @@ RSpec.shared_examples 'common trace features' do stub_feature_flags(gitlab_ci_archived_trace_consistent_reads: trace.job.project) end - it 'calls ::ApplicationRecord.sticking.stick' do - expect(::ApplicationRecord.sticking).to receive(:stick) + it 'calls ::Ci::Build.sticking.stick' do + expect(::Ci::Build.sticking).to receive(:stick) .with(described_class::LOAD_BALANCING_STICKING_NAMESPACE, trace.job.id) .and_call_original @@ -319,8 +319,8 @@ RSpec.shared_examples 'common trace features' do stub_feature_flags(gitlab_ci_archived_trace_consistent_reads: false) end - it 'does not call ::ApplicationRecord.sticking.stick' do - expect(::ApplicationRecord.sticking).not_to receive(:stick) + it 'does not call ::Ci::Build.sticking.stick' do + expect(::Ci::Build.sticking).not_to receive(:stick) subject end @@ -808,7 +808,19 @@ RSpec.shared_examples 'trace with enabled live trace feature' do create(:ci_job_artifact, :trace, job: build) end - it { is_expected.to be_truthy } + it 'is truthy' do + is_expected.to be_truthy + end + end + + context 'when archived trace record exists but file is not stored' do + before do + create(:ci_job_artifact, :unarchived_trace_artifact, job: build) + end + + it 'is falsy' do + is_expected.to be_falsy + end end context 'when live trace exists' do @@ -872,13 +884,35 @@ RSpec.shared_examples 'trace with enabled live trace feature' do build.reload expect(build.trace.exist?).to be_truthy - expect(build.job_artifacts_trace).to be_nil Gitlab::Ci::Trace::ChunkedIO.new(build) do |stream| expect(stream.read).to eq(trace_raw) end end end + shared_examples 'a pre-commit error' do |error:| + it_behaves_like 'source trace in ChunkedIO stays intact', error: error + + it 'does not save the trace artifact' do + expect { subject }.to raise_error(error) + + build.reload + expect(build.job_artifacts_trace).to be_nil + end + end + + shared_examples 'a post-commit error' do |error:| + it_behaves_like 'source trace in ChunkedIO stays intact', error: error + + it 'saves the trace artifact but not the file' do + expect { subject }.to raise_error(error) + + build.reload + expect(build.job_artifacts_trace).to be_present + expect(build.job_artifacts_trace.file.exists?).to be_falsy + end + end + context 'when job does not have trace artifact' do context 'when trace is stored in ChunkedIO' do let!(:build) { create(:ci_build, :success, :trace_live) } @@ -892,7 +926,7 @@ RSpec.shared_examples 'trace with enabled live trace feature' do allow(IO).to receive(:copy_stream).and_return(0) end - it_behaves_like 'source trace in ChunkedIO stays intact', error: Gitlab::Ci::Trace::ArchiveError + it_behaves_like 'a pre-commit error', error: Gitlab::Ci::Trace::ArchiveError end context 'when failed to create job artifact record' do @@ -902,7 +936,16 @@ RSpec.shared_examples 'trace with enabled live trace feature' do .and_return(%w[Error Error]) end - it_behaves_like 'source trace in ChunkedIO stays intact', error: ActiveRecord::RecordInvalid + it_behaves_like 'a pre-commit error', error: ActiveRecord::RecordInvalid + end + + context 'when storing the file raises an error' do + before do + stub_artifacts_object_storage(direct_upload: true) + allow_any_instance_of(Ci::JobArtifact).to receive(:store_file!).and_raise(Excon::Error::BadGateway, 'S3 is down lol') + end + + it_behaves_like 'a post-commit error', error: Excon::Error::BadGateway end end end diff --git a/spec/support/shared_examples/lib/gitlab/cycle_analytics/deployment_metrics.rb b/spec/support/shared_examples/lib/gitlab/cycle_analytics/deployment_metrics.rb index 6342064beb8..bea7cca2744 100644 --- a/spec/support/shared_examples/lib/gitlab/cycle_analytics/deployment_metrics.rb +++ b/spec/support/shared_examples/lib/gitlab/cycle_analytics/deployment_metrics.rb @@ -6,7 +6,7 @@ shared_examples 'deployment metrics examples' do environment = project.environments.production.first || create(:environment, :production, project: project) create(:deployment, :success, args.merge(environment: environment)) - # this is needed for the dora_deployment_frequency_in_vsa feature flag so we have aggregated data + # this is needed for the DORA API so we have aggregated data ::Dora::DailyMetrics::RefreshWorker.new.perform(environment.id, Time.current.to_date.to_s) if Gitlab.ee? 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 a617342ff8c..df795723874 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.main).to receive(:version).and_return('11.1') + allow(ApplicationRecord.database).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.main).to receive(:version).and_return('12.1') + allow(ApplicationRecord.database).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.main).to receive(:version).and_return('15.1') + allow(ApplicationRecord.database).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/import_export/attributes_permitter_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/import_export/attributes_permitter_shared_examples.rb index 5ce698c4701..41d3d76b66b 100644 --- a/spec/support/shared_examples/lib/gitlab/import_export/attributes_permitter_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/import_export/attributes_permitter_shared_examples.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true -RSpec.shared_examples 'a permitted attribute' do |relation_sym, permitted_attributes| +RSpec.shared_examples 'a permitted attribute' do |relation_sym, permitted_attributes, additional_attributes = []| let(:prohibited_attributes) { %i[remote_url my_attributes my_ids token my_id test] } let(:import_export_config) { Gitlab::ImportExport::Config.new.to_h } @@ -26,7 +26,7 @@ RSpec.shared_examples 'a permitted attribute' do |relation_sym, permitted_attrib end it 'does not contain attributes that would be cleaned with AttributeCleaner' do - expect(cleaned_hash.keys).to include(*permitted_hash.keys) + expect(cleaned_hash.keys + additional_attributes.to_a).to include(*permitted_hash.keys) end it 'does not contain prohibited attributes that are not related to given relation' do diff --git a/spec/support/shared_examples/lib/gitlab/sidekiq_middleware/strategy_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/sidekiq_middleware/strategy_shared_examples.rb index 708bc71ae96..ff03051ed37 100644 --- a/spec/support/shared_examples/lib/gitlab/sidekiq_middleware/strategy_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/sidekiq_middleware/strategy_shared_examples.rb @@ -2,7 +2,7 @@ RSpec.shared_examples 'deduplicating jobs when scheduling' do |strategy_name| let(:fake_duplicate_job) do - instance_double(Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob) + instance_double(Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob, duplicate_key_ttl: Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob::DEFAULT_DUPLICATE_KEY_TTL) end let(:expected_message) { "dropped #{strategy_name.to_s.humanize.downcase}" } @@ -11,14 +11,14 @@ RSpec.shared_examples 'deduplicating jobs when scheduling' do |strategy_name| describe '#schedule' do before do - allow(Gitlab::SidekiqLogging::DeduplicationLogger.instance).to receive(:log) + allow(Gitlab::SidekiqLogging::DeduplicationLogger.instance).to receive(:deduplicated_log) end it 'checks for duplicates before yielding' do expect(fake_duplicate_job).to receive(:scheduled?).twice.ordered.and_return(false) expect(fake_duplicate_job).to( receive(:check!) - .with(Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob::DUPLICATE_KEY_TTL) + .with(fake_duplicate_job.duplicate_key_ttl) .ordered .and_return('a jid')) expect(fake_duplicate_job).to receive(:duplicate?).ordered.and_return(false) @@ -40,6 +40,7 @@ RSpec.shared_examples 'deduplicating jobs when scheduling' do |strategy_name| allow(fake_duplicate_job).to receive(:check!).and_return('the jid') allow(fake_duplicate_job).to receive(:idempotent?).and_return(true) allow(fake_duplicate_job).to receive(:update_latest_wal_location!) + allow(fake_duplicate_job).to receive(:set_deduplicated_flag!) allow(fake_duplicate_job).to receive(:options).and_return({}) job_hash = {} @@ -61,10 +62,11 @@ RSpec.shared_examples 'deduplicating jobs when scheduling' do |strategy_name| allow(fake_duplicate_job).to receive(:options).and_return({ including_scheduled: true }) allow(fake_duplicate_job).to( receive(:check!) - .with(Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob::DUPLICATE_KEY_TTL) + .with(fake_duplicate_job.duplicate_key_ttl) .and_return('the jid')) allow(fake_duplicate_job).to receive(:idempotent?).and_return(true) allow(fake_duplicate_job).to receive(:update_latest_wal_location!) + allow(fake_duplicate_job).to receive(:set_deduplicated_flag!) job_hash = {} expect(fake_duplicate_job).to receive(:duplicate?).and_return(true) @@ -83,9 +85,10 @@ RSpec.shared_examples 'deduplicating jobs when scheduling' do |strategy_name| allow(fake_duplicate_job).to receive(:scheduled_at).and_return(Time.now + time_diff) allow(fake_duplicate_job).to receive(:options).and_return({ including_scheduled: true }) allow(fake_duplicate_job).to( - receive(:check!).with(time_diff.to_i).and_return('the jid')) + receive(:check!).with(time_diff.to_i + fake_duplicate_job.duplicate_key_ttl).and_return('the jid')) allow(fake_duplicate_job).to receive(:idempotent?).and_return(true) allow(fake_duplicate_job).to receive(:update_latest_wal_location!) + allow(fake_duplicate_job).to receive(:set_deduplicated_flag!) job_hash = {} expect(fake_duplicate_job).to receive(:duplicate?).and_return(true) @@ -100,6 +103,26 @@ RSpec.shared_examples 'deduplicating jobs when scheduling' do |strategy_name| end end + context "when the job is not duplicate" do + before do + allow(fake_duplicate_job).to receive(:scheduled?).and_return(false) + allow(fake_duplicate_job).to receive(:check!).and_return('the jid') + allow(fake_duplicate_job).to receive(:duplicate?).and_return(false) + allow(fake_duplicate_job).to receive(:options).and_return({}) + allow(fake_duplicate_job).to receive(:existing_jid).and_return('the jid') + end + + it 'does not return false nor drop the job' do + schedule_result = nil + + expect(fake_duplicate_job).not_to receive(:set_deduplicated_flag!) + + expect { |b| schedule_result = strategy.schedule({}, &b) }.to yield_control + + expect(schedule_result).to be_nil + end + end + context "when the job is droppable" do before do allow(fake_duplicate_job).to receive(:scheduled?).and_return(false) @@ -109,6 +132,7 @@ RSpec.shared_examples 'deduplicating jobs when scheduling' do |strategy_name| allow(fake_duplicate_job).to receive(:existing_jid).and_return('the jid') allow(fake_duplicate_job).to receive(:idempotent?).and_return(true) allow(fake_duplicate_job).to receive(:update_latest_wal_location!) + allow(fake_duplicate_job).to receive(:set_deduplicated_flag!) end it 'updates latest wal location' do @@ -117,10 +141,11 @@ RSpec.shared_examples 'deduplicating jobs when scheduling' do |strategy_name| strategy.schedule({ 'jid' => 'new jid' }) {} end - it 'drops the job' do + it 'returns false to drop the job' do schedule_result = nil expect(fake_duplicate_job).to receive(:idempotent?).and_return(true) + expect(fake_duplicate_job).to receive(:set_deduplicated_flag!).once expect { |b| schedule_result = strategy.schedule({}, &b) }.not_to yield_control expect(schedule_result).to be(false) @@ -130,7 +155,7 @@ RSpec.shared_examples 'deduplicating jobs when scheduling' do |strategy_name| fake_logger = instance_double(Gitlab::SidekiqLogging::DeduplicationLogger) expect(Gitlab::SidekiqLogging::DeduplicationLogger).to receive(:instance).and_return(fake_logger) - expect(fake_logger).to receive(:log).with(a_hash_including({ 'jid' => 'new jid' }), expected_message, {}) + expect(fake_logger).to receive(:deduplicated_log).with(a_hash_including({ 'jid' => 'new jid' }), expected_message, {}) strategy.schedule({ 'jid' => 'new jid' }) {} end @@ -140,7 +165,7 @@ RSpec.shared_examples 'deduplicating jobs when scheduling' do |strategy_name| expect(Gitlab::SidekiqLogging::DeduplicationLogger).to receive(:instance).and_return(fake_logger) allow(fake_duplicate_job).to receive(:options).and_return({ foo: :bar }) - expect(fake_logger).to receive(:log).with(a_hash_including({ 'jid' => 'new jid' }), expected_message, { foo: :bar }) + expect(fake_logger).to receive(:deduplicated_log).with(a_hash_including({ 'jid' => 'new jid' }), expected_message, { foo: :bar }) strategy.schedule({ 'jid' => 'new jid' }) {} end @@ -159,6 +184,9 @@ RSpec.shared_examples 'deduplicating jobs when scheduling' do |strategy_name| before do allow(fake_duplicate_job).to receive(:delete!) + allow(fake_duplicate_job).to receive(:scheduled?) { false } + allow(fake_duplicate_job).to receive(:options) { {} } + allow(fake_duplicate_job).to receive(:should_reschedule?) { false } allow(fake_duplicate_job).to receive(:latest_wal_locations).and_return( wal_locations ) end diff --git a/spec/support/shared_examples/lib/sidebars/projects/menus/zentao_menu_shared_examples.rb b/spec/support/shared_examples/lib/sidebars/projects/menus/zentao_menu_shared_examples.rb new file mode 100644 index 00000000000..d3fd28727b5 --- /dev/null +++ b/spec/support/shared_examples/lib/sidebars/projects/menus/zentao_menu_shared_examples.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.shared_examples 'ZenTao menu with CE version' do + let(:project) { create(:project, has_external_issue_tracker: true) } + let(:user) { project.owner } + let(:context) { Sidebars::Projects::Context.new(current_user: user, container: project) } + let(:zentao_integration) { create(:zentao_integration, project: project) } + + subject { described_class.new(context) } + + describe '#render?' do + context 'when issues integration is disabled' do + before do + zentao_integration.update!(active: false) + end + + it 'returns false' do + expect(subject.render?).to eq false + end + end + + context 'when issues integration is enabled' do + before do + zentao_integration.update!(active: true) + end + + it 'returns true' do + expect(subject.render?).to eq true + end + + it 'renders menu link' do + expect(subject.link).to eq zentao_integration.url + end + + it 'contains only open ZenTao item' do + expect(subject.renderable_items.map(&:item_id)).to match_array [:open_zentao] + end + end + end +end diff --git a/spec/support/shared_examples/loose_foreign_keys/have_loose_foreign_key.rb b/spec/support/shared_examples/loose_foreign_keys/have_loose_foreign_key.rb new file mode 100644 index 00000000000..7ccd9533811 --- /dev/null +++ b/spec/support/shared_examples/loose_foreign_keys/have_loose_foreign_key.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'it has loose foreign keys' do + let(:factory_name) { nil } + let(:table_name) { described_class.table_name } + let(:connection) { described_class.connection } + + it 'includes the LooseForeignKey module' do + expect(described_class.ancestors).to include(LooseForeignKey) + end + + it 'responds to #loose_foreign_key_definitions' do + expect(described_class).to respond_to(:loose_foreign_key_definitions) + end + + it 'has at least one loose foreign key definition' do + expect(described_class.loose_foreign_key_definitions.size).to be > 0 + end + + it 'has the deletion trigger present' do + sql = <<-SQL + SELECT trigger_name + FROM information_schema.triggers + WHERE event_object_table = '#{table_name}' + SQL + + triggers = connection.execute(sql) + + expected_trigger_name = "#{table_name}_loose_fk_trigger" + expect(triggers.pluck('trigger_name')).to include(expected_trigger_name) + end + + it 'records record deletions' do + model = create(factory_name) # rubocop: disable Rails/SaveBang + model.destroy! + + deleted_record = LooseForeignKeys::DeletedRecord.find_by(fully_qualified_table_name: "#{connection.current_schema}.#{table_name}", primary_key_value: model.id) + + expect(deleted_record).not_to be_nil + end + + it 'cleans up record deletions' do + model = create(factory_name) # rubocop: disable Rails/SaveBang + + expect { model.destroy! }.to change { LooseForeignKeys::DeletedRecord.count }.by(1) + + LooseForeignKeys::ProcessDeletedRecordsService.new(connection: connection).execute + + expect(LooseForeignKeys::DeletedRecord.status_pending.count).to be(0) + expect(LooseForeignKeys::DeletedRecord.status_processed.count).to be(1) + end +end diff --git a/spec/support/shared_examples/metrics/transaction_metrics_with_labels_shared_examples.rb b/spec/support/shared_examples/metrics/transaction_metrics_with_labels_shared_examples.rb new file mode 100644 index 00000000000..286c60f1f4f --- /dev/null +++ b/spec/support/shared_examples/metrics/transaction_metrics_with_labels_shared_examples.rb @@ -0,0 +1,219 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'transaction metrics with labels' do + let(:sensitive_tags) do + { + path: 'private', + branch: 'sensitive' + } + end + + around do |example| + described_class.reload_metric! + example.run + described_class.reload_metric! + end + + describe '.prometheus_metric' do + let(:prometheus_metric) { instance_double(Prometheus::Client::Histogram, observe: nil, base_labels: {}) } + + it 'adds a metric' do + expect(::Gitlab::Metrics).to receive(:histogram).with( + :meow_observe, 'Meow observe histogram', hash_including(*described_class::BASE_LABEL_KEYS), be_a(Array) + ).and_return(prometheus_metric) + + expect do |block| + metric = described_class.prometheus_metric(:meow_observe, :histogram, &block) + expect(metric).to be(prometheus_metric) + end.to yield_control + end + end + + describe '#method_call_for' do + it 'returns a MethodCall' do + method = transaction_obj.method_call_for('Foo#bar', :Foo, '#bar') + + expect(method).to be_an_instance_of(Gitlab::Metrics::MethodCall) + end + end + + describe '#add_event' do + let(:prometheus_metric) { instance_double(Prometheus::Client::Counter, increment: nil, base_labels: {}) } + + it 'adds a metric' do + expect(prometheus_metric).to receive(:increment).with(labels) + expect(described_class).to receive(:fetch_metric).with(:counter, :gitlab_transaction_event_meow_total).and_return(prometheus_metric) + + transaction_obj.add_event(:meow) + end + + it 'allows tracking of custom tags' do + expect(prometheus_metric).to receive(:increment).with(labels.merge(animal: "dog")) + expect(described_class).to receive(:fetch_metric).with(:counter, :gitlab_transaction_event_bau_total).and_return(prometheus_metric) + + transaction_obj.add_event(:bau, animal: 'dog') + end + + context 'with sensitive tags' do + it 'filters tags' do + expect(described_class).to receive(:fetch_metric).with(:counter, :gitlab_transaction_event_bau_total).and_return(prometheus_metric) + expect(prometheus_metric).not_to receive(:increment).with(hash_including(sensitive_tags)) + + transaction_obj.add_event(:bau, **sensitive_tags.merge(sane: 'yes')) + end + end + end + + describe '#increment' do + let(:prometheus_metric) { instance_double(Prometheus::Client::Counter, increment: nil, base_labels: {}) } + + it 'adds a metric' do + expect(::Gitlab::Metrics).to receive(:counter).with( + :meow, 'Meow counter', hash_including(*described_class::BASE_LABEL_KEYS) + ).and_return(prometheus_metric) + expect(prometheus_metric).to receive(:increment).with(labels, 1) + + transaction_obj.increment(:meow, 1) + end + + context 'with block' do + it 'overrides docstring' do + expect(::Gitlab::Metrics).to receive(:counter).with( + :block_docstring, 'test', hash_including(*described_class::BASE_LABEL_KEYS) + ).and_return(prometheus_metric) + expect(prometheus_metric).to receive(:increment).with(labels, 1) + + transaction_obj.increment(:block_docstring, 1) do + docstring 'test' + end + end + + it 'overrides labels' do + expect(::Gitlab::Metrics).to receive(:counter).with( + :block_labels, 'Block labels counter', hash_including(*described_class::BASE_LABEL_KEYS) + ).and_return(prometheus_metric) + expect(prometheus_metric).to receive(:increment).with(labels.merge(sane: 'yes'), 1) + + transaction_obj.increment(:block_labels, 1, sane: 'yes') do + label_keys %i(sane) + end + end + + it 'filters sensitive tags' do + labels_keys = sensitive_tags.keys + + expect(::Gitlab::Metrics).to receive(:counter).with( + :metric_with_sensitive_block, 'Metric with sensitive block counter', hash_excluding(labels_keys) + ).and_return(prometheus_metric) + expect(prometheus_metric).to receive(:increment).with(labels, 1) + + transaction_obj.increment(:metric_with_sensitive_block, 1, sensitive_tags) do + label_keys labels_keys + end + end + end + end + + describe '#set' do + let(:prometheus_metric) { instance_double(Prometheus::Client::Gauge, set: nil, base_labels: {}) } + + it 'adds a metric' do + expect(::Gitlab::Metrics).to receive(:gauge).with( + :meow_set, 'Meow set gauge', hash_including(*described_class::BASE_LABEL_KEYS), :all + ).and_return(prometheus_metric) + expect(prometheus_metric).to receive(:set).with(labels, 99) + + transaction_obj.set(:meow_set, 99) + end + + context 'with block' do + it 'overrides docstring' do + expect(::Gitlab::Metrics).to receive(:gauge).with( + :block_docstring_set, 'test', hash_including(*described_class::BASE_LABEL_KEYS), :all + ).and_return(prometheus_metric) + expect(prometheus_metric).to receive(:set).with(labels, 99) + + transaction_obj.set(:block_docstring_set, 99) do + docstring 'test' + end + end + + it 'overrides labels' do + expect(::Gitlab::Metrics).to receive(:gauge).with( + :block_labels_set, 'Block labels set gauge', hash_including(*described_class::BASE_LABEL_KEYS), :all + ).and_return(prometheus_metric) + expect(prometheus_metric).to receive(:set).with(labels.merge(sane: 'yes'), 99) + + transaction_obj.set(:block_labels_set, 99, sane: 'yes') do + label_keys %i(sane) + end + end + + it 'filters sensitive tags' do + labels_keys = sensitive_tags.keys + + expect(::Gitlab::Metrics).to receive(:gauge).with( + :metric_set_with_sensitive_block, 'Metric set with sensitive block gauge', hash_excluding(*labels_keys), :all + ).and_return(prometheus_metric) + expect(prometheus_metric).to receive(:set).with(labels, 99) + + transaction_obj.set(:metric_set_with_sensitive_block, 99, sensitive_tags) do + label_keys label_keys + end + end + end + end + + describe '#observe' do + let(:prometheus_metric) { instance_double(Prometheus::Client::Histogram, observe: nil, base_labels: {}) } + + it 'adds a metric' do + expect(::Gitlab::Metrics).to receive(:histogram).with( + :meow_observe, 'Meow observe histogram', hash_including(*described_class::BASE_LABEL_KEYS), kind_of(Array) + ).and_return(prometheus_metric) + expect(prometheus_metric).to receive(:observe).with(labels, 2.0) + + transaction_obj.observe(:meow_observe, 2.0) + end + + context 'with block' do + it 'overrides docstring' do + expect(::Gitlab::Metrics).to receive(:histogram).with( + :block_docstring_observe, 'test', hash_including(*described_class::BASE_LABEL_KEYS), kind_of(Array) + ).and_return(prometheus_metric) + expect(prometheus_metric).to receive(:observe).with(labels, 2.0) + + transaction_obj.observe(:block_docstring_observe, 2.0) do + docstring 'test' + end + end + + it 'overrides labels' do + expect(::Gitlab::Metrics).to receive(:histogram).with( + :block_labels_observe, 'Block labels observe histogram', hash_including(*described_class::BASE_LABEL_KEYS), kind_of(Array) + ).and_return(prometheus_metric) + expect(prometheus_metric).to receive(:observe).with(labels.merge(sane: 'yes'), 2.0) + + transaction_obj.observe(:block_labels_observe, 2.0, sane: 'yes') do + label_keys %i(sane) + end + end + + it 'filters sensitive tags' do + labels_keys = sensitive_tags.keys + + expect(::Gitlab::Metrics).to receive(:histogram).with( + :metric_observe_with_sensitive_block, + 'Metric observe with sensitive block histogram', + hash_excluding(labels_keys), + kind_of(Array) + ).and_return(prometheus_metric) + expect(prometheus_metric).to receive(:observe).with(labels, 2.0) + + transaction_obj.observe(:metric_observe_with_sensitive_block, 2.0, sensitive_tags) do + label_keys label_keys + end + end + end + end +end diff --git a/spec/support/shared_examples/models/concerns/analytics/cycle_analytics/stage_event_model_examples.rb b/spec/support/shared_examples/models/concerns/analytics/cycle_analytics/stage_event_model_examples.rb index f928fb1eb43..d823e7ac221 100644 --- a/spec/support/shared_examples/models/concerns/analytics/cycle_analytics/stage_event_model_examples.rb +++ b/spec/support/shared_examples/models/concerns/analytics/cycle_analytics/stage_event_model_examples.rb @@ -12,6 +12,7 @@ RSpec.shared_examples 'StageEventModel' do project_id: 4, author_id: 5, milestone_id: 6, + state_id: 1, start_event_timestamp: time, end_event_timestamp: time }, @@ -22,6 +23,7 @@ RSpec.shared_examples 'StageEventModel' do project_id: 11, author_id: 12, milestone_id: 13, + state_id: 1, start_event_timestamp: time, end_event_timestamp: time } @@ -34,8 +36,9 @@ RSpec.shared_examples 'StageEventModel' do described_class.issuable_id_column, :group_id, :project_id, - :milestone_id, :author_id, + :milestone_id, + :state_id, :start_event_timestamp, :end_event_timestamp ] @@ -59,10 +62,120 @@ RSpec.shared_examples 'StageEventModel' do upsert_data output_data = described_class.all.map do |record| - column_order.map { |column| record[column] } + column_order.map do |column| + if column == :state_id + described_class.states[record[column]] + else + record[column] + end + end end.sort expect(input_data.map(&:values).sort).to eq(output_data) end end + + describe 'scopes' do + def attributes(array) + array.map(&:attributes) + end + + RSpec::Matchers.define :match_attributes do |expected| + match do |actual| + actual.map(&:attributes) == expected.map(&:attributes) + end + end + + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:user) } + let_it_be(:milestone) { create(:milestone) } + let_it_be(:issuable_with_assignee) { create(issuable_factory, assignees: [user])} + + let_it_be(:record) { create(stage_event_factory, start_event_timestamp: 3.years.ago.to_date, end_event_timestamp: 2.years.ago.to_date) } + let_it_be(:record_with_author) { create(stage_event_factory, author_id: user.id) } + let_it_be(:record_with_project) { create(stage_event_factory, project_id: project.id) } + let_it_be(:record_with_group) { create(stage_event_factory, group_id: project.namespace_id) } + let_it_be(:record_with_assigned_issuable) { create(stage_event_factory, described_class.issuable_id_column => issuable_with_assignee.id) } + let_it_be(:record_with_milestone) { create(stage_event_factory, milestone_id: milestone.id) } + + it 'filters by stage_event_hash_id' do + records = described_class.by_stage_event_hash_id(record.stage_event_hash_id) + + expect(records).to match_attributes([record]) + end + + it 'filters by project_id' do + records = described_class.by_project_id(project.id) + + expect(records).to match_attributes([record_with_project]) + end + + it 'filters by group_id' do + records = described_class.by_group_id(project.namespace_id) + + expect(records).to match_attributes([record_with_group]) + end + + it 'filters by author_id' do + records = described_class.authored(user) + + expect(records).to match_attributes([record_with_author]) + end + + it 'filters by assignee' do + records = described_class.assigned_to(user) + + expect(records).to match_attributes([record_with_assigned_issuable]) + end + + it 'filters by milestone_id' do + records = described_class.with_milestone_id(milestone.id) + + expect(records).to match_attributes([record_with_milestone]) + end + + describe 'start_event_timestamp filtering' do + it 'when range is given' do + records = described_class + .start_event_timestamp_after(4.years.ago) + .start_event_timestamp_before(2.years.ago) + + expect(records).to match_attributes([record]) + end + + it 'when specifying upper bound' do + records = described_class.start_event_timestamp_before(2.years.ago) + + expect(attributes(records)).to include(attributes([record]).first) + end + + it 'when specifying the lower bound' do + records = described_class.start_event_timestamp_after(4.years.ago) + + expect(attributes(records)).to include(attributes([record]).first) + end + end + + describe 'end_event_timestamp filtering' do + it 'when range is given' do + records = described_class + .end_event_timestamp_after(3.years.ago) + .end_event_timestamp_before(1.year.ago) + + expect(records).to match_attributes([record]) + end + + it 'when specifying upper bound' do + records = described_class.end_event_timestamp_before(1.year.ago) + + expect(attributes(records)).to include(attributes([record]).first) + end + + it 'when specifying the lower bound' do + records = described_class.end_event_timestamp_after(3.years.ago) + + expect(attributes(records)).to include(attributes([record]).first) + end + end + end end diff --git a/spec/support/shared_examples/models/concerns/ttl_expirable_shared_examples.rb b/spec/support/shared_examples/models/concerns/ttl_expirable_shared_examples.rb index a4e0d6c871e..2d08de297a3 100644 --- a/spec/support/shared_examples/models/concerns/ttl_expirable_shared_examples.rb +++ b/spec/support/shared_examples/models/concerns/ttl_expirable_shared_examples.rb @@ -11,18 +11,18 @@ RSpec.shared_examples 'ttl_expirable' do it { is_expected.to validate_presence_of(:status) } end - describe '.updated_before' do + describe '.read_before' do # rubocop:disable Rails/SaveBang let_it_be_with_reload(:item1) { create(class_symbol) } let_it_be(:item2) { create(class_symbol) } # rubocop:enable Rails/SaveBang before do - item1.update_column(:updated_at, 1.month.ago) + item1.update_column(:read_at, 1.month.ago) end it 'returns items with created at older than the supplied number of days' do - expect(described_class.updated_before(10)).to contain_exactly(item1) + expect(described_class.read_before(10)).to contain_exactly(item1) end end @@ -48,4 +48,13 @@ RSpec.shared_examples 'ttl_expirable' do expect(described_class.lock_next_by(:created_at)).to contain_exactly(item3) end end + + describe '#read', :freeze_time do + let_it_be(:old_read_at) { 1.day.ago } + let_it_be(:item1) { create(class_symbol, read_at: old_read_at) } + + it 'updates read_at' do + expect { item1.read! }.to change { item1.reload.read_at } + end + end end diff --git a/spec/support/shared_examples/models/member_shared_examples.rb b/spec/support/shared_examples/models/member_shared_examples.rb index 56c202cb228..a2909c66e22 100644 --- a/spec/support/shared_examples/models/member_shared_examples.rb +++ b/spec/support/shared_examples/models/member_shared_examples.rb @@ -299,6 +299,22 @@ RSpec.shared_examples_for "member creation" do end end end + + context 'when `tasks_to_be_done` and `tasks_project_id` are passed' do + before do + stub_experiments(invite_members_for_task: true) + end + + it 'creates a member_task with the correct attributes', :aggregate_failures do + task_project = source.is_a?(Group) ? create(:project, group: source) : source + described_class.new(source, user, :developer, tasks_to_be_done: %w(ci code), tasks_project_id: task_project.id).execute + + member = source.members.last + + expect(member.tasks_to_be_done).to match_array([:ci, :code]) + expect(member.member_task.project).to eq(task_project) + end + end end end @@ -379,5 +395,20 @@ RSpec.shared_examples_for "bulk member creation" do expect(members).to all(be_persisted) end end + + context 'when `tasks_to_be_done` and `tasks_project_id` are passed' do + before do + stub_experiments(invite_members_for_task: true) + end + + it 'creates a member_task with the correct attributes', :aggregate_failures do + task_project = source.is_a?(Group) ? create(:project, group: source) : source + members = described_class.add_users(source, [user1], :developer, tasks_to_be_done: %w(ci code), tasks_project_id: task_project.id) + member = members.last + + expect(member.tasks_to_be_done).to match_array([:ci, :code]) + expect(member.member_task.project).to eq(task_project) + end + end end end diff --git a/spec/support/shared_examples/models/reviewer_state_shared_examples.rb b/spec/support/shared_examples/models/reviewer_state_shared_examples.rb new file mode 100644 index 00000000000..f1392768b06 --- /dev/null +++ b/spec/support/shared_examples/models/reviewer_state_shared_examples.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'having reviewer state' do + describe 'mr_attention_requests feature flag is disabled' do + before do + stub_feature_flags(mr_attention_requests: false) + end + + it { is_expected.to have_attributes(state: 'unreviewed') } + end + + describe 'mr_attention_requests feature flag is enabled' do + it { is_expected.to have_attributes(state: 'attention_requested') } + end +end diff --git a/spec/support/shared_examples/namespaces/traversal_examples.rb b/spec/support/shared_examples/namespaces/traversal_examples.rb index d126b242fb0..ac6a843663f 100644 --- a/spec/support/shared_examples/namespaces/traversal_examples.rb +++ b/spec/support/shared_examples/namespaces/traversal_examples.rb @@ -22,6 +22,8 @@ RSpec.shared_examples 'namespace traversal' do let_it_be(:deep_nested_group) { create(:group, parent: nested_group) } let_it_be(:very_deep_nested_group) { create(:group, parent: deep_nested_group) } let_it_be(:groups) { [group, nested_group, deep_nested_group, very_deep_nested_group] } + let_it_be(:project) { create(:project, group: nested_group) } + let_it_be(:project_namespace) { project.project_namespace } describe '#root_ancestor' do it 'returns the correct root ancestor' do @@ -65,6 +67,7 @@ RSpec.shared_examples 'namespace traversal' do expect(deep_nested_group.ancestors).to contain_exactly(group, nested_group) expect(nested_group.ancestors).to contain_exactly(group) expect(group.ancestors).to eq([]) + expect(project_namespace.ancestors).to be_empty end context 'with asc hierarchy_order' do @@ -73,6 +76,7 @@ RSpec.shared_examples 'namespace traversal' do 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([]) + expect(project_namespace.ancestors(hierarchy_order: :asc)).to be_empty end end @@ -82,6 +86,7 @@ RSpec.shared_examples 'namespace traversal' do 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([]) + expect(project_namespace.ancestors(hierarchy_order: :desc)).to be_empty end end @@ -98,6 +103,7 @@ RSpec.shared_examples 'namespace traversal' do expect(deep_nested_group.ancestor_ids).to contain_exactly(group.id, nested_group.id) expect(nested_group.ancestor_ids).to contain_exactly(group.id) expect(group.ancestor_ids).to be_empty + expect(project_namespace.ancestor_ids).to be_empty end context 'with asc hierarchy_order' do @@ -106,6 +112,7 @@ RSpec.shared_examples 'namespace traversal' do 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([]) + expect(project_namespace.ancestor_ids(hierarchy_order: :asc)).to eq([]) end end @@ -115,6 +122,7 @@ RSpec.shared_examples 'namespace traversal' do 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([]) + expect(project_namespace.ancestor_ids(hierarchy_order: :desc)).to eq([]) end end @@ -131,6 +139,7 @@ RSpec.shared_examples 'namespace traversal' do expect(deep_nested_group.self_and_ancestors).to contain_exactly(group, nested_group, deep_nested_group) expect(nested_group.self_and_ancestors).to contain_exactly(group, nested_group) expect(group.self_and_ancestors).to contain_exactly(group) + expect(project_namespace.self_and_ancestors).to contain_exactly(project_namespace) end context 'with asc hierarchy_order' do @@ -139,6 +148,7 @@ RSpec.shared_examples 'namespace traversal' do 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]) + expect(project_namespace.self_and_ancestors(hierarchy_order: :asc)).to eq([project_namespace]) end end @@ -148,6 +158,7 @@ RSpec.shared_examples 'namespace traversal' do 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]) + expect(project_namespace.self_and_ancestors(hierarchy_order: :desc)).to eq([project_namespace]) end end @@ -164,6 +175,7 @@ RSpec.shared_examples 'namespace traversal' do expect(deep_nested_group.self_and_ancestor_ids).to contain_exactly(group.id, nested_group.id, deep_nested_group.id) expect(nested_group.self_and_ancestor_ids).to contain_exactly(group.id, nested_group.id) expect(group.self_and_ancestor_ids).to contain_exactly(group.id) + expect(project_namespace.self_and_ancestor_ids).to contain_exactly(project_namespace.id) end context 'with asc hierarchy_order' do @@ -172,6 +184,7 @@ RSpec.shared_examples 'namespace traversal' do 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]) + expect(project_namespace.self_and_ancestor_ids(hierarchy_order: :asc)).to eq([project_namespace.id]) end end @@ -181,6 +194,7 @@ RSpec.shared_examples 'namespace traversal' do 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]) + expect(project_namespace.self_and_ancestor_ids(hierarchy_order: :desc)).to eq([project_namespace.id]) end end @@ -205,6 +219,10 @@ RSpec.shared_examples 'namespace traversal' do describe '#recursive_descendants' do it_behaves_like 'recursive version', :descendants end + + it 'does not include project namespaces' do + expect(group.descendants.to_a).not_to include(project_namespace) + end end describe '#self_and_descendants' do @@ -223,6 +241,10 @@ RSpec.shared_examples 'namespace traversal' do it_behaves_like 'recursive version', :self_and_descendants end + + it 'does not include project namespaces' do + expect(group.self_and_descendants.to_a).not_to include(project_namespace) + end end describe '#self_and_descendant_ids' do diff --git a/spec/support/shared_examples/namespaces/traversal_scope_examples.rb b/spec/support/shared_examples/namespaces/traversal_scope_examples.rb index 74b1bacc560..4c09c1c2a3b 100644 --- a/spec/support/shared_examples/namespaces/traversal_scope_examples.rb +++ b/spec/support/shared_examples/namespaces/traversal_scope_examples.rb @@ -25,12 +25,6 @@ RSpec.shared_examples 'namespace traversal scopes' do 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 '.order_by_depth' do subject { described_class.where(id: [group_1, nested_group_1, deep_nested_group_1]).order_by_depth(direction) } @@ -55,6 +49,53 @@ RSpec.shared_examples 'namespace traversal scopes' do it { is_expected.to eq described_class.column_names } end + shared_examples '.roots' do + context 'with only sub-groups' do + subject { described_class.where(id: [deep_nested_group_1, nested_group_1, deep_nested_group_2]).roots } + + it { is_expected.to contain_exactly(group_1, group_2) } + end + + context 'with only root groups' do + subject { described_class.where(id: [group_1, group_2]).roots } + + it { is_expected.to contain_exactly(group_1, group_2) } + end + + context 'with all groups' do + subject { described_class.where(id: groups).roots } + + it { is_expected.to contain_exactly(group_1, group_2) } + end + end + + describe '.roots' do + context "use_traversal_ids_roots feature flag is true" do + before do + stub_feature_flags(use_traversal_ids: true) + stub_feature_flags(use_traversal_ids_roots: true) + end + + it_behaves_like '.roots' + + it 'not make recursive queries' do + expect { described_class.where(id: [nested_group_1]).roots.load }.not_to make_queries_matching(/WITH RECURSIVE/) + end + end + + context "use_traversal_ids_roots feature flag is false" do + before do + stub_feature_flags(use_traversal_ids_roots: false) + end + + it_behaves_like '.roots' + + it 'make recursive queries' do + expect { described_class.where(id: [nested_group_1]).roots.load }.to make_queries_matching(/WITH RECURSIVE/) + end + end + end + shared_examples '.self_and_ancestors' do subject { described_class.where(id: [nested_group_1, nested_group_2]).self_and_ancestors } @@ -156,7 +197,7 @@ RSpec.shared_examples 'namespace traversal scopes' do end end - describe '.self_and_descendants' do + shared_examples '.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) } @@ -174,7 +215,19 @@ RSpec.shared_examples 'namespace traversal scopes' do end end - describe '.self_and_descendant_ids' do + describe '.self_and_descendants' do + include_examples '.self_and_descendants' + + context 'with traversal_ids_btree feature flag disabled' do + before do + stub_feature_flags(traversal_ids_btree: false) + end + + include_examples '.self_and_descendants' + end + end + + shared_examples '.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) } @@ -190,4 +243,16 @@ RSpec.shared_examples 'namespace traversal scopes' do it { is_expected.to contain_exactly(deep_nested_group_1.id, deep_nested_group_2.id) } end end + + describe '.self_and_descendant_ids' do + include_examples '.self_and_descendant_ids' + + context 'with traversal_ids_btree feature flag disabled' do + before do + stub_feature_flags(traversal_ids_btree: false) + end + + include_examples '.self_and_descendant_ids' + end + end end diff --git a/spec/support/shared_examples/quick_actions/issue/promote_to_incident_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issue/promote_to_incident_quick_action_shared_examples.rb new file mode 100644 index 00000000000..5167d27f8b9 --- /dev/null +++ b/spec/support/shared_examples/quick_actions/issue/promote_to_incident_quick_action_shared_examples.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'promote_to_incident quick action' do + describe '/promote_to_incident' do + context 'when issue can be promoted' do + it 'promotes issue to incident' do + add_note('/promote_to_incident') + + expect(issue.reload.issue_type).to eq('incident') + expect(page).to have_content('Issue has been promoted to incident') + end + end + + context 'when issue is already an incident' do + let(:issue) { create(:incident, project: project) } + + it 'does not promote the issue' do + add_note('/promote_to_incident') + + expect(page).to have_content('Could not apply promote_to_incident command') + end + end + + context 'when user does not have permissions' do + let(:guest) { create(:user) } + + before do + sign_in(guest) + visit project_issue_path(project, issue) + wait_for_all_requests + end + + it 'does not promote the issue' do + add_note('/promote_to_incident') + + expect(page).to have_content('Could not apply promote_to_incident command') + end + end + end +end diff --git a/spec/support/shared_examples/requests/api/debian_common_shared_examples.rb b/spec/support/shared_examples/requests/api/debian_common_shared_examples.rb new file mode 100644 index 00000000000..e0225070986 --- /dev/null +++ b/spec/support/shared_examples/requests/api/debian_common_shared_examples.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'rejects Debian access with unknown container id' do |anonymous_status, auth_method| + context 'with an unknown container' do + let(:container) { double(id: non_existing_record_id) } + + context 'as anonymous' do + it_behaves_like 'Debian packages GET request', anonymous_status, nil + end + + context 'as authenticated user' do + include_context 'Debian repository auth headers', :not_a_member, auth_method do + it_behaves_like 'Debian packages GET request', :not_found, nil + end + end + end +end diff --git a/spec/support/shared_examples/requests/api/debian_distributions_shared_examples.rb b/spec/support/shared_examples/requests/api/debian_distributions_shared_examples.rb new file mode 100644 index 00000000000..5cd63c33936 --- /dev/null +++ b/spec/support/shared_examples/requests/api/debian_distributions_shared_examples.rb @@ -0,0 +1,192 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'Debian distributions GET request' do |status, body = nil| + and_body = body.nil? ? '' : ' and expected body' + + it "returns #{status}#{and_body}" do + subject + + expect(response).to have_gitlab_http_status(status) + + unless body.nil? + expect(response.body).to match(body) + end + end +end + +RSpec.shared_examples 'Debian distributions PUT request' do |status, body| + and_body = body.nil? ? '' : ' and expected body' + + if status == :success + it 'updates distribution', :aggregate_failures do + expect(::Packages::Debian::UpdateDistributionService).to receive(:new).with(distribution, api_params.except(:codename)).and_call_original + + expect { subject } + .to not_change { Packages::Debian::GroupDistribution.all.count + Packages::Debian::ProjectDistribution.all.count } + .and not_change { Packages::Debian::GroupComponent.all.count + Packages::Debian::ProjectComponent.all.count } + .and not_change { Packages::Debian::GroupArchitecture.all.count + Packages::Debian::ProjectArchitecture.all.count } + + expect(response).to have_gitlab_http_status(status) + expect(response.media_type).to eq('application/json') + + unless body.nil? + expect(response.body).to match(body) + end + end + else + it "returns #{status}#{and_body}", :aggregate_failures do + subject + + expect(response).to have_gitlab_http_status(status) + + unless body.nil? + expect(response.body).to match(body) + end + end + end +end + +RSpec.shared_examples 'Debian distributions DELETE request' do |status, body| + and_body = body.nil? ? '' : ' and expected body' + + if status == :success + it 'updates distribution', :aggregate_failures do + expect { subject } + .to change { Packages::Debian::GroupDistribution.all.count + Packages::Debian::ProjectDistribution.all.count }.by(-1) + .and change { Packages::Debian::GroupComponent.all.count + Packages::Debian::ProjectComponent.all.count }.by(-1) + .and change { Packages::Debian::GroupArchitecture.all.count + Packages::Debian::ProjectArchitecture.all.count }.by(-2) + + expect(response).to have_gitlab_http_status(status) + expect(response.media_type).to eq('application/json') + + unless body.nil? + expect(response.body).to match(body) + end + end + else + it "returns #{status}#{and_body}", :aggregate_failures do + subject + + expect(response).to have_gitlab_http_status(status) + + unless body.nil? + expect(response.body).to match(body) + end + end + end +end + +RSpec.shared_examples 'Debian distributions POST request' do |status, body| + and_body = body.nil? ? '' : ' and expected body' + + if status == :created + it 'creates distribution', :aggregate_failures do + expect(::Packages::Debian::CreateDistributionService).to receive(:new).with(container, user, api_params).and_call_original + + expect { subject } + .to change { Packages::Debian::GroupDistribution.all.count + Packages::Debian::ProjectDistribution.all.count }.by(1) + .and change { Packages::Debian::GroupComponent.all.count + Packages::Debian::ProjectComponent.all.count }.by(1) + .and change { Packages::Debian::GroupArchitecture.all.count + Packages::Debian::ProjectArchitecture.all.count }.by(2) + + expect(response).to have_gitlab_http_status(status) + expect(response.media_type).to eq('application/json') + + unless body.nil? + expect(response.body).to match(body) + end + end + else + it "returns #{status}#{and_body}", :aggregate_failures do + subject + + expect(response).to have_gitlab_http_status(status) + + unless body.nil? + expect(response.body).to match(body) + end + end + end +end + +RSpec.shared_examples 'Debian distributions read endpoint' do |desired_behavior, success_status, success_body| + context 'with valid container' do + using RSpec::Parameterized::TableSyntax + + where(:visibility_level, :user_type, :auth_method, :expected_status, :expected_body) do + :public | :guest | :private_token | success_status | success_body + :public | :not_a_member | :private_token | success_status | success_body + :public | :anonymous | :private_token | success_status | success_body + :public | :invalid_token | :private_token | :unauthorized | nil + :private | :developer | :private_token | success_status | success_body + :private | :developer | :basic | :not_found | nil + :private | :guest | :private_token | :forbidden | nil + :private | :not_a_member | :private_token | :not_found | nil + :private | :anonymous | :private_token | :not_found | nil + :private | :invalid_token | :private_token | :unauthorized | nil + end + + with_them do + include_context 'Debian repository access', params[:visibility_level], params[:user_type], params[:auth_method] do + it_behaves_like "Debian distributions #{desired_behavior} request", params[:expected_status], params[:expected_body] + end + end + end + + it_behaves_like 'rejects Debian access with unknown container id', :not_found, :private_token +end + +RSpec.shared_examples 'Debian distributions write endpoint' do |desired_behavior, success_status, success_body| + context 'with valid container' do + using RSpec::Parameterized::TableSyntax + + where(:visibility_level, :user_type, :auth_method, :expected_status, :expected_body) do + :public | :developer | :private_token | success_status | success_body + :public | :developer | :basic | :unauthorized | nil + :public | :guest | :private_token | :forbidden | nil + :public | :not_a_member | :private_token | :forbidden | nil + :public | :anonymous | :private_token | :unauthorized | nil + :public | :invalid_token | :private_token | :unauthorized | nil + :private | :developer | :private_token | success_status | success_body + :private | :guest | :private_token | :forbidden | nil + :private | :not_a_member | :private_token | :not_found | nil + :private | :anonymous | :private_token | :not_found | nil + :private | :invalid_token | :private_token | :unauthorized | nil + end + + with_them do + include_context 'Debian repository access', params[:visibility_level], params[:user_type], params[:auth_method] do + it_behaves_like "Debian distributions #{desired_behavior} request", params[:expected_status], params[:expected_body] + end + end + end + + it_behaves_like 'rejects Debian access with unknown container id', :not_found, :private_token +end + +RSpec.shared_examples 'Debian distributions maintainer write endpoint' do |desired_behavior, success_status, success_body| + context 'with valid container' do + using RSpec::Parameterized::TableSyntax + + where(:visibility_level, :user_type, :auth_method, :expected_status, :expected_body) do + :public | :maintainer | :private_token | success_status | success_body + :public | :maintainer | :basic | :unauthorized | nil + :public | :developer | :private_token | :forbidden | nil + :public | :not_a_member | :private_token | :forbidden | nil + :public | :anonymous | :private_token | :unauthorized | nil + :public | :invalid_token | :private_token | :unauthorized | nil + :private | :maintainer | :private_token | success_status | success_body + :private | :developer | :private_token | :forbidden | nil + :private | :not_a_member | :private_token | :not_found | nil + :private | :anonymous | :private_token | :not_found | nil + :private | :invalid_token | :private_token | :unauthorized | nil + end + + with_them do + include_context 'Debian repository access', params[:visibility_level], params[:user_type], params[:auth_method] do + it_behaves_like "Debian distributions #{desired_behavior} request", params[:expected_status], params[:expected_body] + end + end + end + + it_behaves_like 'rejects Debian access with unknown container id', :not_found, :private_token +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 a3ed74085fb..2fd5e6a5f91 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 @@ -1,127 +1,6 @@ # frozen_string_literal: true -RSpec.shared_context 'Debian repository shared context' do |container_type, can_freeze| - include_context 'workhorse headers' - - before do - stub_feature_flags(debian_packages: true, debian_group_packages: true) - end - - let_it_be(:private_container, freeze: can_freeze) { create(container_type, :private) } - let_it_be(:public_container, freeze: can_freeze) { create(container_type, :public) } - let_it_be(:user, freeze: true) { create(:user) } - let_it_be(:personal_access_token, freeze: true) { create(:personal_access_token, user: user) } - - let_it_be(:private_distribution, freeze: true) { create("debian_#{container_type}_distribution", :with_file, container: private_container, codename: 'existing-codename') } - let_it_be(:private_component, freeze: true) { create("debian_#{container_type}_component", distribution: private_distribution, name: 'existing-component') } - let_it_be(:private_architecture_all, freeze: true) { create("debian_#{container_type}_architecture", distribution: private_distribution, name: 'all') } - let_it_be(:private_architecture, freeze: true) { create("debian_#{container_type}_architecture", distribution: private_distribution, name: 'existing-arch') } - let_it_be(:private_component_file) { create("debian_#{container_type}_component_file", component: private_component, architecture: private_architecture) } - - let_it_be(:public_distribution, freeze: true) { create("debian_#{container_type}_distribution", :with_file, container: public_container, codename: 'existing-codename') } - let_it_be(:public_component, freeze: true) { create("debian_#{container_type}_component", distribution: public_distribution, name: 'existing-component') } - let_it_be(:public_architecture_all, freeze: true) { create("debian_#{container_type}_architecture", distribution: public_distribution, name: 'all') } - let_it_be(:public_architecture, freeze: true) { create("debian_#{container_type}_architecture", distribution: public_distribution, name: 'existing-arch') } - let_it_be(:public_component_file) { create("debian_#{container_type}_component_file", component: public_component, architecture: public_architecture) } - - if container_type == :group - let_it_be(:private_project) { create(:project, :private, group: private_container) } - 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 } - let_it_be(:private_project_distribution) { private_distribution } - let_it_be(:public_project_distribution) { public_distribution } - end - - let_it_be(:private_package) { create(:debian_package, project: private_project, published_in: private_project_distribution) } - let_it_be(:public_package) { create(:debian_package, project: public_project, published_in: public_project_distribution) } - - let(:visibility_level) { :public } - - let(:distribution) { { private: private_distribution, public: public_distribution }[visibility_level] } - 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(: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 } - - let(:workhorse_params) do - if method == :put - file_upload = fixture_file_upload("spec/fixtures/packages/debian/#{file_name}") - { file: file_upload } - else - {} - end - end - - let(:api_params) { workhorse_params } - - let(:auth_headers) { {} } - let(:wh_headers) do - if method == :put - workhorse_headers - else - {} - end - end - - let(:headers) { auth_headers.merge(wh_headers) } - - let(:send_rewritten_field) { true } - - subject do - if method == :put - workhorse_finalize( - api(url), - method: method, - file_key: :file, - params: api_params, - headers: headers, - send_rewritten_field: send_rewritten_field - ) - else - send method, api(url), headers: headers, params: api_params - end - 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' } - - let(:auth_headers) do - if user_role == :anonymous - {} - elsif auth_method == :token - { 'Private-Token' => token } - else - basic_auth_header(user.username, token) - end - end -end - -RSpec.shared_context 'Debian repository access' do |visibility_level, user_role, add_member, user_token, auth_method| - include_context 'Debian repository auth headers', user_role, user_token, auth_method do - let(:containers) { { private: private_container, public: public_container } } - let(:container) { containers[visibility_level] } - - before do - container.send("add_#{user_role}", user) if add_member && user_role != :anonymous - end - end -end - -RSpec.shared_examples 'Debian repository GET request' do |status, body = nil| +RSpec.shared_examples 'Debian packages GET request' do |status, body = nil| and_body = body.nil? ? '' : ' and expected body' it "returns #{status}#{and_body}" do @@ -135,7 +14,7 @@ RSpec.shared_examples 'Debian repository GET request' do |status, body = nil| end end -RSpec.shared_examples 'Debian repository upload request' do |status, body = nil| +RSpec.shared_examples 'Debian packages upload request' do |status, body = nil| and_body = body.nil? ? '' : ' and expected body' if status == :created @@ -175,7 +54,7 @@ RSpec.shared_examples 'Debian repository upload request' do |status, body = nil| end end -RSpec.shared_examples 'Debian repository upload authorize request' do |status, body = nil| +RSpec.shared_examples 'Debian packages upload authorize request' do |status, body = nil| and_body = body.nil? ? '' : ' and expected body' if status == :created @@ -221,237 +100,57 @@ RSpec.shared_examples 'Debian repository upload authorize request' do |status, b end end -RSpec.shared_examples 'Debian repository POST distribution request' do |status, body| - and_body = body.nil? ? '' : ' and expected body' - - if status == :created - it 'creates distribution', :aggregate_failures do - expect(::Packages::Debian::CreateDistributionService).to receive(:new).with(container, user, api_params).and_call_original - - expect { subject } - .to change { Packages::Debian::GroupDistribution.all.count + Packages::Debian::ProjectDistribution.all.count }.by(1) - .and change { Packages::Debian::GroupComponent.all.count + Packages::Debian::ProjectComponent.all.count }.by(1) - .and change { Packages::Debian::GroupArchitecture.all.count + Packages::Debian::ProjectArchitecture.all.count }.by(2) - - expect(response).to have_gitlab_http_status(status) - expect(response.media_type).to eq('application/json') - - unless body.nil? - expect(response.body).to match(body) - end - end - else - it "returns #{status}#{and_body}", :aggregate_failures do - subject - - expect(response).to have_gitlab_http_status(status) - - unless body.nil? - expect(response.body).to match(body) - end - end - end -end - -RSpec.shared_examples 'Debian repository PUT distribution request' do |status, body| - and_body = body.nil? ? '' : ' and expected body' - - if status == :success - it 'updates distribution', :aggregate_failures do - expect(::Packages::Debian::UpdateDistributionService).to receive(:new).with(distribution, api_params.except(:codename)).and_call_original - - expect { subject } - .to not_change { Packages::Debian::GroupDistribution.all.count + Packages::Debian::ProjectDistribution.all.count } - .and not_change { Packages::Debian::GroupComponent.all.count + Packages::Debian::ProjectComponent.all.count } - .and not_change { Packages::Debian::GroupArchitecture.all.count + Packages::Debian::ProjectArchitecture.all.count } - - expect(response).to have_gitlab_http_status(status) - expect(response.media_type).to eq('application/json') - - unless body.nil? - expect(response.body).to match(body) - end - end - else - it "returns #{status}#{and_body}", :aggregate_failures do - subject - - expect(response).to have_gitlab_http_status(status) - - unless body.nil? - expect(response.body).to match(body) - end - end - end -end - -RSpec.shared_examples 'Debian repository DELETE distribution request' do |status, body| - and_body = body.nil? ? '' : ' and expected body' - - if status == :success - it 'updates distribution', :aggregate_failures do - expect { subject } - .to change { Packages::Debian::GroupDistribution.all.count + Packages::Debian::ProjectDistribution.all.count }.by(-1) - .and change { Packages::Debian::GroupComponent.all.count + Packages::Debian::ProjectComponent.all.count }.by(-1) - .and change { Packages::Debian::GroupArchitecture.all.count + Packages::Debian::ProjectArchitecture.all.count }.by(-2) - - expect(response).to have_gitlab_http_status(status) - expect(response.media_type).to eq('application/json') - - unless body.nil? - expect(response.body).to match(body) - end - end - else - it "returns #{status}#{and_body}", :aggregate_failures do - subject - - expect(response).to have_gitlab_http_status(status) - - unless body.nil? - expect(response.body).to match(body) - end - end - end -end - -RSpec.shared_examples 'rejects Debian access with unknown container id' do |hidden_status| - context 'with an unknown container' do - let(:container) { double(id: non_existing_record_id) } - - context 'as anonymous' do - it_behaves_like 'Debian repository GET request', hidden_status, nil - end - - context 'as authenticated user' do - subject { get api(url), headers: basic_auth_header(user.username, personal_access_token.token) } - - it_behaves_like 'Debian repository GET request', :not_found, nil - end - end -end - -RSpec.shared_examples 'Debian repository read endpoint' do |desired_behavior, success_status, success_body, authenticate_non_public: true| - hidden_status = if authenticate_non_public - :unauthorized - else - :not_found - end - - context 'with valid container' do - using RSpec::Parameterized::TableSyntax - - where(:visibility_level, :user_role, :member, :user_token, :expected_status, :expected_body) do - :public | :developer | true | true | success_status | success_body - :public | :guest | true | true | success_status | success_body - :public | :developer | true | false | :unauthorized | nil - :public | :guest | true | false | :unauthorized | nil - :public | :developer | false | true | success_status | success_body - :public | :guest | false | true | success_status | success_body - :public | :developer | false | false | :unauthorized | nil - :public | :guest | false | false | :unauthorized | nil - :public | :anonymous | false | true | success_status | success_body - :private | :developer | true | true | success_status | success_body - :private | :guest | true | true | :forbidden | nil - :private | :developer | true | false | :unauthorized | nil - :private | :guest | true | false | :unauthorized | nil - :private | :developer | false | true | :not_found | nil - :private | :guest | false | true | :not_found | nil - :private | :developer | false | false | :unauthorized | nil - :private | :guest | false | false | :unauthorized | nil - :private | :anonymous | false | true | hidden_status | nil - end - - with_them do - include_context 'Debian repository access', params[:visibility_level], params[:user_role], params[:member], params[:user_token], :basic do - it_behaves_like "Debian repository #{desired_behavior}", params[:expected_status], params[:expected_body] - end - end - end - - it_behaves_like 'rejects Debian access with unknown container id', hidden_status -end - -RSpec.shared_examples 'Debian repository write endpoint' do |desired_behavior, success_status, success_body, authenticate_non_public: true| - hidden_status = if authenticate_non_public - :unauthorized - else - :not_found - end - +RSpec.shared_examples 'Debian packages read endpoint' do |desired_behavior, success_status, success_body| context 'with valid container' do using RSpec::Parameterized::TableSyntax - where(:visibility_level, :user_role, :member, :user_token, :expected_status, :expected_body) do - :public | :developer | true | true | success_status | success_body - :public | :guest | true | true | :forbidden | nil - :public | :developer | true | false | :unauthorized | nil - :public | :guest | true | false | :unauthorized | nil - :public | :developer | false | true | :forbidden | nil - :public | :guest | false | true | :forbidden | nil - :public | :developer | false | false | :unauthorized | nil - :public | :guest | false | false | :unauthorized | nil - :public | :anonymous | false | true | :unauthorized | nil - :private | :developer | true | true | success_status | success_body - :private | :guest | true | true | :forbidden | nil - :private | :developer | true | false | :unauthorized | nil - :private | :guest | true | false | :unauthorized | nil - :private | :developer | false | true | :not_found | nil - :private | :guest | false | true | :not_found | nil - :private | :developer | false | false | :unauthorized | nil - :private | :guest | false | false | :unauthorized | nil - :private | :anonymous | false | true | hidden_status | nil + where(:visibility_level, :user_type, :auth_method, :expected_status, :expected_body) do + :public | :guest | :basic | success_status | success_body + :public | :not_a_member | :basic | success_status | success_body + :public | :anonymous | :basic | success_status | success_body + :public | :invalid_token | :basic | :unauthorized | nil + :private | :developer | :basic | success_status | success_body + :private | :developer | :private_token | :unauthorized | nil + :private | :guest | :basic | :forbidden | nil + :private | :not_a_member | :basic | :not_found | nil + :private | :anonymous | :basic | :unauthorized | nil + :private | :invalid_token | :basic | :unauthorized | nil end with_them do - include_context 'Debian repository access', params[:visibility_level], params[:user_role], params[:member], params[:user_token], :basic do - it_behaves_like "Debian repository #{desired_behavior}", params[:expected_status], params[:expected_body] + include_context 'Debian repository access', params[:visibility_level], params[:user_type], params[:auth_method] do + it_behaves_like "Debian packages #{desired_behavior} request", params[:expected_status], params[:expected_body] end end end - it_behaves_like 'rejects Debian access with unknown container id', hidden_status + it_behaves_like 'rejects Debian access with unknown container id', :unauthorized, :basic end -RSpec.shared_examples 'Debian repository maintainer write endpoint' do |desired_behavior, success_status, success_body, authenticate_non_public: true| - hidden_status = if authenticate_non_public - :unauthorized - else - :not_found - end - +RSpec.shared_examples 'Debian packages write endpoint' do |desired_behavior, success_status, success_body| context 'with valid container' do using RSpec::Parameterized::TableSyntax - where(:visibility_level, :user_role, :member, :user_token, :expected_status, :expected_body) do - :public | :maintainer | true | true | success_status | success_body - :public | :developer | true | true | :forbidden | nil - :public | :guest | true | true | :forbidden | nil - :public | :maintainer | true | false | :unauthorized | nil - :public | :guest | true | false | :unauthorized | nil - :public | :maintainer | false | true | :forbidden | nil - :public | :guest | false | true | :forbidden | nil - :public | :maintainer | false | false | :unauthorized | nil - :public | :guest | false | false | :unauthorized | nil - :public | :anonymous | false | true | :unauthorized | nil - :private | :maintainer | true | true | success_status | success_body - :private | :developer | true | true | :forbidden | nil - :private | :guest | true | true | :forbidden | nil - :private | :maintainer | true | false | :unauthorized | nil - :private | :guest | true | false | :unauthorized | nil - :private | :maintainer | false | true | :not_found | nil - :private | :guest | false | true | :not_found | nil - :private | :maintainer | false | false | :unauthorized | nil - :private | :guest | false | false | :unauthorized | nil - :private | :anonymous | false | true | hidden_status | nil + where(:visibility_level, :user_type, :auth_method, :expected_status, :expected_body) do + :public | :developer | :basic | success_status | success_body + :public | :developer | :private_token | :unauthorized | nil + :public | :guest | :basic | :forbidden | nil + :public | :not_a_member | :basic | :forbidden | nil + :public | :anonymous | :basic | :unauthorized | nil + :public | :invalid_token | :basic | :unauthorized | nil + :private | :developer | :basic | success_status | success_body + :private | :guest | :basic | :forbidden | nil + :private | :not_a_member | :basic | :not_found | nil + :private | :anonymous | :basic | :unauthorized | nil + :private | :invalid_token | :basic | :unauthorized | nil end with_them do - include_context 'Debian repository access', params[:visibility_level], params[:user_role], params[:member], params[:user_token], :basic do - it_behaves_like "Debian repository #{desired_behavior}", params[:expected_status], params[:expected_body] + include_context 'Debian repository access', params[:visibility_level], params[:user_type], params[:auth_method] do + it_behaves_like "Debian packages #{desired_behavior} request", params[:expected_status], params[:expected_body] end end end - it_behaves_like 'rejects Debian access with unknown container id', hidden_status + it_behaves_like 'rejects Debian access with unknown container id', :unauthorized, :basic end diff --git a/spec/support/shared_examples/requests/api/graphql/mutations/destroy_list_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/mutations/destroy_list_shared_examples.rb index 0cec67ff541..dca152223fb 100644 --- a/spec/support/shared_examples/requests/api/graphql/mutations/destroy_list_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/graphql/mutations/destroy_list_shared_examples.rb @@ -28,7 +28,7 @@ RSpec.shared_examples 'board lists destroy request' do it 'returns an error' do subject - expect(graphql_errors.first['message']).to include("The resource that you are attempting to access does not exist or you don't have permission to perform this action") + expect(graphql_errors.first['message']).to include(Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR) end end diff --git a/spec/support/shared_examples/requests/api/graphql/packages/package_details_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/packages/package_details_shared_examples.rb index 41a61ba5fd7..d576a5874fd 100644 --- a/spec/support/shared_examples/requests/api/graphql/packages/package_details_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/graphql/packages/package_details_shared_examples.rb @@ -2,12 +2,26 @@ RSpec.shared_examples 'a package detail' do it_behaves_like 'a working graphql query' do - it 'matches the JSON schema' do - expect(package_details).to match_schema('graphql/packages/package_details') + it_behaves_like 'matching the package details schema' + end + + context 'with pipelines' do + let_it_be(:build_info1) { create(:package_build_info, :with_pipeline, package: package) } + let_it_be(:build_info2) { create(:package_build_info, :with_pipeline, package: package) } + let_it_be(:build_info3) { create(:package_build_info, :with_pipeline, package: package) } + + it_behaves_like 'a working graphql query' do + it_behaves_like 'matching the package details schema' end end end +RSpec.shared_examples 'matching the package details schema' do + it 'matches the JSON schema' do + expect(package_details).to match_schema('graphql/packages/package_details') + end +end + RSpec.shared_examples 'a package with files' do it 'has the right amount of files' do expect(package_files_response.length).to be(package.package_files.length) diff --git a/spec/support/shared_examples/requests/api/notes_shared_examples.rb b/spec/support/shared_examples/requests/api/notes_shared_examples.rb index 40799688144..0434d0beb7e 100644 --- a/spec/support/shared_examples/requests/api/notes_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/notes_shared_examples.rb @@ -281,7 +281,7 @@ RSpec.shared_examples 'noteable API' do |parent_type, noteable_type, id_name| end end - context 'when request exceeds the rate limit' do + context 'when request exceeds the rate limit', :freeze_time, :clean_gitlab_redis_rate_limiting do before do stub_application_setting(notes_create_limit: 1) allow(::Gitlab::ApplicationRateLimiter).to receive(:increment).and_return(2) 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 2af7b616659..19677e92001 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 @@ -8,6 +8,8 @@ RSpec.shared_examples 'handling get metadata requests' do |scope: :project| let_it_be(:package_dependency_link3) { create(:packages_dependency_link, package: package, dependency_type: :bundleDependencies) } let_it_be(:package_dependency_link4) { create(:packages_dependency_link, package: package, dependency_type: :peerDependencies) } + let_it_be(:package_metadatum) { create(:npm_metadatum, package: package) } + let(:headers) { {} } subject { get(url, headers: headers) } @@ -39,6 +41,19 @@ RSpec.shared_examples 'handling get metadata requests' do |scope: :project| # query count can slightly change between the examples so we're using a custom threshold expect { get(url, headers: headers) }.not_to exceed_query_limit(control).with_threshold(4) end + + context 'with packages_npm_abbreviated_metadata disabled' do + before do + stub_feature_flags(packages_npm_abbreviated_metadata: false) + end + + it 'calls the presenter without including metadata' do + expect(::Packages::Npm::PackagePresenter) + .to receive(:new).with(anything, anything, include_metadata: false).and_call_original + + subject + end + end end shared_examples 'reject metadata request' do |status:| diff --git a/spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb index ed6d9ed43c8..06c51add438 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 @@ -167,7 +167,7 @@ end RSpec.shared_examples 'rejects PyPI access with unknown project id' do context 'with an unknown project' do - let(:project) { OpenStruct.new(id: 1234567890) } + let(:project) { double('access', id: 1234567890) } it_behaves_like 'unknown PyPI scope id' end @@ -175,7 +175,7 @@ end RSpec.shared_examples 'rejects PyPI access with unknown group id' do context 'with an unknown project' do - let(:group) { OpenStruct.new(id: 1234567890) } + let(:group) { double('access', id: 1234567890) } it_behaves_like 'unknown PyPI scope id' end diff --git a/spec/support/shared_examples/requests/api/status_shared_examples.rb b/spec/support/shared_examples/requests/api/status_shared_examples.rb index 8207190b1dc..40843ccbd15 100644 --- a/spec/support/shared_examples/requests/api/status_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/status_shared_examples.rb @@ -76,3 +76,32 @@ RSpec.shared_examples '412 response' do end end end + +RSpec.shared_examples '422 response' do + let(:message) { nil } + + before do + # Fires the request + request + end + + it 'returns 422' do + expect(response).to have_gitlab_http_status(:unprocessable_entity) + expect(json_response).to be_an Object + + if message.present? + expect(json_response['message']).to eq(message) + end + end +end + +RSpec.shared_examples '503 response' do + before do + # Fires the request + request + end + + it 'returns 503' do + expect(response).to have_gitlab_http_status(:service_unavailable) + end +end diff --git a/spec/support/shared_examples/requests/applications_controller_shared_examples.rb b/spec/support/shared_examples/requests/applications_controller_shared_examples.rb new file mode 100644 index 00000000000..8f852d42c2c --- /dev/null +++ b/spec/support/shared_examples/requests/applications_controller_shared_examples.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'applications controller - GET #show' do + describe 'GET #show' do + it 'renders template' do + get show_path + + expect(response).to render_template :show + end + + context 'when application is viewed after being created' do + before do + create_application + end + + it 'sets `@created` instance variable to `true`' do + get show_path + + expect(assigns[:created]).to eq(true) + end + end + + context 'when application is reviewed' do + it 'sets `@created` instance variable to `false`' do + get show_path + + expect(assigns[:created]).to eq(false) + end + end + end +end + +RSpec.shared_examples 'applications controller - POST #create' do + it "sets `#{OauthApplications::CREATED_SESSION_KEY}` session key to `true`" do + create_application + + expect(session[OauthApplications::CREATED_SESSION_KEY]).to eq(true) + end +end + +def create_application + create_params = attributes_for(:application, trusted: true, confidential: false, scopes: ['api']) + post create_path, params: { doorkeeper_application: create_params } +end diff --git a/spec/support/shared_examples/requests/self_monitoring_shared_examples.rb b/spec/support/shared_examples/requests/self_monitoring_shared_examples.rb index ff87fc5d8df..f8a752a5673 100644 --- a/spec/support/shared_examples/requests/self_monitoring_shared_examples.rb +++ b/spec/support/shared_examples/requests/self_monitoring_shared_examples.rb @@ -39,6 +39,10 @@ end # let(:status_api) { status_create_self_monitoring_project_admin_application_settings_path } # subject { post create_self_monitoring_project_admin_application_settings_path } RSpec.shared_examples 'triggers async worker, returns sidekiq job_id with response accepted' do + before do + allow(worker_class).to receive(:with_status).and_return(worker_class) + end + it 'returns sidekiq job_id of expected length' do subject diff --git a/spec/support/shared_examples/requests/snippet_shared_examples.rb b/spec/support/shared_examples/requests/snippet_shared_examples.rb index dae3a3e74be..b13c4da0bed 100644 --- a/spec/support/shared_examples/requests/snippet_shared_examples.rb +++ b/spec/support/shared_examples/requests/snippet_shared_examples.rb @@ -86,6 +86,7 @@ RSpec.shared_examples 'snippet blob content' do expect(response.header[Gitlab::Workhorse::DETECT_HEADER]).to eq 'true' expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with('git-blob:') + expect(response.parsed_body).to be_empty end context 'when snippet repository is empty' do diff --git a/spec/support/shared_examples/service_desk_issue_templates_examples.rb b/spec/support/shared_examples/service_desk_issue_templates_examples.rb index fd9645df7a3..ed6c5199936 100644 --- a/spec/support/shared_examples/service_desk_issue_templates_examples.rb +++ b/spec/support/shared_examples/service_desk_issue_templates_examples.rb @@ -3,10 +3,10 @@ RSpec.shared_examples 'issue description templates from current project only' do it 'loads issue description templates from the project only' do within('#service-desk-template-select') do - expect(page).to have_content('project-issue-bar') - expect(page).to have_content('project-issue-foo') - expect(page).not_to have_content('group-issue-bar') - expect(page).not_to have_content('group-issue-foo') + expect(page).to have_content(:all, 'project-issue-bar') + expect(page).to have_content(:all, 'project-issue-foo') + expect(page).not_to have_content(:all, 'group-issue-bar') + expect(page).not_to have_content(:all, 'group-issue-foo') end end end diff --git a/spec/support/shared_examples/services/alert_management/alert_processing/alert_firing_shared_examples.rb b/spec/support/shared_examples/services/alert_management/alert_processing/alert_firing_shared_examples.rb index 92a7d7ab3a3..ca86cb082a7 100644 --- a/spec/support/shared_examples/services/alert_management/alert_processing/alert_firing_shared_examples.rb +++ b/spec/support/shared_examples/services/alert_management/alert_processing/alert_firing_shared_examples.rb @@ -3,7 +3,10 @@ # This shared_example requires the following variables: # - `service`, the service which includes AlertManagement::AlertProcessing RSpec.shared_examples 'creates an alert management alert or errors' do - it { is_expected.to be_success } + specify do + expect(subject).to be_success + expect(subject.payload).to match(alerts: all(a_kind_of(AlertManagement::Alert))) + end it 'creates AlertManagement::Alert' do expect(Gitlab::AppLogger).not_to receive(:warn) @@ -89,6 +92,7 @@ RSpec.shared_examples 'adds an alert management alert event' do expect { subject }.to change { alert.reload.events }.by(1) expect(subject).to be_success + expect(subject.payload).to match(alerts: all(a_kind_of(AlertManagement::Alert))) end it_behaves_like 'does not create an alert management alert' 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 index 56a6d24d557..c4f6273b46c 100644 --- a/spec/support/shared_examples/services/jira/requests/base_shared_examples.rb +++ b/spec/support/shared_examples/services/jira/requests/base_shared_examples.rb @@ -26,11 +26,14 @@ RSpec.shared_examples 'a service that handles Jira API errors' do expect(subject).to be_a(ServiceResponse) expect(subject).to be_error - expect(subject.message).to include(expected_message) + expect(subject.message).to start_with(expected_message) end end context 'when the JSON in JIRA::HTTPError is unsafe' do + config_docs_link_url = Rails.application.routes.url_helpers.help_page_path('integration/jira/configure') + let(:docs_link_start) { '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: config_docs_link_url } } + before do stub_client_and_raise(JIRA::HTTPError, error) end @@ -39,7 +42,8 @@ RSpec.shared_examples 'a service that handles Jira API errors' 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.') + error_message = 'An error occurred while requesting data from Jira. Check your %{docs_link_start}Jira integration configuration</a> and try again.' % { docs_link_start: docs_link_start } + expect(subject.message).to eq(error_message) end end @@ -47,7 +51,8 @@ RSpec.shared_examples 'a service that handles Jira API errors' 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.') + error_message = 'An error occurred while requesting data from Jira: foo. Check your %{docs_link_start}Jira integration configuration</a> and try again.' % { docs_link_start: docs_link_start } + expect(subject.message).to eq(error_message) end end end diff --git a/spec/support/shared_examples/services/resource_events/synthetic_notes_builder_shared_examples.rb b/spec/support/shared_examples/services/resource_events/synthetic_notes_builder_shared_examples.rb new file mode 100644 index 00000000000..716bee39fca --- /dev/null +++ b/spec/support/shared_examples/services/resource_events/synthetic_notes_builder_shared_examples.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'filters by paginated notes' do |event_type| + let(:event) { create(event_type) } # rubocop:disable Rails/SaveBang + + before do + create(event_type, issue: event.issue) + end + + it 'only returns given notes' do + paginated_notes = { event_type.to_s.pluralize => [double(id: event.id)] } + notes = described_class.new(event.issue, user, paginated_notes: paginated_notes).execute + + expect(notes.size).to eq(1) + expect(notes.first.event).to eq(event) + end + + context 'when paginated notes is empty' do + it 'does not return any notes' do + notes = described_class.new(event.issue, user, paginated_notes: {}).execute + + expect(notes.size).to eq(0) + end + end +end diff --git a/spec/support/shared_examples/workers/self_monitoring_shared_examples.rb b/spec/support/shared_examples/workers/self_monitoring_shared_examples.rb index 89c0841fbd6..e6da96e12ec 100644 --- a/spec/support/shared_examples/workers/self_monitoring_shared_examples.rb +++ b/spec/support/shared_examples/workers/self_monitoring_shared_examples.rb @@ -17,7 +17,7 @@ end RSpec.shared_examples 'returns in_progress based on Sidekiq::Status' do it 'returns true when job is enqueued' do - jid = described_class.perform_async + jid = described_class.with_status.perform_async expect(described_class.in_progress?(jid)).to eq(true) end |