diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-09-20 14:18:08 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-09-20 14:18:08 +0300 |
commit | 5afcbe03ead9ada87621888a31a62652b10a7e4f (patch) | |
tree | 9918b67a0d0f0bafa6542e839a8be37adf73102d /spec/support/shared_examples | |
parent | c97c0201564848c1f53226fe19d71fdcc472f7d0 (diff) |
Add latest changes from gitlab-org/gitlab@16-4-stable-eev16.4.0-rc42
Diffstat (limited to 'spec/support/shared_examples')
67 files changed, 1562 insertions, 638 deletions
diff --git a/spec/support/shared_examples/channels/noteable/notes_channel_shared_examples.rb b/spec/support/shared_examples/channels/noteable/notes_channel_shared_examples.rb index cb7001a9faf..21bba14f3e6 100644 --- a/spec/support/shared_examples/channels/noteable/notes_channel_shared_examples.rb +++ b/spec/support/shared_examples/channels/noteable/notes_channel_shared_examples.rb @@ -15,16 +15,4 @@ RSpec.shared_examples 'handle subscription based on user access' do expect(subscription).to be_rejected end - - context 'when action_cable_notes is disabled' do - before do - stub_feature_flags(action_cable_notes: false) - end - - it 'rejects the subscription' do - subscribe(subscribe_params) - - expect(subscription).to be_rejected - end - end end diff --git a/spec/support/shared_examples/ci/create_pipeline_service_environment_shared_examples.rb b/spec/support/shared_examples/ci/create_pipeline_service_environment_shared_examples.rb new file mode 100644 index 00000000000..77a352a8326 --- /dev/null +++ b/spec/support/shared_examples/ci/create_pipeline_service_environment_shared_examples.rb @@ -0,0 +1,166 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'creating a pipeline with environment keyword' do + context 'with environment' do + let(:config) do + YAML.dump( + deploy: { + environment: { name: "review/$CI_COMMIT_REF_NAME" }, + **base_config + }) + end + + it 'creates the environment', :sidekiq_inline do + result = execute_service.payload + + expect(result).to be_persisted + expect(Environment.find_by(name: "review/master")).to be_present + expect(result.all_jobs.first.deployment).to be_persisted + expect(result.all_jobs.first.deployment.deployable).to be_a(expected_deployable_class) + end + + it 'sets tags when build job' do + skip unless expected_deployable_class == Ci::Build + + result = execute_service.payload + + expect(result).to be_persisted + expect(result.all_jobs.first.tag_list).to match_array(expected_tag_names) + end + end + + context 'with environment with auto_stop_in' do + let(:config) do + YAML.dump( + deploy: { + environment: { name: "review/$CI_COMMIT_REF_NAME", auto_stop_in: '1 day' }, + **base_config + }) + end + + it 'creates the environment with auto stop in' do + result = execute_service.payload + + expect(result).to be_persisted + expect(result.all_jobs.first.options[:environment][:auto_stop_in]).to eq('1 day') + end + end + + context 'with environment name including persisted variables' do + let(:config) do + YAML.dump( + deploy: { + environment: { name: "review/id1$CI_PIPELINE_ID/id2$CI_JOB_ID" }, + **base_config + } + ) + end + + it 'skips persisted variables in environment name' do + result = execute_service.payload + + expect(result).to be_persisted + expect(Environment.find_by(name: "review/id1/id2")).to be_present + end + end + + context 'when environment with Kubernetes configuration' do + let(:kubernetes_namespace) { 'custom-namespace' } + let(:config) do + YAML.dump( + deploy: { + environment: { + name: "environment-name", + kubernetes: { namespace: kubernetes_namespace } + }, + **base_config + } + ) + end + + it 'stores the requested namespace' do + result = execute_service.payload + job = result.all_jobs.first + + expect(result).to be_persisted + expect(job.options.dig(:environment, :kubernetes, :namespace)).to eq(kubernetes_namespace) + end + end + + context 'when environment with invalid name' do + let(:config) do + YAML.dump(deploy: { environment: { name: 'name,with,commas' }, **base_config }) + end + + it 'does not create an environment' do + expect do + result = execute_service.payload + + expect(result).to be_persisted + end.not_to change { Environment.count } + end + end + + context 'when environment with duplicate names' do + let(:config) do + YAML.dump({ + deploy: { environment: { name: 'production' }, **base_config }, + deploy_2: { environment: { name: 'production' }, **base_config } + }) + end + + it 'creates a pipeline with the environment', :sidekiq_inline do + result = execute_service.payload + + expect(result).to be_persisted + expect(Environment.find_by(name: 'production')).to be_present + expect(result.all_jobs.first.deployment).to be_persisted + expect(result.all_jobs.first.deployment.deployable).to be_a(expected_deployable_class) + end + end + + context 'when pipeline has a job with environment' do + let(:pipeline) { execute_service.payload } + + context 'when environment name is valid' do + let(:config) do + YAML.dump({ + review_app: { + environment: { + name: 'review/${CI_COMMIT_REF_NAME}', + url: 'http://${CI_COMMIT_REF_SLUG}-staging.example.com' + }, + **base_config + } + }) + end + + it 'has a job with environment', :sidekiq_inline do + expect(pipeline.all_jobs.count).to eq(1) + expect(pipeline.all_jobs.first.persisted_environment.name).to eq('review/master') + expect(pipeline.all_jobs.first.deployment.status).to eq(expected_deployment_status) + expect(pipeline.all_jobs.first.status).to eq(expected_job_status) + end + end + + context 'when environment name is invalid' do + let(:config) do + YAML.dump({ + 'job:deploy-to-test-site': { + environment: { + name: '${CI_JOB_NAME}', + url: 'https://$APP_URL' + }, + **base_config + } + }) + end + + it 'has a job without environment' do + expect(pipeline.all_jobs.count).to eq(1) + expect(pipeline.all_jobs.first.persisted_environment).to be_nil + expect(pipeline.all_jobs.first.deployment).to be_nil + end + end + end +end diff --git a/spec/support/shared_examples/ci/deployable_shared_examples.rb b/spec/support/shared_examples/ci/deployable_shared_examples.rb index b51a8fa20e2..4f43d38e604 100644 --- a/spec/support/shared_examples/ci/deployable_shared_examples.rb +++ b/spec/support/shared_examples/ci/deployable_shared_examples.rb @@ -96,7 +96,7 @@ RSpec.shared_examples 'a deployable job' do ActiveRecord::QueryRecorder.new { subject } end - index_for_build = recorded.log.index { |l| l.include?("UPDATE #{Ci::Build.quoted_table_name}") } + index_for_build = recorded.log.index { |l| l.include?("UPDATE #{described_class.quoted_table_name}") } index_for_deployment = recorded.log.index { |l| l.include?("UPDATE \"deployments\"") } expect(index_for_build).to be < index_for_deployment @@ -259,7 +259,7 @@ RSpec.shared_examples 'a deployable job' do describe '#environment_tier_from_options' do subject { job.environment_tier_from_options } - let(:job) { Ci::Build.new(options: options) } + let(:job) { described_class.new(options: options) } let(:options) { { environment: { deployment_tier: 'production' } } } it { is_expected.to eq('production') } @@ -276,7 +276,7 @@ RSpec.shared_examples 'a deployable job' do let(:options) { { environment: { deployment_tier: 'production' } } } let!(:environment) { create(:environment, name: 'production', tier: 'development', project: project) } - let(:job) { Ci::Build.new(options: options, environment: 'production', project: project) } + let(:job) { described_class.new(options: options, environment: 'production', project: project) } it { is_expected.to eq('production') } @@ -295,6 +295,52 @@ RSpec.shared_examples 'a deployable job' do end end + describe '#environment_url' do + subject { job.environment_url } + + let!(:job) { create(factory_type, :with_deployment, :deploy_to_production, pipeline: pipeline) } + + it { is_expected.to eq('http://prd.example.com/$CI_JOB_NAME') } + + context 'when options does not include url' do + before do + job.update!(options: { environment: { url: nil } }) + job.persisted_environment.update!(external_url: 'http://prd.example.com/$CI_JOB_NAME') + end + + it 'fetches from the persisted environment' do + expect_any_instance_of(::Environment) do |environment| + expect(environment).to receive(:external_url).once + end + + is_expected.to eq('http://prd.example.com/$CI_JOB_NAME') + end + + context 'when persisted environment is absent' do + before do + job.clear_memoization(:persisted_environment) + job.persisted_environment = nil + end + + it { is_expected.to be_nil } + end + end + end + + describe '#environment_slug' do + subject { job.environment_slug } + + let!(:job) { create(factory_type, :with_deployment, :start_review_app, pipeline: pipeline) } + + it { is_expected.to eq('review-master-8dyme2') } + + context 'when persisted environment is absent' do + let!(:job) { create(factory_type, :start_review_app, pipeline: pipeline) } + + it { is_expected.to be_nil } + end + end + describe 'environment' do describe '#has_environment_keyword?' do subject { job.has_environment_keyword? } @@ -536,10 +582,6 @@ RSpec.shared_examples 'a deployable job' do end describe '#deployment_status' do - before do - allow_any_instance_of(Ci::Build).to receive(:create_deployment) # rubocop:disable RSpec/AnyInstanceOf - end - context 'when job is a last deployment' do let(:job) { create(factory_type, :success, environment: 'production', pipeline: pipeline) } let(:environment) { create(:environment, name: 'production', project: job.project) } diff --git a/spec/support/shared_examples/config/metrics/every_metric_definition_shared_examples.rb b/spec/support/shared_examples/config/metrics/every_metric_definition_shared_examples.rb index e61c884cd2b..14d0ac81250 100644 --- a/spec/support/shared_examples/config/metrics/every_metric_definition_shared_examples.rb +++ b/spec/support/shared_examples/config/metrics/every_metric_definition_shared_examples.rb @@ -121,8 +121,7 @@ RSpec.shared_examples 'every metric definition' do let(:ignored_classes) do [ Gitlab::Usage::Metrics::Instrumentations::IssuesWithAlertManagementAlertsMetric, - Gitlab::Usage::Metrics::Instrumentations::IssuesWithPrometheusAlertEvents, - Gitlab::Usage::Metrics::Instrumentations::IssuesWithSelfManagedPrometheusAlertEvents + Gitlab::Usage::Metrics::Instrumentations::IssuesWithPrometheusAlertEvents ].freeze end diff --git a/spec/support/shared_examples/controllers/concerns/web_hooks/integrations_hook_log_actions_shared_examples.rb b/spec/support/shared_examples/controllers/concerns/web_hooks/integrations_hook_log_actions_shared_examples.rb index 56a5dcb10b2..cb4e68122d9 100644 --- a/spec/support/shared_examples/controllers/concerns/web_hooks/integrations_hook_log_actions_shared_examples.rb +++ b/spec/support/shared_examples/controllers/concerns/web_hooks/integrations_hook_log_actions_shared_examples.rb @@ -43,5 +43,14 @@ RSpec.shared_examples WebHooks::HookLogActions do expect(response).to have_gitlab_http_status(:not_found) end + + it 'redirects back with a warning if the hook log url is outdated' do + web_hook_log.update!(url_hash: 'some_other_value') + + post retry_path, headers: { 'REFERER' => show_path } + + expect(response).to redirect_to(show_path) + expect(flash[:warning]).to eq(_('The hook URL has changed, and this log entry cannot be retried')) + end end end diff --git a/spec/support/shared_examples/controllers/issuable_notes_filter_shared_examples.rb b/spec/support/shared_examples/controllers/issuable_notes_filter_shared_examples.rb index a4eb6a839c0..bd9c2582d2f 100644 --- a/spec/support/shared_examples/controllers/issuable_notes_filter_shared_examples.rb +++ b/spec/support/shared_examples/controllers/issuable_notes_filter_shared_examples.rb @@ -18,23 +18,6 @@ RSpec.shared_examples 'issuable notes filter' do expect(UserPreference.count).to eq(1) end - it 'expires notes e-tag cache for issuable if filter changed' do - notes_filter = UserPreference::NOTES_FILTERS[:only_comments] - - expect_any_instance_of(issuable.class).to receive(:expire_note_etag_cache) - - get :discussions, params: params.merge(notes_filter: notes_filter) - end - - it 'does not expires notes e-tag cache for issuable if filter did not change' do - notes_filter = UserPreference::NOTES_FILTERS[:only_comments] - user.set_notes_filter(notes_filter, issuable) - - expect_any_instance_of(issuable.class).not_to receive(:expire_note_etag_cache) - - get :discussions, params: params.merge(notes_filter: notes_filter) - end - it 'does not set notes filter when database is in read-only mode' do allow(Gitlab::Database).to receive(:read_only?).and_return(true) notes_filter = UserPreference::NOTES_FILTERS[:only_comments] diff --git a/spec/support/shared_examples/controllers/labels_controller_shared_examples.rb b/spec/support/shared_examples/controllers/labels_controller_shared_examples.rb new file mode 100644 index 00000000000..aea552f6ac7 --- /dev/null +++ b/spec/support/shared_examples/controllers/labels_controller_shared_examples.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'lock_on_merge when editing labels' do + context 'when feature flag is disabled' do + before do + stub_feature_flags(enforce_locked_labels_on_merge: false) + visit edit_label_path_unlocked + end + + it 'does not display the checkbox/help text' do + expect(page).not_to have_content(_('Lock label after a merge request is merged')) + expect(page).not_to have_content(label_lock_on_merge_help_text) + end + end + + it 'updates lock_on_merge' do + expect(page).to have_content(_('Lock label after a merge request is merged')) + expect(page).to have_content(label_lock_on_merge_help_text) + + check(_('Lock label after a merge request is merged')) + click_button 'Save changes' + + expect(label_unlocked.reload.lock_on_merge).to be_truthy + end + + it 'checkbox is disabled if lock_on_merge already set' do + visit edit_label_path_locked + + expect(page.find('#label_lock_on_merge')).to be_disabled + end +end + +RSpec.shared_examples 'lock_on_merge when creating labels' do + it 'is not supported when creating a label' do + expect(page).not_to have_content(_('Lock label after a merge request is merged')) + expect(page).not_to have_content(label_lock_on_merge_help_text) + end +end diff --git a/spec/support/shared_examples/controllers/search_rate_limit_shared_examples.rb b/spec/support/shared_examples/controllers/search_rate_limit_shared_examples.rb new file mode 100644 index 00000000000..aefcdc70082 --- /dev/null +++ b/spec/support/shared_examples/controllers/search_rate_limit_shared_examples.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +# Requires a context containing: +# - user +# - params + +RSpec.shared_examples 'search request exceeding rate limit' do + include_examples 'rate limited endpoint', rate_limit_key: :search_rate_limit + + it 'allows user in allow-list to search without applying rate limit', :freeze_time, + :clean_gitlab_redis_rate_limiting do + allow(Gitlab::ApplicationRateLimiter).to receive(:threshold).with(:search_rate_limit).and_return(1) + + stub_application_setting(search_rate_limit_allowlist: [current_user.username]) + + request + request + + expect(response).to have_gitlab_http_status(:ok) + end +end diff --git a/spec/support/shared_examples/controllers/snowplow_event_tracking_examples.rb b/spec/support/shared_examples/controllers/snowplow_event_tracking_examples.rb index 3d3b619451d..29d6f202498 100644 --- a/spec/support/shared_examples/controllers/snowplow_event_tracking_examples.rb +++ b/spec/support/shared_examples/controllers/snowplow_event_tracking_examples.rb @@ -57,7 +57,7 @@ RSpec.shared_examples 'Snowplow event tracking with Redis context' do |overrides it_behaves_like 'Snowplow event tracking', overrides: overrides do let(:context) do key_path = try(:label) || action - [Gitlab::Tracking::ServicePingContext.new(data_source: :redis, key_path: key_path).to_context.to_json] + [Gitlab::Usage::MetricDefinition.context_for(key_path).to_context.to_json] 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 6c4e98c9989..f50874b6b05 100644 --- a/spec/support/shared_examples/features/2fa_shared_examples.rb +++ b/spec/support/shared_examples/features/2fa_shared_examples.rb @@ -14,7 +14,7 @@ RSpec.shared_examples 'hardware device for 2fa' do |device_type| end describe "registration" do - let(:user) { create(:user) } + let(:user) { create(:user, :no_super_sidebar) } before do gitlab_sign_in(user) @@ -67,7 +67,7 @@ RSpec.shared_examples 'hardware device for 2fa' do |device_type| end describe 'fallback code authentication' do - let(:user) { create(:user) } + let(:user) { create(:user, :no_super_sidebar) } before do # Register and logout diff --git a/spec/support/shared_examples/features/content_editor_shared_examples.rb b/spec/support/shared_examples/features/content_editor_shared_examples.rb index fff8ef915eb..3e81f969462 100644 --- a/spec/support/shared_examples/features/content_editor_shared_examples.rb +++ b/spec/support/shared_examples/features/content_editor_shared_examples.rb @@ -244,7 +244,8 @@ RSpec.shared_examples 'edits content using the content editor' do |params = { wi end end - it 'expands the link, updates the link attributes and text if text is updated' do + it 'expands the link, updates the link attributes and text if text is updated', + quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/419684' do page.within '[data-testid="link-bubble-menu"]' do fill_in 'link-text', with: 'new text' fill_in 'link-href', with: 'https://about.gitlab.com' diff --git a/spec/support/shared_examples/features/project_features_apply_to_issuables_shared_examples.rb b/spec/support/shared_examples/features/project_features_apply_to_issuables_shared_examples.rb index d410653ca43..58bf461c733 100644 --- a/spec/support/shared_examples/features/project_features_apply_to_issuables_shared_examples.rb +++ b/spec/support/shared_examples/features/project_features_apply_to_issuables_shared_examples.rb @@ -4,8 +4,8 @@ RSpec.shared_examples 'project features apply to issuables' do |klass| let(:described_class) { klass } let(:group) { create(:group) } - let(:user_in_group) { create(:group_member, :developer, user: create(:user), group: group ).user } - let(:user_outside_group) { create(:user) } + let(:user_in_group) { create(:group_member, :developer, user: create(:user, :no_super_sidebar), group: group ).user } + let(:user_outside_group) { create(:user, :no_super_sidebar) } let(:project) { create(:project, :public, project_args) } diff --git a/spec/support/shared_examples/features/protected_branches_access_control_ce_shared_examples.rb b/spec/support/shared_examples/features/protected_branches_access_control_ce_shared_examples.rb index fb882ef8a23..fc717fbac20 100644 --- a/spec/support/shared_examples/features/protected_branches_access_control_ce_shared_examples.rb +++ b/spec/support/shared_examples/features/protected_branches_access_control_ce_shared_examples.rb @@ -2,6 +2,7 @@ RSpec.shared_examples "protected branches > access control > CE" do let(:no_one) { ProtectedRef::AccessLevel.humanize(::Gitlab::Access::NO_ACCESS) } + let_it_be(:edit_form) { '.js-protected-branch-edit-form' } ProtectedRef::AccessLevel.human_access_levels.each do |(access_type_id, access_type_name)| it "allows creating protected branches that #{access_type_name} can push to" do @@ -30,7 +31,8 @@ RSpec.shared_examples "protected branches > access control > CE" do expect(ProtectedBranch.last.merge_access_levels.map(&:access_level)).to eq([access_type_id]) end - it "allows updating protected branches so that #{access_type_name} can push to them" do + it "allows updating protected branches so that #{access_type_name} can push to them", + quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/425080' do visit project_protected_branches_path(project) show_add_form @@ -41,18 +43,14 @@ RSpec.shared_examples "protected branches > access control > CE" do expect(ProtectedBranch.count).to eq(1) - within(".protected-branches-list") do - within_select(".js-allowed-to-push") do - click_on(access_type_name) - end - end - + set_allowed_to('push', access_type_name, form: edit_form) wait_for_requests expect(ProtectedBranch.last.push_access_levels.map(&:access_level)).to include(access_type_id) end - it "allows updating protected branches so that #{access_type_name} can merge to them" do + it "allows updating protected branches so that #{access_type_name} can merge to them", + quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/425080' do visit project_protected_branches_path(project) show_add_form @@ -63,12 +61,7 @@ RSpec.shared_examples "protected branches > access control > CE" do expect(ProtectedBranch.count).to eq(1) - within(".protected-branches-list") do - within_select(".js-allowed-to-merge") do - click_on(access_type_name) - end - end - + set_allowed_to('merge', access_type_name, form: edit_form) wait_for_requests expect(ProtectedBranch.last.merge_access_levels.map(&:access_level)).to include(access_type_id) diff --git a/spec/support/shared_examples/features/protected_tags_with_deploy_keys_examples.rb b/spec/support/shared_examples/features/protected_tags_with_deploy_keys_examples.rb index 703ba5b018a..2b7147fa4b4 100644 --- a/spec/support/shared_examples/features/protected_tags_with_deploy_keys_examples.rb +++ b/spec/support/shared_examples/features/protected_tags_with_deploy_keys_examples.rb @@ -19,7 +19,7 @@ RSpec.shared_examples 'Deploy keys with protected tags' do find(".js-allowed-to-create").click wait_for_requests - within('[data-testid="allowed-to-create-dropdown"]') do + within('.dropdown-menu') do dropdown_headers = page.all('.dropdown-header').map(&:text) expect(dropdown_headers).to contain_exactly(*all_dropdown_sections) @@ -53,7 +53,7 @@ RSpec.shared_examples 'Deploy keys with protected tags' do find(".js-allowed-to-create").click wait_for_requests - within('[data-testid="allowed-to-create-dropdown"]') do + within('.dropdown-menu') do dropdown_headers = page.all('.dropdown-header').map(&:text) expect(dropdown_headers).to contain_exactly(*dropdown_sections_minus_deploy_keys) diff --git a/spec/support/shared_examples/features/runners_shared_examples.rb b/spec/support/shared_examples/features/runners_shared_examples.rb index 0c043f48c5f..861c205337a 100644 --- a/spec/support/shared_examples/features/runners_shared_examples.rb +++ b/spec/support/shared_examples/features/runners_shared_examples.rb @@ -94,6 +94,17 @@ RSpec.shared_examples 'shows runner in list' do end end +RSpec.shared_examples 'shows runner details from list' do + it 'shows runner details page' do + click_link("##{runner.id} (#{runner.short_sha})") + + expect(current_url).to include(runner_page_path) + + expect(page).to have_selector 'h1', text: "##{runner.id} (#{runner.short_sha})" + expect(page).to have_content "#{s_('Runners|Description')} #{runner.description}" + end +end + RSpec.shared_examples 'pauses, resumes and deletes a runner' do include Spec::Support::Helpers::ModalHelpers @@ -191,6 +202,13 @@ RSpec.shared_examples 'shows runner jobs tab' do end end +RSpec.shared_examples 'shows locked field' do + it 'shows locked checkbox with description', :js do + expect(page).to have_selector('input[type="checkbox"][name="locked"]') + expect(page).to have_content(_('Lock to current projects')) + end +end + RSpec.shared_examples 'submits edit runner form' do it 'breadcrumb contains runner id and token' do page.within '[data-testid="breadcrumb-links"]' do diff --git a/spec/support/shared_examples/features/sidebar/sidebar_labels_shared_examples.rb b/spec/support/shared_examples/features/sidebar/sidebar_labels_shared_examples.rb index 8ebec19a884..c2d144bef3b 100644 --- a/spec/support/shared_examples/features/sidebar/sidebar_labels_shared_examples.rb +++ b/spec/support/shared_examples/features/sidebar/sidebar_labels_shared_examples.rb @@ -86,7 +86,7 @@ RSpec.shared_examples 'labels sidebar widget' do context 'creating a label', :js do before do page.within(labels_widget) do - page.find('[data-testid="create-label-button"]').click + click_button 'Create project label' end end @@ -96,12 +96,11 @@ RSpec.shared_examples 'labels sidebar widget' do end end - it 'creates new label', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/391240' do + it 'creates new label' do page.within(labels_widget) do fill_in 'Name new label', with: 'wontfix' - page.find('.suggest-colors a', match: :first).click - page.find('button', text: 'Create').click - wait_for_requests + click_link 'Magenta-pink' + click_button 'Create' expect(page).to have_content 'wontfix' end diff --git a/spec/support/shared_examples/features/snippets_shared_examples.rb b/spec/support/shared_examples/features/snippets_shared_examples.rb index bf870b3ce66..383f81d048f 100644 --- a/spec/support/shared_examples/features/snippets_shared_examples.rb +++ b/spec/support/shared_examples/features/snippets_shared_examples.rb @@ -52,7 +52,7 @@ RSpec.shared_examples 'tabs with counts' do end RSpec.shared_examples 'does not show New Snippet button' do - let(:user) { create(:user, :external) } + let(:user) { create(:user, :external, :no_super_sidebar) } specify do sign_in(user) diff --git a/spec/support/shared_examples/features/variable_list_pagination_shared_examples.rb b/spec/support/shared_examples/features/variable_list_pagination_shared_examples.rb index 0b0c9edcb42..c1057671699 100644 --- a/spec/support/shared_examples/features/variable_list_pagination_shared_examples.rb +++ b/spec/support/shared_examples/features/variable_list_pagination_shared_examples.rb @@ -41,15 +41,15 @@ RSpec.shared_examples 'variable list pagination' do |variable_type| it 'sorts variables alphabetically in ASC and DESC order' do page.within('[data-testid="ci-variable-table"]') do - expect(find('.js-ci-variable-row:nth-child(1) td[data-label="Key"]').text).to eq(variable.key) - expect(find('.js-ci-variable-row:nth-child(20) td[data-label="Key"]').text).to eq('test_key_8') + expect(find('.js-ci-variable-row:nth-child(1) td[data-label="Key"]')).to have_content(variable.key) + expect(find('.js-ci-variable-row:nth-child(20) td[data-label="Key"]')).to have_content('test_key_8') end click_button 'Next' wait_for_requests page.within('[data-testid="ci-variable-table"]') do - expect(find('.js-ci-variable-row:nth-child(1) td[data-label="Key"]').text).to eq('test_key_9') + expect(find('.js-ci-variable-row:nth-child(1) td[data-label="Key"]')).to have_content('test_key_9') end page.within('[data-testid="ci-variable-table"]') do @@ -59,8 +59,8 @@ RSpec.shared_examples 'variable list pagination' do |variable_type| wait_for_requests page.within('[data-testid="ci-variable-table"]') do - expect(find('.js-ci-variable-row:nth-child(1) td[data-label="Key"]').text).to eq('test_key_9') - expect(find('.js-ci-variable-row:nth-child(20) td[data-label="Key"]').text).to eq('test_key_0') + expect(find('.js-ci-variable-row:nth-child(1) td[data-label="Key"]')).to have_content('test_key_9') + expect(find('.js-ci-variable-row:nth-child(20) td[data-label="Key"]')).to have_content('test_key_0') end end end diff --git a/spec/support/shared_examples/features/variable_list_shared_examples.rb b/spec/support/shared_examples/features/variable_list_shared_examples.rb index 3a91b798bbd..5951d3e781b 100644 --- a/spec/support/shared_examples/features/variable_list_shared_examples.rb +++ b/spec/support/shared_examples/features/variable_list_shared_examples.rb @@ -3,7 +3,7 @@ RSpec.shared_examples 'variable list' do it 'shows a list of variables' do page.within('[data-testid="ci-variable-table"]') do - expect(find('.js-ci-variable-row:nth-child(1) td[data-label="Key"]').text).to eq(variable.key) + expect(find('.js-ci-variable-row:nth-child(1) td[data-label="Key"]')).to have_content(variable.key) end end @@ -17,7 +17,7 @@ RSpec.shared_examples 'variable list' do wait_for_requests page.within('[data-testid="ci-variable-table"]') do - expect(find('.js-ci-variable-row:nth-child(1) td[data-label="Key"]').text).to eq('key') + expect(find('.js-ci-variable-row:nth-child(1) td[data-label="Key"]')).to have_content('key') end end @@ -31,8 +31,8 @@ RSpec.shared_examples 'variable list' do wait_for_requests page.within('[data-testid="ci-variable-table"]') do - expect(find(".js-ci-variable-row:nth-child(1) td[data-label='#{s_('CiVariables|Key')}']").text).to eq('key') - expect(find(".js-ci-variable-row:nth-child(1) td[data-label='#{s_('CiVariables|Attributes')}']")).to have_content(s_('CiVariables|Protected')) + expect(find(".js-ci-variable-row:nth-child(1) td[data-label='#{s_('CiVariables|Key')}']")).to have_content('key') + expect(find(".js-ci-variable-row:nth-child(1) td[data-label='#{s_('CiVariables|Key')}']")).to have_content(s_('CiVariables|Protected')) end end @@ -46,25 +46,25 @@ RSpec.shared_examples 'variable list' do wait_for_requests page.within('[data-testid="ci-variable-table"]') do - expect(find(".js-ci-variable-row:nth-child(1) td[data-label='#{s_('CiVariables|Key')}']").text).to eq('key') - expect(find(".js-ci-variable-row:nth-child(1) td[data-label='#{s_('CiVariables|Attributes')}']")).not_to have_content(s_('CiVariables|Masked')) + expect(find(".js-ci-variable-row:nth-child(1) td[data-label='#{s_('CiVariables|Key')}']")).to have_content('key') + expect(find(".js-ci-variable-row:nth-child(1) td[data-label='#{s_('CiVariables|Key')}']")).not_to have_content(s_('CiVariables|Masked')) end end it 'reveals and hides variables' do page.within('[data-testid="ci-variable-table"]') do - expect(first('.js-ci-variable-row td[data-label="Key"]').text).to eq(variable.key) + expect(first('.js-ci-variable-row td[data-label="Key"]')).to have_content(variable.key) expect(page).to have_content('*' * 5) click_button('Reveal value') - expect(first('.js-ci-variable-row td[data-label="Key"]').text).to eq(variable.key) - expect(first('.js-ci-variable-row td[data-label="Value"]').text).to eq(variable.value) + expect(first('.js-ci-variable-row td[data-label="Key"]')).to have_content(variable.key) + expect(first('.js-ci-variable-row td[data-label="Value"]')).to have_content(variable.value) expect(page).not_to have_content('*' * 5) click_button('Hide value') - expect(first('.js-ci-variable-row td[data-label="Key"]').text).to eq(variable.key) + expect(first('.js-ci-variable-row td[data-label="Key"]')).to have_content(variable.key) expect(page).to have_content('*' * 5) end end @@ -98,7 +98,7 @@ RSpec.shared_examples 'variable list' do wait_for_requests - expect(first('.js-ci-variable-row td[data-label="Key"]').text).to eq('new_key') + expect(first('.js-ci-variable-row td[data-label="Key"]')).to have_content('new_key') end it 'edits a variable to be unmasked' do @@ -116,8 +116,8 @@ RSpec.shared_examples 'variable list' do wait_for_requests page.within('[data-testid="ci-variable-table"]') do - expect(find(".js-ci-variable-row:nth-child(1) td[data-label='#{s_('CiVariables|Attributes')}']")).to have_content(s_('CiVariables|Protected')) - expect(find(".js-ci-variable-row:nth-child(1) td[data-label='#{s_('CiVariables|Attributes')}']")).not_to have_content(s_('CiVariables|Masked')) + expect(find(".js-ci-variable-row:nth-child(1) td[data-label='#{s_('CiVariables|Key')}']")).to have_content(s_('CiVariables|Protected')) + expect(find(".js-ci-variable-row:nth-child(1) td[data-label='#{s_('CiVariables|Key')}']")).not_to have_content(s_('CiVariables|Masked')) end end @@ -145,7 +145,7 @@ RSpec.shared_examples 'variable list' do end page.within('[data-testid="ci-variable-table"]') do - expect(find(".js-ci-variable-row:nth-child(1) td[data-label='#{s_('CiVariables|Attributes')}']")).to have_content(s_('CiVariables|Masked')) + expect(find(".js-ci-variable-row:nth-child(1) td[data-label='#{s_('CiVariables|Key')}']")).to have_content(s_('CiVariables|Masked')) end end @@ -234,9 +234,9 @@ RSpec.shared_examples 'variable list' do # expect to find 3 rows of variables in alphabetical order expect(page).to have_selector('.js-ci-variable-row', count: 3) rows = all('.js-ci-variable-row') - expect(rows[0].find('td[data-label="Key"]').text).to eq('ckey') - expect(rows[1].find('td[data-label="Key"]').text).to eq('test_key') - expect(rows[2].find('td[data-label="Key"]').text).to eq('zkey') + expect(rows[0].find('td[data-label="Key"]')).to have_content('ckey') + expect(rows[1].find('td[data-label="Key"]')).to have_content('test_key') + expect(rows[2].find('td[data-label="Key"]')).to have_content('zkey') end context 'defaults to the application setting' do diff --git a/spec/support/shared_examples/features/work_items_shared_examples.rb b/spec/support/shared_examples/features/work_items_shared_examples.rb index d3863c9a675..18e0cfdad00 100644 --- a/spec/support/shared_examples/features/work_items_shared_examples.rb +++ b/spec/support/shared_examples/features/work_items_shared_examples.rb @@ -179,6 +179,45 @@ RSpec.shared_examples 'work items assignees' do expect(work_item.reload.assignees).to include(user) end + + it 'successfully assigns the current user by clicking `Assign myself` button' do + find('[data-testid="work-item-assignees-input"]').hover + find('[data-testid="assign-self"]').click + wait_for_requests + + expect(work_item.reload.assignees).to include(user) + end + + it 'successfully removes all users on clear all button click' do + find('[data-testid="work-item-assignees-input"]').hover + find('[data-testid="assign-self"]').click + wait_for_requests + + expect(work_item.reload.assignees).to include(user) + + find('[data-testid="work-item-assignees-input"]').click + find('[data-testid="clear-all-button"]').click + find("body").click + wait_for_requests + + expect(work_item.reload.assignees).not_to include(user) + end + + it 'successfully removes user on clicking badge cross button' do + find('[data-testid="work-item-assignees-input"]').hover + find('[data-testid="assign-self"]').click + wait_for_requests + + expect(work_item.reload.assignees).to include(user) + + within('[data-testid="work-item-assignees-input"]') do + find('[data-testid="close-icon"]').click + end + find("body").click + wait_for_requests + + expect(work_item.reload.assignees).not_to include(user) + end end RSpec.shared_examples 'work items labels' do diff --git a/spec/support/shared_examples/finders/issues_finder_shared_examples.rb b/spec/support/shared_examples/finders/issues_finder_shared_examples.rb index 19001abcbe2..ed8feebf1f6 100644 --- a/spec/support/shared_examples/finders/issues_finder_shared_examples.rb +++ b/spec/support/shared_examples/finders/issues_finder_shared_examples.rb @@ -1406,7 +1406,7 @@ RSpec.shared_examples 'issues or work items finder' do |factory, execute_context it 'returns true' do expect(finder.use_cte_for_search?).to be_truthy expect(finder.execute.to_sql) - .to match(/^WITH "issues" AS #{Gitlab::Database::AsWithMaterialized.materialized_if_supported}/) + .to match(/^WITH "issues" AS MATERIALIZED/) end end @@ -1416,7 +1416,7 @@ RSpec.shared_examples 'issues or work items finder' do |factory, execute_context it 'returns true' do expect(finder.use_cte_for_search?).to be_truthy expect(finder.execute.to_sql) - .to match(/^WITH "issues" AS #{Gitlab::Database::AsWithMaterialized.materialized_if_supported}/) + .to match(/^WITH "issues" AS MATERIALIZED/) end end @@ -1426,7 +1426,7 @@ RSpec.shared_examples 'issues or work items finder' do |factory, execute_context it 'returns true' do expect(finder.use_cte_for_search?).to be_truthy expect(finder.execute.to_sql) - .to match(/^WITH "issues" AS #{Gitlab::Database::AsWithMaterialized.materialized_if_supported}/) + .to match(/^WITH "issues" AS MATERIALIZED/) end end @@ -1436,7 +1436,7 @@ RSpec.shared_examples 'issues or work items finder' do |factory, execute_context it 'returns true' do expect(finder.use_cte_for_search?).to be_truthy expect(finder.execute.to_sql) - .to match(/^WITH "issues" AS #{Gitlab::Database::AsWithMaterialized.materialized_if_supported}/) + .to match(/^WITH "issues" AS MATERIALIZED/) end end end diff --git a/spec/support/shared_examples/graphql/mutations/update_time_estimate_shared_examples.rb b/spec/support/shared_examples/graphql/mutations/update_time_estimate_shared_examples.rb index d6d360bb413..a69b56c3d58 100644 --- a/spec/support/shared_examples/graphql/mutations/update_time_estimate_shared_examples.rb +++ b/spec/support/shared_examples/graphql/mutations/update_time_estimate_shared_examples.rb @@ -6,11 +6,25 @@ RSpec.shared_examples 'updating time estimate' do let(:input_params) { input.merge(extra_params).merge({ timeEstimate: time_estimate }) } + before do + resource.update!(time_estimate: 1800) + end + + context 'when time estimate is not provided' do + let(:input_params) { input.merge(extra_params).except(:timeEstimate) } + + it 'does not update' do + expect { post_graphql_mutation(mutation, current_user: current_user) } + .not_to change { resource.reload.time_estimate } + end + end + context 'when time estimate is not a valid numerical value' do let(:time_estimate) { '-3.5d' } it 'does not update' do - expect { post_graphql_mutation(mutation, current_user: current_user) }.not_to change { resource.time_estimate } + expect { post_graphql_mutation(mutation, current_user: current_user) } + .not_to change { resource.reload.time_estimate } end it 'returns error' do @@ -24,7 +38,8 @@ RSpec.shared_examples 'updating time estimate' do let(:time_estimate) { 'nonsense' } it 'does not update' do - expect { post_graphql_mutation(mutation, current_user: current_user) }.not_to change { resource.time_estimate } + expect { post_graphql_mutation(mutation, current_user: current_user) } + .not_to change { resource.reload.time_estimate } end it 'returns error' do @@ -47,6 +62,7 @@ RSpec.shared_examples 'updating time estimate' do '1h' | 3600 '0h' | 0 '-0h' | 0 + nil | 0 end with_them do diff --git a/spec/support/shared_examples/graphql/types/gitlab_style_deprecations_shared_examples.rb b/spec/support/shared_examples/graphql/types/gitlab_style_deprecations_shared_examples.rb index b346f35bdc9..2c94f21e144 100644 --- a/spec/support/shared_examples/graphql/types/gitlab_style_deprecations_shared_examples.rb +++ b/spec/support/shared_examples/graphql/types/gitlab_style_deprecations_shared_examples.rb @@ -61,14 +61,6 @@ RSpec.shared_examples 'Gitlab-style deprecations' do expect(deprecable.deprecation_reason).to eq('This was renamed. Deprecated in 1.10.') end - it 'supports named reasons: alpha' do - deprecable = subject(deprecated: { milestone: '1.10', reason: :alpha }) - - expect(deprecable.deprecation_reason).to eq( - 'This feature is an Experiment. It can be changed or removed at any time. Introduced in 1.10.' - ) - end - it 'supports :alpha' do deprecable = subject(alpha: { milestone: '1.10' }) @@ -82,7 +74,7 @@ RSpec.shared_examples 'Gitlab-style deprecations' do subject(alpha: { milestone: '1.10' }, deprecated: { milestone: '1.10', reason: 'my reason' } ) end.to raise_error( ArgumentError, - eq("`experiment` and `deprecated` arguments cannot be passed at the same time") + eq("`alpha` and `deprecated` arguments cannot be passed at the same time") ) end diff --git a/spec/support/shared_examples/harbor/artifacts_controller_shared_examples.rb b/spec/support/shared_examples/harbor/artifacts_controller_shared_examples.rb index 85fcd426e3d..16e25bf96dd 100644 --- a/spec/support/shared_examples/harbor/artifacts_controller_shared_examples.rb +++ b/spec/support/shared_examples/harbor/artifacts_controller_shared_examples.rb @@ -87,17 +87,7 @@ RSpec.shared_examples 'a harbor artifacts controller' do |args| get harbor_artifact_url(container, repository_id), headers: json_header end - context 'with harbor registry feature flag enabled' do - it_behaves_like 'responds with 200 status with json' - end - - context 'with harbor registry feature flag disabled' do - before do - stub_feature_flags(harbor_registry_integration: false) - end - - it_behaves_like 'responds with 404 status' - end + it_behaves_like 'responds with 200 status with json' context 'with anonymous user' do before do diff --git a/spec/support/shared_examples/harbor/repositories_controller_shared_examples.rb b/spec/support/shared_examples/harbor/repositories_controller_shared_examples.rb index b35595a10b2..a0d47d1a2d1 100644 --- a/spec/support/shared_examples/harbor/repositories_controller_shared_examples.rb +++ b/spec/support/shared_examples/harbor/repositories_controller_shared_examples.rb @@ -87,17 +87,7 @@ RSpec.shared_examples 'a harbor repositories controller' do |args| get harbor_repository_url(container) end - context 'with harbor registry feature flag enabled' do - it_behaves_like 'responds with 200 status with html' - end - - context 'with harbor registry feature flag disabled' do - before do - stub_feature_flags(harbor_registry_integration: false) - end - - it_behaves_like 'responds with 404 status' - end + it_behaves_like 'responds with 200 status with html' context 'with anonymous user' do before do @@ -121,17 +111,7 @@ RSpec.shared_examples 'a harbor repositories controller' do |args| get harbor_repository_url(container), headers: json_header end - context 'with harbor registry feature flag enabled' do - it_behaves_like 'responds with 200 status with json' - end - - context 'with harbor registry feature flag disabled' do - before do - stub_feature_flags(harbor_registry_integration: false) - end - - it_behaves_like 'responds with 404 status' - end + it_behaves_like 'responds with 200 status with json' context 'with valid params' do context 'with valid page params' do diff --git a/spec/support/shared_examples/harbor/tags_controller_shared_examples.rb b/spec/support/shared_examples/harbor/tags_controller_shared_examples.rb index 46fea7fdff6..aee728295de 100644 --- a/spec/support/shared_examples/harbor/tags_controller_shared_examples.rb +++ b/spec/support/shared_examples/harbor/tags_controller_shared_examples.rb @@ -76,17 +76,7 @@ RSpec.shared_examples 'a harbor tags controller' do |args| headers: json_header) end - context 'with harbor registry feature flag enabled' do - it_behaves_like 'responds with 200 status with json' - end - - context 'with harbor registry feature flag disabled' do - before do - stub_feature_flags(harbor_registry_integration: false) - end - - it_behaves_like 'responds with 404 status' - end + it_behaves_like 'responds with 200 status with json' context 'with anonymous user' do before do diff --git a/spec/support/shared_examples/lib/api/ai_workhorse_shared_examples.rb b/spec/support/shared_examples/lib/api/ai_workhorse_shared_examples.rb deleted file mode 100644 index d4fe45a91a0..00000000000 --- a/spec/support/shared_examples/lib/api/ai_workhorse_shared_examples.rb +++ /dev/null @@ -1,45 +0,0 @@ -# frozen_string_literal: true - -RSpec.shared_examples 'behind AI related feature flags' do |provider_flag| - context "when #{provider_flag} is disabled" do - before do - stub_feature_flags(provider_flag => false) - end - - it 'responds as not found' do - post api(url, current_user), params: input_params - - expect(response).to have_gitlab_http_status(:not_found) - end - end - - context 'when ai_experimentation_api is disabled' do - before do - stub_feature_flags(ai_experimentation_api: false) - end - - it 'responds as not found' do - post api(url, current_user), params: input_params - - expect(response).to have_gitlab_http_status(:not_found) - end - end -end - -RSpec.shared_examples 'delegates AI request to Workhorse' do - it 'responds with Workhorse send-url headers' do - post api(url, current_user), params: input_params - - expect(response.body).to eq('""') - expect(response).to have_gitlab_http_status(:ok) - - send_url_prefix, encoded_data = response.headers['Gitlab-Workhorse-Send-Data'].split(':') - data = Gitlab::Json.parse(Base64.urlsafe_decode64(encoded_data)) - - expect(send_url_prefix).to eq('send-url') - expect(data).to eq({ - 'AllowRedirects' => false, - 'Method' => 'POST' - }.merge(expected_params)) - end -end diff --git a/spec/support/shared_examples/lib/gitlab/bitbucket_import/object_import_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/bitbucket_import/object_import_shared_examples.rb new file mode 100644 index 00000000000..3dbe43d822f --- /dev/null +++ b/spec/support/shared_examples/lib/gitlab/bitbucket_import/object_import_shared_examples.rb @@ -0,0 +1,100 @@ +# frozen_string_literal: true + +RSpec.shared_examples Gitlab::BitbucketImport::ObjectImporter do + include AfterNextHelpers + + describe '.sidekiq_retries_exhausted' do + let(:job) { { 'args' => [1, {}, 'key'], 'jid' => 'jid' } } + + it 'notifies the waiter' do + expect(Gitlab::JobWaiter).to receive(:notify).with('key', 'jid') + + described_class.sidekiq_retries_exhausted_block.call(job, StandardError.new) + end + end + + describe '#perform' do + let_it_be(:import_started_project) { create(:project, :import_started) } + + let(:project_id) { project_id } + let(:waiter_key) { 'key' } + + shared_examples 'notifies the waiter' do + specify do + allow_next(worker.importer_class).to receive(:execute) + + expect(Gitlab::JobWaiter).to receive(:notify).with(waiter_key, anything) + + worker.perform(project_id, {}, waiter_key) + end + end + + context 'when project does not exist' do + let(:project_id) { non_existing_record_id } + + it_behaves_like 'notifies the waiter' + end + + context 'when project has import started' do + let_it_be(:project) do + create(:project, :import_started, import_data_attributes: { + data: { 'project_key' => 'key', 'repo_slug' => 'slug' }, + credentials: { 'token' => 'token' } + }) + end + + let(:project_id) { project.id } + + it 'calls the importer' do + expect(Gitlab::BitbucketImport::Logger).to receive(:info).twice + expect_next(worker.importer_class, project, kind_of(Hash)).to receive(:execute) + + worker.perform(project_id, {}, waiter_key) + end + + it_behaves_like 'notifies the waiter' + + context 'when the importer raises an ActiveRecord::RecordInvalid error' do + before do + allow_next(worker.importer_class).to receive(:execute).and_raise(ActiveRecord::RecordInvalid) + end + + it 'tracks the error' do + expect(Gitlab::Import::ImportFailureService).to receive(:track).once + + worker.perform(project_id, {}, waiter_key) + end + end + + context 'when the importer raises a StandardError' do + before do + allow_next(worker.importer_class).to receive(:execute).and_raise(StandardError) + end + + it 'tracks the error and raises the error' do + expect(Gitlab::Import::ImportFailureService).to receive(:track).once + + expect { worker.perform(project_id, {}, waiter_key) }.to raise_error(StandardError) + end + end + end + + context 'when project import has been cancelled' do + let_it_be(:project_id) { create(:project, :import_canceled).id } + + it 'does not call the importer' do + expect_next(worker.importer_class).not_to receive(:execute) + + worker.perform(project_id, {}, waiter_key) + end + + it_behaves_like 'notifies the waiter' + end + end + + describe '#importer_class' do + it 'does not raise a NotImplementedError' do + expect(worker.importer_class).not_to be_nil + end + end +end diff --git a/spec/support/shared_examples/lib/gitlab/bitbucket_import/stage_methods_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/bitbucket_import/stage_methods_shared_examples.rb new file mode 100644 index 00000000000..f128aa92a53 --- /dev/null +++ b/spec/support/shared_examples/lib/gitlab/bitbucket_import/stage_methods_shared_examples.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +RSpec.shared_examples Gitlab::BitbucketImport::StageMethods do + describe '.sidekiq_retries_exhausted' do + let(:job) { { 'args' => [project.id] } } + + it 'tracks the import failure' do + expect(Gitlab::Import::ImportFailureService) + .to receive(:track).with( + project_id: project.id, + exception: StandardError.new, + fail_import: true + ) + + described_class.sidekiq_retries_exhausted_block.call(job, StandardError.new) + end + end + + describe '.perform' do + let(:worker) { described_class.new } + + it 'executes the import' do + expect(worker).to receive(:import).with(project).once + expect(Gitlab::BitbucketImport::Logger).to receive(:info).twice + + worker.perform(project.id) + end + end +end diff --git a/spec/support/shared_examples/lib/gitlab/bitbucket_server_import/object_import_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/bitbucket_server_import/object_import_shared_examples.rb index ec2ae0b8a73..4eae8632467 100644 --- a/spec/support/shared_examples/lib/gitlab/bitbucket_server_import/object_import_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/bitbucket_server_import/object_import_shared_examples.rb @@ -7,7 +7,7 @@ RSpec.shared_examples Gitlab::BitbucketServerImport::ObjectImporter do let(:job) { { 'args' => [1, {}, 'key'], 'jid' => 'jid' } } it 'notifies the waiter' do - expect(Gitlab::JobWaiter).to receive(:notify).with('key', 'jid') + expect(Gitlab::JobWaiter).to receive(:notify).with('key', 'jid', ttl: Gitlab::Import::JOB_WAITER_TTL) described_class.sidekiq_retries_exhausted_block.call(job, StandardError.new) end @@ -23,7 +23,7 @@ RSpec.shared_examples Gitlab::BitbucketServerImport::ObjectImporter do specify do allow_next(worker.importer_class).to receive(:execute) - expect(Gitlab::JobWaiter).to receive(:notify).with(waiter_key, anything) + expect(Gitlab::JobWaiter).to receive(:notify).with(waiter_key, anything, ttl: Gitlab::Import::JOB_WAITER_TTL) worker.perform(project_id, {}, waiter_key) 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 10f58748698..7cfab5c8295 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 ::Ci::Build.sticking.unstick_or_continue_sticking' do - expect(::Ci::Build.sticking).to receive(:unstick_or_continue_sticking) + it 'calls ::Ci::Build.sticking.find_caught_up_replica' do + expect(::Ci::Build.sticking).to receive(:find_caught_up_replica) .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 ::Ci::Build.sticking.unstick_or_continue_sticking' do - expect(::Ci::Build.sticking).not_to receive(:unstick_or_continue_sticking) + it 'does not call ::Ci::Build.sticking.find_caught_up_replica' do + expect(::Ci::Build.sticking).not_to receive(:find_caught_up_replica) trace.read { |stream| stream } 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 df795723874..b80a51a1fc6 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 @@ -4,40 +4,17 @@ RSpec.shared_examples 'CTE with MATERIALIZED keyword examples' do describe 'adding MATERIALIZE to the CTE' do let(:options) { {} } - before do - # Clear the cached value before the test - Gitlab::Database::AsWithMaterialized.clear_memoization(:materialized_supported) - end - - context 'when PG version is <12' do - it 'does not add MATERIALIZE keyword' do - allow(ApplicationRecord.database).to receive(:version).and_return('11.1') + it 'adds MATERIALIZE keyword' do + allow(ApplicationRecord.database).to receive(:version).and_return('12.1') - expect(query).to include(expected_query_block_without_materialized) - end + expect(query).to include(expected_query_block_with_materialized) end - context 'when PG version is >=12' do - it 'adds MATERIALIZE keyword' do - allow(ApplicationRecord.database).to receive(:version).and_return('12.1') - - expect(query).to include(expected_query_block_with_materialized) - end + context 'when materialized is disabled' do + let(:options) { { materialized: false } } - context 'when version is higher than 12' do - it 'adds MATERIALIZE keyword' do - allow(ApplicationRecord.database).to receive(:version).and_return('15.1') - - expect(query).to include(expected_query_block_with_materialized) - end - end - - context 'when materialized is disabled' do - let(:options) { { materialized: false } } - - it 'does not add MATERIALIZE keyword' do - expect(query).to include(expected_query_block_without_materialized) - end + it 'does not add MATERIALIZE keyword' do + expect(query).to include(expected_query_block_without_materialized) end end end diff --git a/spec/support/shared_examples/lib/gitlab/import/advance_stage_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/import/advance_stage_shared_examples.rb new file mode 100644 index 00000000000..0fef5269ab6 --- /dev/null +++ b/spec/support/shared_examples/lib/gitlab/import/advance_stage_shared_examples.rb @@ -0,0 +1,109 @@ +# frozen_string_literal: true + +RSpec.shared_examples Gitlab::Import::AdvanceStage do |factory:| + let_it_be(:project) { create(:project) } + let_it_be_with_reload(:import_state) { create(factory, :started, project: project, jid: '123') } + let(:worker) { described_class.new } + let(:next_stage) { :finish } + + describe '#perform', :clean_gitlab_redis_shared_state do + context 'when the project no longer exists' do + it 'does not perform any work' do + expect(worker).not_to receive(:wait_for_jobs) + + worker.perform(non_existing_record_id, { '123' => 2 }, next_stage) + end + end + + context 'when there are remaining jobs' do + it 'reschedules itself' do + expect(worker) + .to receive(:wait_for_jobs) + .with({ '123' => 2 }) + .and_return({ '123' => 1 }) + + expect(described_class) + .to receive(:perform_in) + .with(described_class::INTERVAL, project.id, { '123' => 1 }, next_stage) + + worker.perform(project.id, { '123' => 2 }, next_stage) + end + + context 'when the project import is not running' do + before do + import_state.update_column(:status, :failed) + end + + it 'does not perform any work' do + expect(worker).not_to receive(:wait_for_jobs) + expect(described_class).not_to receive(:perform_in) + + worker.perform(project.id, { '123' => 2 }, next_stage) + end + + it 'clears the JobWaiter cache' do + expect(Gitlab::JobWaiter).to receive(:delete_key).with('123') + + worker.perform(project.id, { '123' => 2 }, next_stage) + end + end + end + + context 'when there are no remaining jobs' do + before do + allow(worker) + .to receive(:wait_for_jobs) + .with({ '123' => 2 }) + .and_return({}) + end + + it 'schedules the next stage' do + next_worker = described_class::STAGES[next_stage] + + expect_next_found_instance_of(import_state.class) do |state| + expect(state).to receive(:refresh_jid_expiration) + end + + expect(next_worker).to receive(:perform_async).with(project.id) + + worker.perform(project.id, { '123' => 2 }, next_stage) + end + + it 'raises KeyError when the stage name is invalid' do + expect { worker.perform(project.id, { '123' => 2 }, :kittens) } + .to raise_error(KeyError) + end + end + end + + describe '#wait_for_jobs' do + it 'waits for jobs to complete and returns a new pair of keys to wait for' do + waiter1 = instance_double("Gitlab::JobWaiter", jobs_remaining: 1, key: '123') + waiter2 = instance_double("Gitlab::JobWaiter", jobs_remaining: 0, key: '456') + + expect(Gitlab::JobWaiter) + .to receive(:new) + .ordered + .with(2, '123') + .and_return(waiter1) + + expect(Gitlab::JobWaiter) + .to receive(:new) + .ordered + .with(1, '456') + .and_return(waiter2) + + expect(waiter1) + .to receive(:wait) + .with(described_class::BLOCKING_WAIT_TIME) + + expect(waiter2) + .to receive(:wait) + .with(described_class::BLOCKING_WAIT_TIME) + + new_waiters = worker.wait_for_jobs({ '123' => 2, '456' => 1 }) + + expect(new_waiters).to eq({ '123' => 1 }) + end + end +end diff --git a/spec/support/shared_examples/lib/gitlab/repo_type_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/repo_type_shared_examples.rb index c2898513424..025f0d5c7ea 100644 --- a/spec/support/shared_examples/lib/gitlab/repo_type_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/repo_type_shared_examples.rb @@ -15,7 +15,7 @@ RSpec.shared_examples 'a repo type' do describe '#repository_for' do it 'finds the repository for the repo type' do - expect(described_class.repository_for(expected_repository_resolver)).to eq(expected_repository) + expect(described_class.repository_for(expected_container)).to eq(expected_repository) end it 'returns nil when container is nil' do diff --git a/spec/support/shared_examples/lib/gitlab/search_archived_filter_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/search_archived_filter_shared_examples.rb index 6b296d0e78a..ac72b31d5a4 100644 --- a/spec/support/shared_examples/lib/gitlab/search_archived_filter_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/search_archived_filter_shared_examples.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -RSpec.shared_examples 'search results filtered by archived' do |feature_flag_name| +RSpec.shared_examples 'search results filtered by archived' do |feature_flag_name, migration_name| context 'when filter not provided (all behavior)' do let(:filters) { {} } @@ -28,16 +28,33 @@ RSpec.shared_examples 'search results filtered by archived' do |feature_flag_nam end end - context "when the #{feature_flag_name} feature flag is disabled" do - let(:filters) { {} } + if feature_flag_name.present? + context "when the #{feature_flag_name} feature flag is disabled" do + let(:filters) { {} } + + before do + stub_feature_flags("#{feature_flag_name}": false) + end - before do - stub_feature_flags("#{feature_flag_name}": false) + it 'returns archived and unarchived results' do + expect(results.objects(scope)).to include unarchived_result + expect(results.objects(scope)).to include archived_result + end end + end - it 'returns archived and unarchived results' do - expect(results.objects(scope)).to include unarchived_result - expect(results.objects(scope)).to include archived_result + if migration_name.present? + context "when the #{migration_name} is not completed" do + let(:filters) { {} } + + before do + set_elasticsearch_migration_to(migration_name.to_s, including: false) + end + + it 'returns archived and unarchived results' do + expect(results.objects(scope)).to include unarchived_result + expect(results.objects(scope)).to include archived_result + end end end end diff --git a/spec/support/shared_examples/lib/gitlab/usage_data_counters/issuable_activity_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/usage_data_counters/issuable_activity_shared_examples.rb deleted file mode 100644 index 9dc18555340..00000000000 --- a/spec/support/shared_examples/lib/gitlab/usage_data_counters/issuable_activity_shared_examples.rb +++ /dev/null @@ -1,87 +0,0 @@ -# frozen_string_literal: true - -RSpec.shared_examples 'tracked issuable events' do - before do - stub_application_setting(usage_ping_enabled: true) - end - - def count_unique(date_from: Date.today.beginning_of_week, date_to: 1.week.from_now) - Gitlab::UsageDataCounters::HLLRedisCounter.unique_events(event_names: action, start_date: date_from, end_date: date_to) - end - - specify do - aggregate_failures do - expect(track_action({ author: user1 }.merge(track_params))).to be_truthy - expect(track_action({ author: user1 }.merge(track_params))).to be_truthy - expect(track_action({ author: user2 }.merge(track_params))).to be_truthy - expect(count_unique).to eq(2) - end - end - - it 'does not track edit actions if author is not present' do - expect(track_action({ author: nil }.merge(track_params))).to be_nil - end -end - -RSpec.shared_examples 'tracked issuable snowplow and service ping events for given event params' do - it_behaves_like 'tracked issuable events' - - it 'emits snowplow event' do - track_action({ author: user1 }.merge(track_params)) - - expect_snowplow_event(**{ category: category, action: event_action, user: user1 }.merge(event_params)) - end -end - -RSpec.shared_examples 'tracked issuable internal event for given event params' do - it_behaves_like 'tracked issuable events' - - it_behaves_like 'internal event tracking' do - subject(:track_event) { track_action({ author: user1 }.merge(track_params)) } - - let(:user) { user1 } - let(:namespace) { project&.namespace } - end -end - -RSpec.shared_examples 'tracked issuable internal event with project' do - it_behaves_like 'tracked issuable internal event for given event params' do - let(:track_params) { original_params || { project: project } } - end -end - -RSpec.shared_examples 'tracked issuable snowplow and service ping events with project' do - it_behaves_like 'tracked issuable snowplow and service ping events for given event params' do - let(:context) do - Gitlab::Tracking::ServicePingContext - .new(data_source: :redis_hll, event: event_property) - .to_h - end - - let(:track_params) { original_params || { project: project } } - let(:event_params) { { project: project }.merge(label: event_label, property: event_property, namespace: project.namespace, context: [context]) } - end -end - -RSpec.shared_examples 'tracked issuable snowplow and service ping events with namespace' do - it_behaves_like 'tracked issuable snowplow and service ping events for given event params' do - let(:context) do - Gitlab::Tracking::ServicePingContext - .new(data_source: :redis_hll, event: event_property) - .to_h - end - - let(:track_params) { { namespace: namespace } } - let(:event_params) { track_params.merge(label: event_label, property: event_property, context: [context]) } - end -end - -RSpec.shared_examples 'does not track with namespace when feature flag is disabled' do |feature_flag| - context "when feature flag #{feature_flag} is disabled" do - it 'does not track action' do - stub_feature_flags(feature_flag => false) - - expect(track_action(author: user1, namespace: namespace)).to be_nil - end - end -end diff --git a/spec/support/shared_examples/lib/menus_shared_examples.rb b/spec/support/shared_examples/lib/menus_shared_examples.rb index 0aa98517444..575f48c43e0 100644 --- a/spec/support/shared_examples/lib/menus_shared_examples.rb +++ b/spec/support/shared_examples/lib/menus_shared_examples.rb @@ -62,6 +62,13 @@ RSpec.shared_examples_for 'not serializable as super_sidebar_menu_args' do end end +RSpec.shared_examples_for 'a panel instantiable by the anonymous user' do + it do + context.instance_variable_set(:@current_user, nil) + expect(described_class.new(context)).to be_a(described_class) + end +end + RSpec.shared_examples_for 'a panel with uniquely identifiable menu items' do let(:menu_items) do subject.instance_variable_get(:@menus) diff --git a/spec/support/shared_examples/lib/sidebars/user_profile/user_profile_menus_shared_examples.rb b/spec/support/shared_examples/lib/sidebars/user_profile/user_profile_menus_shared_examples.rb index 5e8aebb4f29..6a43b9b4300 100644 --- a/spec/support/shared_examples/lib/sidebars/user_profile/user_profile_menus_shared_examples.rb +++ b/spec/support/shared_examples/lib/sidebars/user_profile/user_profile_menus_shared_examples.rb @@ -1,6 +1,10 @@ # frozen_string_literal: true -RSpec.shared_examples 'User profile menu' do |title:, icon:, active_route:| +RSpec.shared_examples 'User profile menu' do | + icon:, active_route:, avatar_shape: 'rect', expect_avatar: false, entity_id: nil, + # A nil title will fall back to user.name. + title: nil +| let_it_be(:current_user) { build(:user) } let_it_be(:user) { build(:user) } @@ -17,13 +21,19 @@ RSpec.shared_examples 'User profile menu' do |title:, icon:, active_route:| end it 'renders the correct title' do - expect(subject.title).to eq title + expect(subject.title).to eq(title || user.name) end it 'renders the correct icon' do expect(subject.sprite_icon).to eq icon end + it 'renders the correct avatar' do + expect(subject.avatar).to eq(expect_avatar ? user.avatar_url : nil) + expect(subject.avatar_shape).to eq(avatar_shape) + expect(subject.entity_id).to eq(entity_id) + end + it 'defines correct active route' do expect(subject.active_routes[:path]).to be active_route end diff --git a/spec/support/shared_examples/loose_foreign_keys/have_loose_foreign_key.rb b/spec/support/shared_examples/loose_foreign_keys/have_loose_foreign_key.rb index 5f59d43ad19..179bbc8734d 100644 --- a/spec/support/shared_examples/loose_foreign_keys/have_loose_foreign_key.rb +++ b/spec/support/shared_examples/loose_foreign_keys/have_loose_foreign_key.rb @@ -55,6 +55,8 @@ RSpec.shared_examples 'it has loose foreign keys' do end RSpec.shared_examples 'cleanup by a loose foreign key' do + include LooseForeignKeysHelper + let(:foreign_key_definition) do foreign_keys_for_parent = Gitlab::Database::LooseForeignKeys.definitions_by_table[parent.class.table_name] foreign_keys_for_parent.find { |definition| definition.from_table == model.class.table_name } @@ -75,9 +77,7 @@ RSpec.shared_examples 'cleanup by a loose foreign key' do expect(find_model).to be_present - LooseForeignKeys::DeletedRecord.using_connection(parent.connection) do - LooseForeignKeys::ProcessDeletedRecordsService.new(connection: parent.connection).execute - end + process_loose_foreign_key_deletions(record: parent) if foreign_key_definition.on_delete.eql?(:async_delete) expect(find_model).not_to be_present diff --git a/spec/support/shared_examples/mailers/notify_shared_examples.rb b/spec/support/shared_examples/mailers/notify_shared_examples.rb index cf1ab7697ab..987060d73b9 100644 --- a/spec/support/shared_examples/mailers/notify_shared_examples.rb +++ b/spec/support/shared_examples/mailers/notify_shared_examples.rb @@ -54,6 +54,14 @@ RSpec.shared_examples 'an email with X-GitLab headers containing IDs' do expect(subject.header["X-GitLab-#{model.class.name}-IID"]).to eq nil end end + + it 'has X-GitLab-*-State header if model has state defined' do + if model.respond_to?(:state) + is_expected.to have_header "X-GitLab-#{model.class.name}-State", model.state.to_s + else + expect(subject.header["X-GitLab-#{model.class.name}-State"]).to eq nil + end + end end RSpec.shared_examples 'an email with X-GitLab headers containing project details' do diff --git a/spec/support/shared_examples/migrations/add_work_item_widget_shared_examples.rb b/spec/support/shared_examples/migrations/add_work_item_widget_shared_examples.rb index fdb31fa5d9d..37c338a7712 100644 --- a/spec/support/shared_examples/migrations/add_work_item_widget_shared_examples.rb +++ b/spec/support/shared_examples/migrations/add_work_item_widget_shared_examples.rb @@ -32,3 +32,110 @@ RSpec.shared_examples 'migration that adds widget to work items definitions' do end end end + +# Shared examples for testing migration that adds a single widget to a work item type +# +# It expects the following variables +# - `target_type_enum_value`: Int, enum value for the target work item type, typically defined in the migration +# as a constant +# - `target_type`: Symbol, the target type's name +# - `additional_types`: Hash (optional), name of work item types and their corresponding enum value that are defined +# at the time the migration was created but are missing from `base_types`. +# - `widgets_for_type`: Hash, name of the widgets included in the target type with their corresponding enum value +RSpec.shared_examples 'migration that adds a widget to a work item type' do + include MigrationHelpers::WorkItemTypesHelper + + let(:work_item_types) { table(:work_item_types) } + let(:work_item_widget_definitions) { table(:work_item_widget_definitions) } + let(:additional_base_types) { try(:additional_types) || {} } + let(:base_types) do + { + issue: 0, + incident: 1, + test_case: 2, + requirement: 3, + task: 4, + objective: 5, + key_result: 6, + epic: 7 + }.merge!(additional_base_types) + end + + after(:all) do + # Make sure base types are recreated after running the migration + # because migration specs are not run in a transaction + reset_work_item_types + end + + before do + # Database needs to be in a similar state as when the migration was created + reset_db_state_prior_to_migration + end + + describe '#up' do + it "adds widget to work item type", :aggregate_failures do + expect do + migrate! + end.to change { work_item_widget_definitions.count }.by(1) + + work_item_type = work_item_types.find_by(namespace_id: nil, base_type: target_type_enum_value) + created_widget = work_item_widget_definitions.last + + expect(created_widget).to have_attributes( + widget_type: described_class::WIDGET_ENUM_VALUE, + name: described_class::WIDGET_NAME, + work_item_type_id: work_item_type.id + ) + end + + context 'when type does not exist' do + it 'skips creating the new widget definition' do + work_item_types.where(namespace_id: nil, base_type: base_types[target_type]).delete_all + + expect do + migrate! + end.to not_change(work_item_widget_definitions, :count) + end + end + end + + describe '#down' do + it "removes widget from work item type" do + migrate! + + expect { schema_migrate_down! }.to change { work_item_widget_definitions.count }.by(-1) + end + end + + def reset_db_state_prior_to_migration + work_item_types.delete_all + + base_types.each do |type_sym, type_enum| + create_work_item_type!(type_sym.to_s.titleize, type_enum) + end + + target_type_record = work_item_types.find_by_name(target_type.to_s.titleize) + + widgets = widgets_for_type.map do |widget_name_value, widget_enum_value| + { + work_item_type_id: target_type_record.id, + name: widget_name_value, + widget_type: widget_enum_value + } + end + + # Creating all widgets for the type so the state in the DB is as close as possible to the actual state + work_item_widget_definitions.upsert_all( + widgets, + unique_by: :index_work_item_widget_definitions_on_default_witype_and_name + ) + end + + def create_work_item_type!(type_name, type_enum_value) + work_item_types.create!( + name: type_name, + namespace_id: nil, + base_type: type_enum_value + ) + end +end diff --git a/spec/support/shared_examples/models/concerns/linkable_items_shared_examples.rb b/spec/support/shared_examples/models/concerns/linkable_items_shared_examples.rb index efd27a051fe..eb37fe66c11 100644 --- a/spec/support/shared_examples/models/concerns/linkable_items_shared_examples.rb +++ b/spec/support/shared_examples/models/concerns/linkable_items_shared_examples.rb @@ -78,5 +78,16 @@ RSpec.shared_examples 'includes LinkableItem concern' do expect(described_class.for_items(item, item2)).to contain_exactly(target_link) end end + + describe '.for_source_and_target' do + let_it_be(:item3) { create(:work_item, project: project) } + let_it_be(:link1) { create(link_factory, source: item, target: item1) } + let_it_be(:link2) { create(link_factory, source: item, target: item2) } + let_it_be(:link3) { create(link_factory, source: item, target: item3) } + + it 'includes links for provided source and target' do + expect(described_class.for_source_and_target(item, [item1, item2])).to contain_exactly(link1, link2) + end + end end end diff --git a/spec/support/shared_examples/models/group_shared_examples.rb b/spec/support/shared_examples/models/group_shared_examples.rb index 9f3359ba4ab..6397e7a87d7 100644 --- a/spec/support/shared_examples/models/group_shared_examples.rb +++ b/spec/support/shared_examples/models/group_shared_examples.rb @@ -3,7 +3,6 @@ RSpec.shared_examples 'checks self and root ancestor feature flag' do let_it_be(:root_group) { create(:group) } let_it_be(:group) { create(:group, parent: root_group) } - let_it_be(:project) { create(:project, group: group) } subject { group.public_send(feature_flag_method) } @@ -41,3 +40,47 @@ RSpec.shared_examples 'checks self and root ancestor feature flag' do it { is_expected.to be_truthy } end end + +RSpec.shared_examples 'checks self (project) and root ancestor feature flag' do + let_it_be(:root_group) { create(:group) } + let_it_be(:group) { create(:group, parent: root_group) } + let_it_be(:project) { create(:project, group: group) } + + subject { project.public_send(feature_flag_method) } + + context 'when FF is enabled for the root group' do + before do + stub_feature_flags(feature_flag => root_group) + end + + it { is_expected.to be_truthy } + end + + context 'when FF is enabled for the group' do + before do + stub_feature_flags(feature_flag => group) + end + + it { is_expected.to be_truthy } + end + + context 'when FF is enabled for the project' do + before do + stub_feature_flags(feature_flag => project) + end + + it { is_expected.to be_truthy } + end + + context 'when FF is disabled globally' do + before do + stub_feature_flags(feature_flag => false) + end + + it { is_expected.to be_falsey } + end + + context 'when FF is enabled globally' do + it { is_expected.to be_truthy } + end +end diff --git a/spec/support/shared_examples/models/members_notifications_shared_example.rb b/spec/support/shared_examples/models/members_notifications_shared_example.rb index 329cb812a08..5c783b5cfa7 100644 --- a/spec/support/shared_examples/models/members_notifications_shared_example.rb +++ b/spec/support/shared_examples/models/members_notifications_shared_example.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true RSpec.shared_examples 'members notifications' do |entity_type| + let_it_be(:user) { create(:user) } + let(:notification_service) { double('NotificationService').as_null_object } before do @@ -8,7 +10,7 @@ RSpec.shared_examples 'members notifications' do |entity_type| end describe "#after_create" do - let(:member) { build(:"#{entity_type}_member", "#{entity_type}": create(entity_type.to_s)) } + let(:member) { build(:"#{entity_type}_member", "#{entity_type}": create(entity_type.to_s), user: user) } it "sends email to user" do expect(notification_service).to receive(:"new_#{entity_type}_member").with(member) @@ -35,7 +37,9 @@ RSpec.shared_examples 'members notifications' do |entity_type| describe '#after_commit' do context 'on creation of a member requesting access' do - let(:member) { build(:"#{entity_type}_member", :access_request, "#{entity_type}": create(entity_type.to_s)) } + let(:member) do + build(:"#{entity_type}_member", :access_request, "#{entity_type}": create(entity_type.to_s), user: user) + end it "calls NotificationService.new_access_request" do expect(notification_service).to receive(:new_access_request).with(member) diff --git a/spec/support/shared_examples/models/users/pages_visits_shared_examples.rb b/spec/support/shared_examples/models/users/pages_visits_shared_examples.rb new file mode 100644 index 00000000000..0b3e8516d25 --- /dev/null +++ b/spec/support/shared_examples/models/users/pages_visits_shared_examples.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'namespace visits model' do + it { is_expected.to validate_presence_of(:entity_id) } + it { is_expected.to validate_presence_of(:user_id) } + it { is_expected.to validate_presence_of(:visited_at) } + + describe '#visited_around?' do + context 'when the checked time matches a recent visit' do + [-15.minutes, 15.minutes].each do |time_diff| + it 'returns true' do + expect(described_class.visited_around?(entity_id: entity.id, user_id: user.id, + time: base_time + time_diff)).to be(true) + end + end + end + + context 'when the checked time does not match a recent visit' do + [-16.minutes, 16.minutes].each do |time_diff| + it 'returns false' do + expect(described_class.visited_around?(entity_id: entity.id, user_id: user.id, + time: base_time + time_diff)).to be(false) + end + end + end + end +end diff --git a/spec/support/shared_examples/redis/redis_shared_examples.rb b/spec/support/shared_examples/redis/redis_shared_examples.rb index 9224e01b1fe..23ec4a632b7 100644 --- a/spec/support/shared_examples/redis/redis_shared_examples.rb +++ b/spec/support/shared_examples/redis/redis_shared_examples.rb @@ -365,6 +365,90 @@ RSpec.shared_examples "redis_shared_examples" do end end + describe "#parse_client_tls_options" do + let(:dummy_certificate) { OpenSSL::X509::Certificate.new } + let(:dummy_key) { OpenSSL::PKey::RSA.new } + let(:resque_yaml_config_without_tls) { { url: 'redis://localhost:6379' } } + let(:resque_yaml_config_with_tls) do + { + url: 'rediss://localhost:6380', + ssl_params: { + cert_file: '/tmp/client.crt', + key_file: '/tmp/client.key' + } + } + end + + let(:parsed_config_with_tls) do + { + url: 'rediss://localhost:6380', + ssl_params: { + cert: dummy_certificate, + key: dummy_key + } + } + end + + before do + allow(::File).to receive(:exist?).and_call_original + allow(::File).to receive(:read).and_call_original + end + + context 'when configuration does not have TLS related options' do + it 'returns the coniguration as-is' do + expect(subject.send(:parse_client_tls_options, + resque_yaml_config_without_tls)).to eq(resque_yaml_config_without_tls) + end + end + + context 'when specified certificate file does not exist' do + before do + allow(::File).to receive(:exist?).with("/tmp/client.crt").and_return(false) + allow(::File).to receive(:exist?).with("/tmp/client.key").and_return(true) + end + + it 'raises error about missing certificate file' do + expect do + subject.send(:parse_client_tls_options, + resque_yaml_config_with_tls) + end.to raise_error(Gitlab::Redis::Wrapper::InvalidPathError, + "Certificate file /tmp/client.crt specified in in `resque.yml` does not exist.") + end + end + + context 'when specified key file does not exist' do + before do + allow(::File).to receive(:exist?).with("/tmp/client.crt").and_return(true) + allow(::File).to receive(:read).with("/tmp/client.crt").and_return("DUMMY_CERTIFICATE") + allow(OpenSSL::X509::Certificate).to receive(:new).with("DUMMY_CERTIFICATE").and_return(dummy_certificate) + allow(::File).to receive(:exist?).with("/tmp/client.key").and_return(false) + end + + it 'raises error about missing key file' do + expect do + subject.send(:parse_client_tls_options, + resque_yaml_config_with_tls) + end.to raise_error(Gitlab::Redis::Wrapper::InvalidPathError, + "Key file /tmp/client.key specified in in `resque.yml` does not exist.") + end + end + + context 'when configuration valid TLS related options' do + before do + allow(::File).to receive(:exist?).with("/tmp/client.crt").and_return(true) + allow(::File).to receive(:exist?).with("/tmp/client.key").and_return(true) + allow(::File).to receive(:read).with("/tmp/client.crt").and_return("DUMMY_CERTIFICATE") + allow(::File).to receive(:read).with("/tmp/client.key").and_return("DUMMY_KEY") + allow(OpenSSL::X509::Certificate).to receive(:new).with("DUMMY_CERTIFICATE").and_return(dummy_certificate) + allow(OpenSSL::PKey).to receive(:read).with("DUMMY_KEY").and_return(dummy_key) + end + + it "converts cert_file and key_file appropriately" do + expect(subject.send(:parse_client_tls_options, resque_yaml_config_with_tls)).to eq(parsed_config_with_tls) + end + end + end + describe '#fetch_config' do before do FileUtils.mkdir_p(File.join(rails_root, 'config')) diff --git a/spec/support/shared_examples/requests/access_tokens_controller_shared_examples.rb b/spec/support/shared_examples/requests/access_tokens_controller_shared_examples.rb index 74dbec063e0..625f16824b4 100644 --- a/spec/support/shared_examples/requests/access_tokens_controller_shared_examples.rb +++ b/spec/support/shared_examples/requests/access_tokens_controller_shared_examples.rb @@ -72,7 +72,7 @@ RSpec.shared_examples 'GET access tokens are paginated and ordered' do first_token = assigns(:active_access_tokens).first.as_json expect(first_token['name']).to eq("Token1") - expect(first_token['expires_at']).to eq(expires_1_day_from_now.strftime("%Y-%m-%d")) + expect(first_token['expires_at']).to eq(expires_1_day_from_now.iso8601) end it "orders tokens on id in case token has same expires_at" do @@ -82,11 +82,11 @@ RSpec.shared_examples 'GET access tokens are paginated and ordered' do first_token = assigns(:active_access_tokens).first.as_json expect(first_token['name']).to eq("Token3") - expect(first_token['expires_at']).to eq(expires_1_day_from_now.strftime("%Y-%m-%d")) + expect(first_token['expires_at']).to eq(expires_1_day_from_now.iso8601) second_token = assigns(:active_access_tokens).second.as_json expect(second_token['name']).to eq("Token1") - expect(second_token['expires_at']).to eq(expires_1_day_from_now.strftime("%Y-%m-%d")) + expect(second_token['expires_at']).to eq(expires_1_day_from_now.iso8601) end end diff --git a/spec/support/shared_examples/requests/api/graphql/issue_list_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/issue_list_shared_examples.rb index 6eceb7c350d..04f340fef37 100644 --- a/spec/support/shared_examples/requests/api/graphql/issue_list_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/graphql/issue_list_shared_examples.rb @@ -518,6 +518,39 @@ RSpec.shared_examples 'graphql issue list request spec' do end end + context 'when fetching external participants' do + before_all do + issue_a.update!(external_author: 'user@example.com') + end + + let(:fields) do + <<~QUERY + nodes { + id + externalAuthor + } + QUERY + end + + it 'returns the email address' do + post_query + + emails = issues_data.pluck('externalAuthor').compact + expect(emails).to contain_exactly('user@example.com') + end + + context 'when user does not have access to view emails' do + let(:current_user) { external_user } + + it 'obfuscates the email address' do + post_query + + emails = issues_data.pluck('externalAuthor').compact + expect(emails).to contain_exactly("us*****@e*****.c**") + end + end + end + context 'when fetching escalation status' do let_it_be(:escalation_status) { create(:incident_management_issuable_escalation_status, issue: issue_a) } let_it_be(:incident_type) { WorkItems::Type.default_by_type(:incident) } diff --git a/spec/support/shared_examples/requests/api/graphql/work_item_list_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/work_item_list_shared_examples.rb new file mode 100644 index 00000000000..a9c422c8f2d --- /dev/null +++ b/spec/support/shared_examples/requests/api/graphql/work_item_list_shared_examples.rb @@ -0,0 +1,98 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'graphql work item list request spec' do + let(:work_item_ids) { graphql_dig_at(work_item_data, :id) } + + it_behaves_like 'a working graphql query' do + before do + post_query + end + end + + describe 'filters' do + before do + post_query + end + + context 'when filtering by author username' do + let(:author) { create(:author) } + let(:authored_work_item) { create(:work_item, author: author, **container_build_params) } + + let(:item_filter_params) { { author_username: authored_work_item.author.username } } + + it 'returns correct results' do + expect(work_item_ids).to contain_exactly(authored_work_item.to_global_id.to_s) + end + end + + context 'when filtering by state' do + let_it_be(:opened_work_item) { create(:work_item, :opened, **container_build_params) } + let_it_be(:closed_work_item) { create(:work_item, :closed, **container_build_params) } + + context 'when filtering by state opened' do + let(:item_filter_params) { { state: :opened } } + + it 'filters by state' do + expect(work_item_ids).to include(opened_work_item.to_global_id.to_s) + expect(work_item_ids).not_to include(closed_work_item.to_global_id.to_s) + end + end + + context 'when filtering by state closed' do + let(:item_filter_params) { { state: :closed } } + + it 'filters by state' do + expect(work_item_ids).not_to include(opened_work_item.to_global_id.to_s) + expect(work_item_ids).to include(closed_work_item.to_global_id.to_s) + end + end + end + + context 'when filtering by type' do + let_it_be(:issue_work_item) { create(:work_item, :issue, **container_build_params) } + let_it_be(:task_work_item) { create(:work_item, :task, **container_build_params) } + + context 'when filtering by issue type' do + let(:item_filter_params) { { types: [:ISSUE] } } + + it 'filters by type' do + expect(work_item_ids).to include(issue_work_item.to_global_id.to_s) + expect(work_item_ids).not_to include(task_work_item.to_global_id.to_s) + end + end + + context 'when filtering by task type' do + let(:item_filter_params) { { types: [:TASK] } } + + it 'filters by type' do + expect(work_item_ids).not_to include(issue_work_item.to_global_id.to_s) + expect(work_item_ids).to include(task_work_item.to_global_id.to_s) + end + end + end + + context 'when filtering by iid' do + let_it_be(:work_item_by_iid) { create(:work_item, **container_build_params) } + + context 'when using the iid filter' do + let(:item_filter_params) { { iid: work_item_by_iid.iid.to_s } } + + it 'returns only items by the given iid' do + expect(work_item_ids).to contain_exactly(work_item_by_iid.to_global_id.to_s) + end + end + + context 'when using the iids filter' do + let(:item_filter_params) { { iids: [work_item_by_iid.iid.to_s] } } + + it 'returns only items by the given iid' do + expect(work_item_ids).to contain_exactly(work_item_by_iid.to_global_id.to_s) + end + end + end + end + + def work_item_data + graphql_data.dig(*work_item_node_path) + end +end diff --git a/spec/support/shared_examples/requests/api/ml/mlflow/mlflow_shared_examples.rb b/spec/support/shared_examples/requests/api/ml/mlflow/mlflow_shared_examples.rb index f2c38d70508..00e50b07909 100644 --- a/spec/support/shared_examples/requests/api/ml/mlflow/mlflow_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/ml/mlflow/mlflow_shared_examples.rb @@ -8,12 +8,25 @@ RSpec.shared_examples 'MLflow|Not Found - Resource Does Not Exist' do end end -RSpec.shared_examples 'MLflow|Requires api scope' do +RSpec.shared_examples 'MLflow|Requires api scope and write permission' do context 'when user has access but token has wrong scope' do let(:access_token) { tokens[:read] } it { is_expected.to have_gitlab_http_status(:forbidden) } end + + context 'when user has access but is not allowed to write' do + before do + allow(Ability).to receive(:allowed?).and_call_original + allow(Ability).to receive(:allowed?) + .with(current_user, :write_model_experiments, project) + .and_return(false) + end + + it "is Unauthorized" do + is_expected.to have_gitlab_http_status(:unauthorized) + end + end end RSpec.shared_examples 'MLflow|Requires read_api scope' do 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 6b6bf375827..5f043cdd996 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 @@ -280,7 +280,10 @@ RSpec.shared_examples 'handling get metadata requests' do |scope: :project| end end - status = :not_found if scope == :group && params[:package_name_type] == :non_existing && !params[:request_forward] + if (scope == :group && params[:package_name_type] == :non_existing) && + (!params[:request_forward] || (!params[:auth] && params[:request_forward] && params[:visibility] != :public)) + status = :not_found + end # Check the error message for :not_found example_name = 'returning response status with error' if status == :not_found @@ -873,3 +876,67 @@ RSpec.shared_examples 'rejects invalid package names' do expect(Gitlab::Json.parse(response.body)).to eq({ 'error' => 'package_name should be a valid file path' }) end end + +RSpec.shared_examples 'handling get metadata requests for packages in multiple projects' do + let_it_be(:project2) { create(:project, namespace: namespace) } + let_it_be(:package2) do + create(:npm_package, + project: project2, + name: "@#{group.path}/scoped_package", + version: '1.2.0') + end + + let(:headers) { build_token_auth_header(personal_access_token.token) } + + subject { get(url, headers: headers) } + + before_all do + project.update!(visibility: 'private') + + group.add_guest(user) + project.add_reporter(user) + project2.add_reporter(user) + end + + it 'includes all matching package versions in the response' do + subject + + expect(json_response['versions'].keys).to match_array([package.version, package2.version]) + end + + context 'with the feature flag disabled' do + before do + stub_feature_flags(npm_allow_packages_in_multiple_projects: false) + end + + it 'returns matching package versions from only one project' do + subject + + expect(json_response['versions'].keys).to match_array([package2.version]) + end + end + + context 'with limited access to the project with the last package version' do + before_all do + project2.add_guest(user) + end + + it 'includes matching package versions from authorized projects in the response' do + subject + + expect(json_response['versions'].keys).to contain_exactly(package.version) + end + end + + context 'with limited access to the project with the first package version' do + before do + project.add_guest(user) + end + + it 'includes matching package versions from authorized projects in the response' do + subject + + expect(json_response['versions'].keys).to contain_exactly(package2.version) + end + end +end diff --git a/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb index 2e66bae26ba..1be99040ae5 100644 --- a/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb @@ -373,14 +373,6 @@ RSpec.shared_examples 'process nuget download content request' do |user_type, st end it_behaves_like 'bumping the package last downloaded at field' - - context 'when nuget_normalized_version feature flag is disabled' do - before do - stub_feature_flags(nuget_normalized_version: false) - end - - it_behaves_like 'returning response status', :not_found - end end end end @@ -710,4 +702,38 @@ RSpec.shared_examples 'nuget upload endpoint' do |symbol_package: false| it_behaves_like 'returning response status', :forbidden end + + context 'when package duplicates are not allowed' do + let(:headers) { basic_auth_header(deploy_token.username, deploy_token.token).merge(workhorse_headers) } + let_it_be(:existing_package) { create(:nuget_package, project: project) } + let_it_be(:metadata) { { package_name: existing_package.name, package_version: existing_package.version } } + let_it_be(:package_settings) do + create(:namespace_package_setting, :group, namespace: project.namespace, nuget_duplicates_allowed: false) + end + + before do + allow_next_instance_of(::Packages::Nuget::MetadataExtractionService) do |instance| + allow(instance).to receive(:execute).and_return(ServiceResponse.success(payload: metadata)) + end + end + + it_behaves_like 'returning response status', :conflict unless symbol_package + it_behaves_like 'returning response status', :created if symbol_package + + context 'when exception_regex is set' do + before do + package_settings.update_column(:nuget_duplicate_exception_regex, ".*#{existing_package.name.last(3)}.*") + end + + it_behaves_like 'returning response status', :created + end + + context 'when nuget_duplicates_option feature flag is disabled' do + before do + stub_feature_flags(nuget_duplicates_option: false) + end + + it_behaves_like 'returning response status', :created + end + end end diff --git a/spec/support/shared_examples/requests/api_keyset_pagination_shared_examples.rb b/spec/support/shared_examples/requests/api_keyset_pagination_shared_examples.rb new file mode 100644 index 00000000000..85db36013dd --- /dev/null +++ b/spec/support/shared_examples/requests/api_keyset_pagination_shared_examples.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'an endpoint with keyset pagination' do |invalid_order: 'name', invalid_sort: 'asc'| + include KeysetPaginationHelpers + + let(:keyset_params) { { pagination: 'keyset', per_page: 1 } } + let(:additional_params) { {} } + + subject do + get api_call, params: keyset_params.merge(additional_params) + response + end + + context 'on making requests with supported ordering structure' do + it 'includes keyset url params in the url response' do + is_expected.to have_gitlab_http_status(:ok) + is_expected.to include_keyset_url_params + end + + it 'does not include pagination headers' do + is_expected.to have_gitlab_http_status(:ok) + is_expected.not_to include_pagination_headers + end + + it 'paginates the records correctly', :aggregate_failures do + is_expected.to have_gitlab_http_status(:ok) + records = json_response + expect(records.size).to eq(1) + expect(records.first['id']).to eq(first_record.id) + + get api_call, params: pagination_params_from_next_url(response) + + expect(response).to have_gitlab_http_status(:ok) + records = Gitlab::Json.parse(response.body) + expect(records.size).to eq(1) + expect(records.first['id']).to eq(second_record.id) + end + end + + context 'on making requests with unsupported ordering structure' do + let(:additional_params) { { order_by: invalid_order, sort: invalid_sort } } + + if invalid_order + it 'returns error', :aggregate_failures do + is_expected.to have_gitlab_http_status(:method_not_allowed) + expect(json_response['error']).to eq('Keyset pagination is not yet available for this type of request') + end + end + end +end diff --git a/spec/support/shared_examples/requests/rack_attack_shared_examples.rb b/spec/support/shared_examples/requests/rack_attack_shared_examples.rb index dafa324b3c6..48d3e438322 100644 --- a/spec/support/shared_examples/requests/rack_attack_shared_examples.rb +++ b/spec/support/shared_examples/requests/rack_attack_shared_examples.rb @@ -148,6 +148,12 @@ RSpec.shared_examples 'rate-limited token requests' do expect(response).not_to have_gitlab_http_status(:too_many_requests) end + matched = throttle_types[throttle_setting_prefix] + + if request_method == 'GET' && throttle_setting_prefix == 'throttle_protected_paths' + matched = 'throttle_authenticated_get_protected_paths_api' + end + arguments = a_hash_including({ message: 'Rack_Attack', status: 429, @@ -155,7 +161,7 @@ RSpec.shared_examples 'rate-limited token requests' do remote_ip: '127.0.0.1', request_method: request_method, path: request_args.first, - matched: throttle_types[throttle_setting_prefix] + matched: matched }.merge(log_data)) expect(Gitlab::AuthLogger).to receive(:error).with(arguments).once @@ -166,7 +172,14 @@ RSpec.shared_examples 'rate-limited token requests' do end it_behaves_like 'tracking when dry-run mode is set' do - let(:throttle_name) { throttle_types[throttle_setting_prefix] } + let(:throttle_name) do + name = throttle_types[throttle_setting_prefix] + if request_method == 'GET' && throttle_setting_prefix == 'throttle_protected_paths' + name = 'throttle_authenticated_get_protected_paths_api' + end + + name + end def do_request make_request(request_args) @@ -315,7 +328,13 @@ RSpec.shared_examples 'rate-limited web authenticated requests' do expect(response).not_to have_gitlab_http_status(:too_many_requests) end - arguments = a_hash_including({ + matched = throttle_types[throttle_setting_prefix] + + if request_method == 'GET' && throttle_setting_prefix == 'throttle_protected_paths' + matched = 'throttle_authenticated_get_protected_paths_web' + end + + arguments = a_hash_including( message: 'Rack_Attack', status: 429, env: :throttle, @@ -324,15 +343,22 @@ RSpec.shared_examples 'rate-limited web authenticated requests' do path: url_that_requires_authentication, user_id: user.id, 'meta.user' => user.username, - matched: throttle_types[throttle_setting_prefix] - }) + matched: matched + ) expect(Gitlab::AuthLogger).to receive(:error).with(arguments).once expect { request_authenticated_web_url }.not_to exceed_query_limit(control_count) end it_behaves_like 'tracking when dry-run mode is set' do - let(:throttle_name) { throttle_types[throttle_setting_prefix] } + let(:throttle_name) do + name = throttle_types[throttle_setting_prefix] + if request_method == 'GET' && throttle_setting_prefix == 'throttle_protected_paths' + name = 'throttle_authenticated_get_protected_paths_web' + end + + name + end def do_request request_authenticated_web_url diff --git a/spec/support/shared_examples/services/incident_shared_examples.rb b/spec/support/shared_examples/services/incident_shared_examples.rb index db2b448f567..94467ad53fa 100644 --- a/spec/support/shared_examples/services/incident_shared_examples.rb +++ b/spec/support/shared_examples/services/incident_shared_examples.rb @@ -40,7 +40,7 @@ end RSpec.shared_examples 'incident management label service' do let_it_be(:project) { create(:project, :private) } - let_it_be(:user) { User.alert_bot } + let_it_be(:user) { Users::Internal.alert_bot } let(:service) { described_class.new(project, user) } subject(:execute) { service.execute } diff --git a/spec/support/shared_examples/services/issuable/issuable_update_service_shared_examples.rb b/spec/support/shared_examples/services/issuable/issuable_update_service_shared_examples.rb index 3f95d6060ea..9624f7a4450 100644 --- a/spec/support/shared_examples/services/issuable/issuable_update_service_shared_examples.rb +++ b/spec/support/shared_examples/services/issuable/issuable_update_service_shared_examples.rb @@ -145,6 +145,77 @@ RSpec.shared_examples 'updating issuable labels' do end end +RSpec.shared_examples 'updating merged MR with locked labels' do + context 'when add_label_ids and label_ids are passed' do + let(:params) { { label_ids: [label_a.id], add_label_ids: [label_c.id] } } + + it 'replaces unlocked labels with the ones in label_ids and adds those in add_label_ids' do + issuable.update!(labels: [label_b, label_unlocked]) + update_issuable(params) + + expect(issuable.label_ids).to contain_exactly(label_a.id, label_b.id, label_c.id) + end + end + + context 'when remove_label_ids and label_ids are passed' do + let(:params) { { label_ids: [label_a.id, label_b.id, label_c.id], remove_label_ids: [label_a.id] } } + + it 'replaces unlocked labels with the ones in label_ids and does not remove locked label in remove_label_ids' do + issuable.update!(labels: [label_a, label_c, label_unlocked]) + update_issuable(params) + + expect(issuable.label_ids).to contain_exactly(label_a.id, label_b.id, label_c.id) + end + end + + context 'when add_label_ids and remove_label_ids are passed' do + let(:params) { { add_label_ids: [label_c.id], remove_label_ids: [label_a.id, label_unlocked.id] } } + + before do + issuable.update!(labels: [label_a, label_unlocked]) + update_issuable(params) + end + + it 'adds the passed labels' do + expect(issuable.label_ids).to include(label_c.id) + end + + it 'removes the passed unlocked labels' do + expect(issuable.label_ids).to include(label_a.id) + expect(issuable.label_ids).not_to include(label_unlocked.id) + end + end + + context 'when same id is passed as add_label_ids and remove_label_ids' do + let(:params) { { add_label_ids: [label_a.id], remove_label_ids: [label_a.id] } } + + context 'for a label assigned to an issue' do + it 'does not remove the label' do + issuable.update!(labels: [label_a]) + update_issuable(params) + + expect(issuable.label_ids).to contain_exactly(label_a.id) + end + end + + context 'for a label not assigned to an issue' do + it 'does not add the label' do + expect(issuable.label_ids).to be_empty + end + end + end + + context 'when duplicate label titles are given' do + let(:params) { { labels: [label_c.title, label_c.title] } } + + it 'assigns the label once' do + update_issuable(params) + + expect(issuable.labels).to contain_exactly(label_c) + end + end +end + RSpec.shared_examples 'keeps issuable labels sorted after update' do before do update_issuable(label_ids: [label_b.id]) diff --git a/spec/support/shared_examples/services/issuable_links/destroyable_issuable_links_shared_examples.rb b/spec/support/shared_examples/services/issuable_links/destroyable_issuable_links_shared_examples.rb index 1532e870dcc..b955b71a6bb 100644 --- a/spec/support/shared_examples/services/issuable_links/destroyable_issuable_links_shared_examples.rb +++ b/spec/support/shared_examples/services/issuable_links/destroyable_issuable_links_shared_examples.rb @@ -1,13 +1,7 @@ # frozen_string_literal: true -RSpec.shared_examples 'a destroyable issuable link' do |required_role: :reporter| +RSpec.shared_examples 'a destroyable issuable link' do context 'when successfully removes an issuable link' do - before do - [issuable_link.target, issuable_link.source].each do |issuable| - issuable.resource_parent.try(:"add_#{required_role}", user) - end - end - it 'removes related issue' do expect { subject }.to change { issuable_link.class.count }.by(-1) end @@ -28,6 +22,9 @@ RSpec.shared_examples 'a destroyable issuable link' do |required_role: :reporter end context 'when failing to remove an issuable link' do + let_it_be(:non_member) { create(:user) } + let(:user) { non_member } + it 'does not remove relation' do expect { subject }.not_to change { issuable_link.class.count }.from(1) end diff --git a/spec/support/shared_examples/services/metrics/dashboard_shared_examples.rb b/spec/support/shared_examples/services/metrics/dashboard_shared_examples.rb deleted file mode 100644 index 9b2e038a331..00000000000 --- a/spec/support/shared_examples/services/metrics/dashboard_shared_examples.rb +++ /dev/null @@ -1,193 +0,0 @@ -# frozen_string_literal: true - -RSpec.shared_examples 'misconfigured dashboard service response' do |status_code, message = nil| - it 'returns an appropriate message and status code', :aggregate_failures do - result = service_call - - expect(result.keys).to contain_exactly(:message, :http_status, :status) - expect(result[:status]).to eq(:error) - expect(result[:http_status]).to eq(status_code) - expect(result[:message]).to eq(message) if message - end -end - -RSpec.shared_examples 'valid dashboard service response for schema' do - it 'returns a json representation of the dashboard' do - result = service_call - - expect(result.keys).to contain_exactly(:dashboard, :status) - expect(result[:status]).to eq(:success) - - schema_path = Rails.root.join('spec/fixtures', dashboard_schema) - validator = JSONSchemer.schema(schema_path) - expect(validator.valid?(result[:dashboard].with_indifferent_access)).to be true - end -end - -RSpec.shared_examples 'valid dashboard service response' do - let(:dashboard_schema) { 'lib/gitlab/metrics/dashboard/schemas/dashboard.json' } - - it_behaves_like 'valid dashboard service response for schema' -end - -RSpec.shared_examples 'caches the unprocessed dashboard for subsequent calls' do - specify do - expect_next_instance_of(::Gitlab::Config::Loader::Yaml) do |loader| - expect(loader).to receive(:load_raw!).once.and_call_original - end - - described_class.new(*service_params).get_dashboard - described_class.new(*service_params).get_dashboard - end -end - -# This spec is applicable for predefined/out-of-the-box dashboard services. -RSpec.shared_examples 'refreshes cache when dashboard_version is changed' do - specify do - allow_next_instance_of(described_class) do |service| - allow(service).to receive(:dashboard_version).and_return('1', '2') - end - - expect_file_read(Rails.root.join(described_class::DASHBOARD_PATH)).twice.and_call_original - - service = described_class.new(*service_params) - - service.get_dashboard - service.get_dashboard - end -end - -# This spec is applicable for predefined/out-of-the-box dashboard services. -# This shared_example requires the following variables to be defined: -# dashboard_path: Relative path to the dashboard, ex: 'config/prometheus/common_metrics.yml' -# dashboard_version: The version string used in the cache_key. -RSpec.shared_examples 'dashboard_version contains SHA256 hash of dashboard file content' do - specify do - dashboard = File.read(Rails.root.join(dashboard_path)) - expect(dashboard_version).to eq(Digest::SHA256.hexdigest(dashboard)) - end -end - -RSpec.shared_examples 'valid embedded dashboard service response' do - let(:dashboard_schema) { 'lib/gitlab/metrics/dashboard/schemas/embedded_dashboard.json' } - - it_behaves_like 'valid dashboard service response for schema' -end - -RSpec.shared_examples 'raises error for users with insufficient permissions' do - context 'when the user does not have sufficient access' do - let(:user) { build(:user) } - - it_behaves_like 'misconfigured dashboard service response', :unauthorized - end - - context 'when the user is anonymous' do - let(:user) { nil } - - it_behaves_like 'misconfigured dashboard service response', :unauthorized - end -end - -RSpec.shared_examples 'valid dashboard cloning process' do |dashboard_template, sequence| - context "dashboard template: #{dashboard_template}" do - let(:dashboard) { dashboard_template } - let(:dashboard_attrs) do - { - commit_message: commit_message, - branch_name: branch, - start_branch: project.default_branch, - encoding: 'text', - file_path: ".gitlab/dashboards/#{file_name}", - file_content: file_content_hash.to_yaml - } - end - - it 'delegates commit creation to Files::CreateService', :aggregate_failures do - service_instance = instance_double(::Files::CreateService) - allow(::Gitlab::Metrics::Dashboard::Processor).to receive(:new).and_return(double(process: file_content_hash)) - expect(::Files::CreateService).to receive(:new).with(project, user, dashboard_attrs).and_return(service_instance) - expect(service_instance).to receive(:execute).and_return(status: :success) - - service_call - end - - context 'user has defined custom metrics' do - it 'uses external service to includes them into new file content', :aggregate_failures do - service_instance = double(::Gitlab::Metrics::Dashboard::Processor) - expect(::Gitlab::Metrics::Dashboard::Processor).to receive(:new).with(project, file_content_hash, sequence, {}).and_return(service_instance) - expect(service_instance).to receive(:process).and_return(file_content_hash) - expect(::Files::CreateService).to receive(:new).with(project, user, dashboard_attrs).and_return(double(execute: { status: :success })) - - service_call - end - end - end -end - -RSpec.shared_examples 'misconfigured dashboard service response with stepable' do |status_code, message = nil| - it 'returns an appropriate message and status code', :aggregate_failures do - result = service_call - - expect(result.keys).to contain_exactly(:message, :http_status, :status, :last_step) - expect(result[:status]).to eq(:error) - expect(result[:http_status]).to eq(status_code) - expect(result[:message]).to eq(message) if message - end -end - -RSpec.shared_examples 'updates gitlab_metrics_dashboard_processing_time_ms metric' do - specify :prometheus do - service_call - metric = subject.send(:processing_time_metric) - labels = subject.send(:processing_time_metric_labels) - - expect(metric.get(labels)).to be > 0 - end -end - -RSpec.shared_examples '#raw_dashboard raises error if dashboard loading fails' do - context 'when yaml is too large' do - before do - allow_next_instance_of(::Gitlab::Config::Loader::Yaml) do |loader| - allow(loader).to receive(:load_raw!) - .and_raise(Gitlab::Config::Loader::Yaml::DataTooLargeError, 'The parsed YAML is too big') - end - end - - it 'raises error' do - expect { subject.raw_dashboard }.to raise_error( - Gitlab::Metrics::Dashboard::Errors::LayoutError, - 'The parsed YAML is too big' - ) - end - end - - context 'when yaml loader returns error' do - before do - allow_next_instance_of(::Gitlab::Config::Loader::Yaml) do |loader| - allow(loader).to receive(:load_raw!) - .and_raise(Gitlab::Config::Loader::FormatError, 'Invalid configuration format') - end - end - - it 'raises error' do - expect { subject.raw_dashboard }.to raise_error( - Gitlab::Metrics::Dashboard::Errors::LayoutError, - 'Invalid yaml' - ) - end - end - - context 'when yaml is not a hash' do - before do - allow_next_instance_of(::Gitlab::Config::Loader::Yaml) do |loader| - allow(loader).to receive(:load_raw!) - .and_raise(Gitlab::Config::Loader::Yaml::NotHashError, 'Invalid configuration format') - end - end - - it 'returns nil' do - expect(subject.raw_dashboard).to eq({}) - end - end -end diff --git a/spec/support/shared_examples/services/migrate_to_ghost_user_service_shared_examples.rb b/spec/support/shared_examples/services/migrate_to_ghost_user_service_shared_examples.rb index e77d73d1c72..621fb99afe5 100644 --- a/spec/support/shared_examples/services/migrate_to_ghost_user_service_shared_examples.rb +++ b/spec/support/shared_examples/services/migrate_to_ghost_user_service_shared_examples.rb @@ -38,7 +38,7 @@ RSpec.shared_examples "migrating a deleted user's associated records to the ghos migrated_record = record_class.find_by_id(record.id) migrated_fields.each do |field| - expect(migrated_record.public_send(field)).to eq(User.ghost) + expect(migrated_record.public_send(field)).to eq(Users::Internal.ghost) end end @@ -47,7 +47,7 @@ RSpec.shared_examples "migrating a deleted user's associated records to the ghos migrated_record = record_class.find_by_id(record.id) - check_user = always_ghost ? User.ghost : user + check_user = always_ghost ? Users::Internal.ghost : user migrated_fields.each do |field| expect(migrated_record.public_send(field)).to eq(check_user) diff --git a/spec/support/shared_examples/services/pages_size_limit_shared_examples.rb b/spec/support/shared_examples/services/pages_size_limit_shared_examples.rb deleted file mode 100644 index d9e906ebb75..00000000000 --- a/spec/support/shared_examples/services/pages_size_limit_shared_examples.rb +++ /dev/null @@ -1,32 +0,0 @@ -# frozen_string_literal: true - -RSpec.shared_examples 'pages size limit is' do |size_limit| - context "when size is below the limit" do - before do - allow(metadata).to receive(:total_size).and_return(size_limit - 1.megabyte) - allow(metadata).to receive(:entries).and_return([]) - end - - it 'updates pages correctly' do - subject.execute - - expect(deploy_status.description).not_to be_present - expect(project.pages_metadatum).to be_deployed - end - end - - context "when size is above the limit" do - before do - allow(metadata).to receive(:total_size).and_return(size_limit + 1.megabyte) - allow(metadata).to receive(:entries).and_return([]) - end - - it 'limits the maximum size of gitlab pages' do - subject.execute - - expect(deploy_status.description) - .to match(/artifacts for pages are too large/) - expect(deploy_status).to be_script_failure - end - end -end diff --git a/spec/support/shared_examples/services/protected_branches_shared_examples.rb b/spec/support/shared_examples/services/protected_branches_shared_examples.rb new file mode 100644 index 00000000000..ce607a6b956 --- /dev/null +++ b/spec/support/shared_examples/services/protected_branches_shared_examples.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +RSpec.shared_context 'with scan result policy blocking protected branches' do + before do + create( + :scan_result_policy_read, + :blocking_protected_branches, + project: project) + + stub_licensed_features(security_orchestration_policies: true) + end +end diff --git a/spec/support/shared_examples/services/users/build_service_shared_examples.rb b/spec/support/shared_examples/services/users/build_service_shared_examples.rb index e448f2f874b..45ebc837e6d 100644 --- a/spec/support/shared_examples/services/users/build_service_shared_examples.rb +++ b/spec/support/shared_examples/services/users/build_service_shared_examples.rb @@ -33,57 +33,6 @@ RSpec.shared_examples 'common user build items' do end RSpec.shared_examples_for 'current user not admin build items' do - using RSpec::Parameterized::TableSyntax - - context 'with "user_default_external" application setting' do - where(:user_default_external, :external, :email, :user_default_internal_regex, :result) do - true | nil | 'fl@example.com' | nil | true - true | true | 'fl@example.com' | nil | true - true | false | 'fl@example.com' | nil | true # admin difference - - true | nil | 'fl@example.com' | '' | true - true | true | 'fl@example.com' | '' | true - true | false | 'fl@example.com' | '' | true # admin difference - - true | nil | 'fl@example.com' | '^(?:(?!\.ext@).)*$\r?' | false - true | true | 'fl@example.com' | '^(?:(?!\.ext@).)*$\r?' | false # admin difference - true | false | 'fl@example.com' | '^(?:(?!\.ext@).)*$\r?' | false - - true | nil | 'tester.ext@domain.com' | '^(?:(?!\.ext@).)*$\r?' | true - true | true | 'tester.ext@domain.com' | '^(?:(?!\.ext@).)*$\r?' | true - true | false | 'tester.ext@domain.com' | '^(?:(?!\.ext@).)*$\r?' | true # admin difference - - false | nil | 'fl@example.com' | nil | false - false | true | 'fl@example.com' | nil | false # admin difference - false | false | 'fl@example.com' | nil | false - - false | nil | 'fl@example.com' | '' | false - false | true | 'fl@example.com' | '' | false # admin difference - false | false | 'fl@example.com' | '' | false - - false | nil | 'fl@example.com' | '^(?:(?!\.ext@).)*$\r?' | false - false | true | 'fl@example.com' | '^(?:(?!\.ext@).)*$\r?' | false # admin difference - false | false | 'fl@example.com' | '^(?:(?!\.ext@).)*$\r?' | false - - false | nil | 'tester.ext@domain.com' | '^(?:(?!\.ext@).)*$\r?' | false - false | true | 'tester.ext@domain.com' | '^(?:(?!\.ext@).)*$\r?' | false # admin difference - false | false | 'tester.ext@domain.com' | '^(?:(?!\.ext@).)*$\r?' | false - end - - with_them do - before do - stub_application_setting(user_default_external: user_default_external) - stub_application_setting(user_default_internal_regex: user_default_internal_regex) - - params.merge!({ external: external, email: email }.compact) - end - - it 'sets the value of Gitlab::CurrentSettings.user_default_external' do - expect(user.external).to eq(result) - end - end - end - context 'when "email_confirmation_setting" application setting is set to `hard`' do before do stub_application_setting_enum('email_confirmation_setting', 'hard') diff --git a/spec/support/shared_examples/users/migrate_records_to_ghost_user_service_shared_examples.rb b/spec/support/shared_examples/users/migrate_records_to_ghost_user_service_shared_examples.rb index eb03f0888b9..5d24aa10453 100644 --- a/spec/support/shared_examples/users/migrate_records_to_ghost_user_service_shared_examples.rb +++ b/spec/support/shared_examples/users/migrate_records_to_ghost_user_service_shared_examples.rb @@ -32,7 +32,7 @@ RSpec.shared_examples 'migrating records to the ghost user' do |record_class, fi migrated_record = record_class.find_by_id(record.id) migrated_fields.each do |field| - expect(migrated_record.public_send(field)).to eq(User.ghost) + expect(migrated_record.public_send(field)).to eq(Users::Internal.ghost) end end end diff --git a/spec/support/shared_examples/users/pages_visits_shared_examples.rb b/spec/support/shared_examples/users/pages_visits_shared_examples.rb new file mode 100644 index 00000000000..1a613fa0aed --- /dev/null +++ b/spec/support/shared_examples/users/pages_visits_shared_examples.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'namespace visits tracking worker' do + let_it_be(:base_time) { DateTime.now } + + context 'when params are provided' do + before do + worker.perform(entity_type, entity.id, user.id, base_time) + end + + include_examples 'an idempotent worker' do + let(:job_args) { [entity_type, entity.id, user.id, base_time] } + + it 'tracks the entity visit' do + latest_visit = model.last + + expect(model.count).to be(1) + expect(latest_visit[:entity_id]).to be(entity.id) + expect(latest_visit.user_id).to be(user.id) + end + end + + context 'when a visit occurs within 15 minutes of a previously tracked one' do + [-15.minutes, 15.minutes].each do |time_diff| + it 'does not track the visit' do + worker.perform(entity_type, entity.id, user.id, base_time + time_diff) + + expect(model.count).to be(1) + end + end + end + + context 'when a visit occurs more than 15 minutes away from a previously tracked one' do + [-16.minutes, 16.minutes].each do |time_diff| + it 'tracks the visit' do + worker.perform(entity_type, entity.id, user.id, base_time + time_diff) + + expect(model.count).to be > 1 + end + end + end + end + + context 'when user is missing' do + before do + worker.perform(entity_type, entity.id, nil, base_time) + end + + it 'does not do anything' do + expect(model.count).to be(0) + end + end + + context 'when entity is missing' do + before do + worker.perform(entity_type, nil, user.id, base_time) + end + + it 'does not do anything' do + expect(model.count).to be(0) + end + end +end diff --git a/spec/support/shared_examples/workers/background_migration_worker_shared_examples.rb b/spec/support/shared_examples/workers/background_migration_worker_shared_examples.rb index 8ecb04bfdd6..1ea5eb6fd2e 100644 --- a/spec/support/shared_examples/workers/background_migration_worker_shared_examples.rb +++ b/spec/support/shared_examples/workers/background_migration_worker_shared_examples.rb @@ -1,6 +1,10 @@ # frozen_string_literal: true RSpec.shared_examples 'it runs background migration jobs' do |tracking_database| + before do + stub_feature_flags(disallow_database_ddl_feature_flags: false) + end + describe 'defining the job attributes' do it 'defines the data_consistency as always' do expect(described_class.get_data_consistency).to eq(:always) @@ -74,6 +78,38 @@ RSpec.shared_examples 'it runs background migration jobs' do |tracking_database| end end + context 'when disallow_database_ddl_feature_flags feature flag is disabled' do + before do + stub_feature_flags(disallow_database_ddl_feature_flags: true) + end + + it 'does not perform the job, reschedules it in the future, and logs a message' do + expect(worker).not_to receive(:perform_with_connection) + + expect(Sidekiq.logger).to receive(:info) do |payload| + expect(payload[:class]).to eq(described_class.name) + expect(payload[:database]).to eq(tracking_database) + expect(payload[:message]).to match(/skipping execution, migration rescheduled/) + end + + lease_attempts = 3 + delay = described_class::BACKGROUND_MIGRATIONS_DELAY + job_args = [10, 20] + + freeze_time do + worker.perform('Foo', job_args, lease_attempts) + + job = described_class.jobs.find { |job| job['args'] == ['Foo', job_args, lease_attempts] } + expect(job).to be, "Expected the job to be rescheduled with (#{job_args}, #{lease_attempts}), but it was not." + + expected_time = delay.to_i + Time.now.to_i + expect(job['at']).to eq(expected_time), + "Expected the job to be rescheduled in #{expected_time} seconds, " \ + "but it was rescheduled in #{job['at']} seconds." + end + end + end + context 'when execute_background_migrations feature flag is enabled' do before do stub_feature_flags(execute_background_migrations: true) diff --git a/spec/support/shared_examples/workers/batched_background_migration_execution_worker_shared_example.rb b/spec/support/shared_examples/workers/batched_background_migration_execution_worker_shared_example.rb index 8fdd59d1d8c..cf488a4d753 100644 --- a/spec/support/shared_examples/workers/batched_background_migration_execution_worker_shared_example.rb +++ b/spec/support/shared_examples/workers/batched_background_migration_execution_worker_shared_example.rb @@ -3,6 +3,10 @@ RSpec.shared_examples 'batched background migrations execution worker' do include ExclusiveLeaseHelpers + before do + stub_feature_flags(disallow_database_ddl_feature_flags: false) + end + it 'is a limited capacity worker' do expect(described_class.new).to be_a(LimitedCapacity::Worker) end @@ -100,6 +104,23 @@ RSpec.shared_examples 'batched background migrations execution worker' do end end + context 'when disable ddl flag is enabled' do + let(:migration) do + create(:batched_background_migration, :active, interval: job_interval, table_name: table_name) + end + + before do + stub_feature_flags(disallow_database_ddl_feature_flags: true) + end + + it 'does nothing' do + expect(Gitlab::Database::BackgroundMigration::BatchedMigration).not_to receive(:find_executable) + expect(worker).not_to receive(:run_migration_job) + + worker.perform_work(database_name, migration.id) + end + end + context 'when the feature flag is enabled' do before do stub_feature_flags(execute_batched_migrations_on_schedule: true) diff --git a/spec/support/shared_examples/workers/batched_background_migration_worker_shared_examples.rb b/spec/support/shared_examples/workers/batched_background_migration_worker_shared_examples.rb index e7385f9abb6..003b8d07819 100644 --- a/spec/support/shared_examples/workers/batched_background_migration_worker_shared_examples.rb +++ b/spec/support/shared_examples/workers/batched_background_migration_worker_shared_examples.rb @@ -3,6 +3,10 @@ RSpec.shared_examples 'it runs batched background migration jobs' do |tracking_database, table_name| include ExclusiveLeaseHelpers + before do + stub_feature_flags(disallow_database_ddl_feature_flags: false) + end + describe 'defining the job attributes' do it 'defines the data_consistency as always' do expect(described_class.get_data_consistency).to eq(:always) @@ -51,6 +55,12 @@ RSpec.shared_examples 'it runs batched background migration jobs' do |tracking_d expect(described_class.enabled?).to be_falsey end + + it 'returns false when disallow_database_ddl_feature_flags feature flag is enabled' do + stub_feature_flags(disallow_database_ddl_feature_flags: true) + + expect(described_class.enabled?).to be_falsey + end end describe '#perform' do @@ -116,6 +126,18 @@ RSpec.shared_examples 'it runs batched background migration jobs' do |tracking_d end end + context 'when the disallow_database_ddl_feature_flags feature flag is enabled' do + before do + stub_feature_flags(disallow_database_ddl_feature_flags: true) + end + + it 'does nothing' do + expect(worker).not_to receive(:queue_migrations_for_execution) + + worker.perform + end + end + context 'when the execute_batched_migrations_on_schedule feature flag is enabled' do let(:base_model) { Gitlab::Database.database_base_models[tracking_database] } let(:connection) { base_model.connection } |