diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-03-18 20:02:30 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-03-18 20:02:30 +0000 |
commit | 41fe97390ceddf945f3d967b8fdb3de4c66b7dea (patch) | |
tree | 9c8d89a8624828992f06d892cd2f43818ff5dcc8 /spec/support | |
parent | 0804d2dc31052fb45a1efecedc8e06ce9bc32862 (diff) | |
download | gitlab-ce-7fd8f62e898848bf3d8f058077d7756742ae3bb0.tar.gz |
Add latest changes from gitlab-org/gitlab@14-9-stable-eev14.9.0-rc42
Diffstat (limited to 'spec/support')
78 files changed, 1473 insertions, 294 deletions
diff --git a/spec/support/enable_multiple_database_metrics_by_default.rb b/spec/support/enable_multiple_database_metrics_by_default.rb deleted file mode 100644 index 6eeb4acd3d6..00000000000 --- a/spec/support/enable_multiple_database_metrics_by_default.rb +++ /dev/null @@ -1,8 +0,0 @@ -# frozen_string_literal: true - -RSpec.configure do |config| - config.before do - # Enable this by default in all tests so it behaves like a FF - stub_env('GITLAB_MULTIPLE_DATABASE_METRICS', '1') - end -end diff --git a/spec/support/event_store.rb b/spec/support/event_store.rb new file mode 100644 index 00000000000..057a5550746 --- /dev/null +++ b/spec/support/event_store.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +RSpec.configure do |config| + config.before(:each, :event_store_publisher) do + allow(Gitlab::EventStore).to receive(:publish) + end +end diff --git a/spec/support/helpers/ci/template_helpers.rb b/spec/support/helpers/ci/template_helpers.rb index 7bab58a574e..598a5a0becc 100644 --- a/spec/support/helpers/ci/template_helpers.rb +++ b/spec/support/helpers/ci/template_helpers.rb @@ -3,7 +3,7 @@ module Ci module TemplateHelpers def secure_analyzers_prefix - 'registry.gitlab.com/gitlab-org/security-products/analyzers' + 'registry.gitlab.com/security-products' end end end diff --git a/spec/support/helpers/content_security_policy_helpers.rb b/spec/support/helpers/content_security_policy_helpers.rb new file mode 100644 index 00000000000..c9f15e65c74 --- /dev/null +++ b/spec/support/helpers/content_security_policy_helpers.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module ContentSecurityPolicyHelpers + # Expecting 2 calls to current_content_security_policy by default, once for + # the call that's being tested and once for the call in ApplicationController + def setup_csp_for_controller(controller_class, times = 2) + expect_next_instance_of(controller_class) do |controller| + expect(controller).to receive(:current_content_security_policy) + .and_return(ActionDispatch::ContentSecurityPolicy.new).exactly(times).times + end + end + + # Expecting 2 calls to current_content_security_policy by default, once for + # the call that's being tested and once for the call in ApplicationController + def setup_existing_csp_for_controller(controller_class, csp, times = 2) + expect_next_instance_of(controller_class) do |controller| + expect(controller).to receive(:current_content_security_policy).and_return(csp).exactly(times).times + end + end +end diff --git a/spec/support/helpers/database_connection_helpers.rb b/spec/support/helpers/database_connection_helpers.rb deleted file mode 100644 index 10ea7b5de91..00000000000 --- a/spec/support/helpers/database_connection_helpers.rb +++ /dev/null @@ -1,11 +0,0 @@ -# frozen_string_literal: true - -module DatabaseConnectionHelpers - def run_with_new_database_connection - pool = ActiveRecord::Base.connection_pool - conn = pool.checkout - yield conn - ensure - pool.checkin(conn) - end -end diff --git a/spec/support/helpers/graphql_helpers.rb b/spec/support/helpers/graphql_helpers.rb index 8b7d1c753d5..ff8908e531a 100644 --- a/spec/support/helpers/graphql_helpers.rb +++ b/spec/support/helpers/graphql_helpers.rb @@ -552,6 +552,12 @@ module GraphqlHelpers expect(flattened_errors).to be_empty end + # Helps migrate to the new GraphQL interpreter, + # https://gitlab.com/gitlab-org/gitlab/-/issues/210556 + def expect_graphql_error_to_be_created(error_class, match_message = nil) + expect { yield }.to raise_error(error_class, match_message) + end + def flattened_errors Array.wrap(graphql_errors).flatten.compact end diff --git a/spec/support/helpers/migrations_helpers.rb b/spec/support/helpers/migrations_helpers.rb index 0c5bf09f6b7..afa7ee84bda 100644 --- a/spec/support/helpers/migrations_helpers.rb +++ b/spec/support/helpers/migrations_helpers.rb @@ -13,6 +13,8 @@ module MigrationsHelpers def self.name table_name.singularize.camelcase end + + yield self if block_given? end end @@ -104,9 +106,9 @@ module MigrationsHelpers stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false') end - def previous_migration - migrations.each_cons(2) do |previous, migration| - break previous if migration.name == described_class.name + def previous_migration(steps_back = 2) + migrations.each_cons(steps_back) do |cons| + break cons.first if cons.last.name == described_class.name end end diff --git a/spec/support/helpers/navbar_structure_helper.rb b/spec/support/helpers/navbar_structure_helper.rb index 6fa69cbd6ad..fb06ebfdae2 100644 --- a/spec/support/helpers/navbar_structure_helper.rb +++ b/spec/support/helpers/navbar_structure_helper.rb @@ -77,6 +77,14 @@ module NavbarStructureHelper ) end + def insert_harbor_registry_nav(within) + insert_after_sub_nav_item( + within, + within: _('Packages & Registries'), + new_sub_nav_item_name: _('Harbor Registry') + ) + end + def insert_infrastructure_google_cloud_nav insert_after_sub_nav_item( _('Terraform'), diff --git a/spec/support/helpers/next_found_instance_of.rb b/spec/support/helpers/next_found_instance_of.rb index c8cdbaf2c5d..c7079e64ffd 100644 --- a/spec/support/helpers/next_found_instance_of.rb +++ b/spec/support/helpers/next_found_instance_of.rb @@ -2,19 +2,36 @@ module NextFoundInstanceOf ERROR_MESSAGE = 'NextFoundInstanceOf mock helpers can only be used with ActiveRecord targets' + HELPER_METHOD_PATTERN = /(?:allow|expect)_next_found_(?<number>\d+)_instances_of/.freeze - def expect_next_found_instance_of(klass) + def method_missing(method_name, ...) + return super unless match_data = method_name.match(HELPER_METHOD_PATTERN) + + helper_method = method_name.to_s.sub("_#{match_data[:number]}", '') + + public_send(helper_method, *args, match_data[:number].to_i, &block) + end + + def expect_next_found_instance_of(klass, &block) + expect_next_found_instances_of(klass, nil, &block) + end + + def expect_next_found_instances_of(klass, number) check_if_active_record!(klass) - stub_allocate(expect(klass), klass) do |expectation| + stub_allocate(expect(klass), klass, number) do |expectation| yield(expectation) end end - def allow_next_found_instance_of(klass) + def allow_next_found_instance_of(klass, &block) + allow_next_found_instances_of(klass, nil, &block) + end + + def allow_next_found_instances_of(klass, number) check_if_active_record!(klass) - stub_allocate(allow(klass), klass) do |allowance| + stub_allocate(allow(klass), klass, number) do |allowance| yield(allowance) end end @@ -25,8 +42,11 @@ module NextFoundInstanceOf raise ArgumentError, ERROR_MESSAGE unless klass < ActiveRecord::Base end - def stub_allocate(target, klass) - target.to receive(:allocate).and_wrap_original do |method| + def stub_allocate(target, klass, number) + stub = receive(:allocate) + stub.exactly(number).times if number + + target.to stub.and_wrap_original do |method| method.call.tap do |allocation| # ActiveRecord::Core.allocate returns a frozen object: # https://github.com/rails/rails/blob/291a3d2ef29a3842d1156ada7526f4ee60dd2b59/activerecord/lib/active_record/core.rb#L620 diff --git a/spec/support/helpers/search_helpers.rb b/spec/support/helpers/search_helpers.rb index 3d4ff4801a7..f5a1a97a1d0 100644 --- a/spec/support/helpers/search_helpers.rb +++ b/spec/support/helpers/search_helpers.rb @@ -11,8 +11,12 @@ module SearchHelpers end def submit_search(query) - page.within('.search-form, .search-page-form') do + # Once the `new_header_search` feature flag has been removed + # We can remove the `.search-form` selector + # https://gitlab.com/gitlab-org/gitlab/-/issues/339348 + page.within('.header-search, .search-form, .search-page-form') do field = find_field('search') + field.click field.fill_in(with: query) if javascript_test? diff --git a/spec/support/helpers/sorting_helper.rb b/spec/support/helpers/sorting_helper.rb index f19f8c12928..6ff6dbb7800 100644 --- a/spec/support/helpers/sorting_helper.rb +++ b/spec/support/helpers/sorting_helper.rb @@ -26,6 +26,7 @@ module SortingHelper include Comparable attr_reader :value + delegate :==, :eql?, :hash, to: :value def initialize(value) diff --git a/spec/support/helpers/stub_configuration.rb b/spec/support/helpers/stub_configuration.rb index 8c60dc30cdb..20f46396424 100644 --- a/spec/support/helpers/stub_configuration.rb +++ b/spec/support/helpers/stub_configuration.rb @@ -90,10 +90,18 @@ module StubConfiguration allow(Gitlab.config.repositories).to receive(:storages).and_return(Settingslogic.new(messages)) end - def stub_sentry_settings - allow(Gitlab.config.sentry).to receive(:enabled).and_return(true) - allow(Gitlab.config.sentry).to receive(:dsn).and_return('dummy://b44a0828b72421a6d8e99efd68d44fa8@example.com/42') - allow(Gitlab.config.sentry).to receive(:clientside_dsn).and_return('dummy://b44a0828b72421a6d8e99efd68d44fa8@example.com/43') + def stub_sentry_settings(enabled: true) + allow(Gitlab.config.sentry).to receive(:enabled) { enabled } + allow(Gitlab::CurrentSettings).to receive(:sentry_enabled?) { enabled } + + dsn = 'dummy://b44a0828b72421a6d8e99efd68d44fa8@example.com/42' + allow(Gitlab.config.sentry).to receive(:dsn) { dsn } + allow(Gitlab::CurrentSettings).to receive(:sentry_dsn) { dsn } + + clientside_dsn = 'dummy://b44a0828b72421a6d8e99efd68d44fa8@example.com/43' + allow(Gitlab.config.sentry).to receive(:clientside_dsn) { clientside_dsn } + allow(Gitlab::CurrentSettings) + .to receive(:sentry_clientside_dsn) { clientside_dsn } end def stub_kerberos_setting(messages) diff --git a/spec/support/helpers/terms_helper.rb b/spec/support/helpers/terms_helper.rb index a61bae18f9a..2547ea62e37 100644 --- a/spec/support/helpers/terms_helper.rb +++ b/spec/support/helpers/terms_helper.rb @@ -15,7 +15,9 @@ module TermsHelper end def expect_to_be_on_terms_page - expect(current_path).to eq terms_path + expect(page).to have_current_path terms_path, ignore_query: true expect(page).to have_content('Please accept the Terms of Service before continuing.') end end + +TermsHelper.prepend_mod diff --git a/spec/support/helpers/test_env.rb b/spec/support/helpers/test_env.rb index 18c25f4b770..587d4e22828 100644 --- a/spec/support/helpers/test_env.rb +++ b/spec/support/helpers/test_env.rb @@ -54,7 +54,7 @@ module TestEnv 'wip' => 'b9238ee', 'csv' => '3dd0896', 'v1.1.0' => 'b83d6e3', - 'add-ipython-files' => '532c837', + 'add-ipython-files' => 'a867a602', 'add-pdf-file' => 'e774ebd', 'squash-large-files' => '54cec52', 'add-pdf-text-binary' => '79faa7b', @@ -80,7 +80,8 @@ module TestEnv 'invalid-utf8-diff-paths' => '99e4853', 'compare-with-merge-head-source' => 'f20a03d', 'compare-with-merge-head-target' => '2f1e176', - 'trailers' => 'f0a5ed6' + 'trailers' => 'f0a5ed6', + 'add_commit_with_5mb_subject' => '8cf8e80' }.freeze # gitlab-test-fork is a fork of gitlab-fork, but we don't necessarily diff --git a/spec/support/helpers/usage_data_helpers.rb b/spec/support/helpers/usage_data_helpers.rb index 776ea37ffdc..b9f90b11a69 100644 --- a/spec/support/helpers/usage_data_helpers.rb +++ b/spec/support/helpers/usage_data_helpers.rb @@ -129,6 +129,7 @@ module UsageDataHelpers uploads web_hooks user_preferences_user_gitpod_enabled + service_usage_data_download_payload_click ).push(*SMAU_KEYS) USAGE_DATA_KEYS = %i( diff --git a/spec/support/matchers/be_color.rb b/spec/support/matchers/be_color.rb new file mode 100644 index 00000000000..8fe29d003f9 --- /dev/null +++ b/spec/support/matchers/be_color.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +# Assert that this value is a valid color equal to the argument +# +# ``` +# expect(value).to be_color('#fff') +# ``` +RSpec::Matchers.define :be_color do |expected| + match do |actual| + next false unless actual.present? + + if expected + ::Gitlab::Color.of(actual) == ::Gitlab::Color.of(expected) + else + ::Gitlab::Color.of(actual).valid? + end + end +end + +RSpec::Matchers.alias_matcher :a_valid_color, :be_color diff --git a/spec/support/matchers/event_store.rb b/spec/support/matchers/event_store.rb index 96a71ae3c22..eb5b37f39e5 100644 --- a/spec/support/matchers/event_store.rb +++ b/spec/support/matchers/event_store.rb @@ -1,12 +1,39 @@ # frozen_string_literal: true -RSpec::Matchers.define :event_type do |event_class| - match do |actual| - actual.instance_of?(event_class) && - actual.data == @expected_data +RSpec::Matchers.define :publish_event do |expected_event_class| + supports_block_expectations + + match do |proc| + raise ArgumentError, 'This matcher only supports block expectation' unless proc.respond_to?(:call) + + @events ||= [] + + allow(Gitlab::EventStore).to receive(:publish) do |published_event| + @events << published_event + end + + proc.call + + @events.any? do |event| + event.instance_of?(expected_event_class) && event.data == @expected_data + end end - chain :containing do |expected_data| + chain :with do |expected_data| @expected_data = expected_data end + + failure_message do + "expected #{expected_event_class} with #{@expected_data} to be published, but got #{@events}" + end + + match_when_negated do |proc| + raise ArgumentError, 'This matcher only supports block expectation' unless proc.respond_to?(:call) + + allow(Gitlab::EventStore).to receive(:publish) + + proc.call + + expect(Gitlab::EventStore).not_to have_received(:publish).with(instance_of(expected_event_class)) + end end diff --git a/spec/support/matchers/pushed_frontend_feature_flags_matcher.rb b/spec/support/matchers/pushed_frontend_feature_flags_matcher.rb index b49d4da8cda..ecd174edec9 100644 --- a/spec/support/matchers/pushed_frontend_feature_flags_matcher.rb +++ b/spec/support/matchers/pushed_frontend_feature_flags_matcher.rb @@ -5,15 +5,19 @@ RSpec::Matchers.define :have_pushed_frontend_feature_flags do |expected| "\"#{key}\":#{value}" end + def html(actual) + actual.try(:html) || actual + end + match do |actual| expected.all? do |feature_flag_name, enabled| - page.html.include?(to_js(feature_flag_name, enabled)) + html(actual).include?(to_js(feature_flag_name, enabled)) end end failure_message do |actual| missing = expected.select do |feature_flag_name, enabled| - !page.html.include?(to_js(feature_flag_name, enabled)) + !html(actual).include?(to_js(feature_flag_name, enabled)) end formatted_missing_flags = missing.map { |feature_flag_name, enabled| to_js(feature_flag_name, enabled) }.join("\n") diff --git a/spec/support/sentry.rb b/spec/support/sentry.rb new file mode 100644 index 00000000000..c439b6c0fd9 --- /dev/null +++ b/spec/support/sentry.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +RSpec.configure do |config| + config.around(:example, :sentry) do |example| + dsn = Sentry.get_current_client.configuration.dsn + Sentry.get_current_client.configuration.dsn = 'dummy://b44a0828b72421a6d8e99efd68d44fa8@example.com/42' + begin + example.run + ensure + Sentry.get_current_client.configuration.dsn = dsn.to_s.presence + end + end +end diff --git a/spec/support/shared_contexts/cache_allowed_users_in_namespace_shared_context.rb b/spec/support/shared_contexts/cache_allowed_users_in_namespace_shared_context.rb index bfb719fd840..f5aa4178ae6 100644 --- a/spec/support/shared_contexts/cache_allowed_users_in_namespace_shared_context.rb +++ b/spec/support/shared_contexts/cache_allowed_users_in_namespace_shared_context.rb @@ -10,7 +10,7 @@ RSpec.shared_examples 'allowed user IDs are cached' do end it 'caches the allowed user IDs in L1 cache for 1 minute', :use_clean_rails_memory_store_caching do - Timecop.travel 2.minutes do + travel_to 2.minutes.from_now do expect do expect(described_class.l1_cache_backend).to receive(:fetch).and_call_original expect(described_class.l2_cache_backend).to receive(:fetch).and_call_original @@ -20,7 +20,7 @@ RSpec.shared_examples 'allowed user IDs are cached' do end it 'caches the allowed user IDs in L2 cache for 5 minutes', :use_clean_rails_memory_store_caching do - Timecop.travel 6.minutes do + travel_to 6.minutes.from_now do expect do expect(described_class.l1_cache_backend).to receive(:fetch).and_call_original expect(described_class.l2_cache_backend).to receive(:fetch).and_call_original diff --git a/spec/support/shared_contexts/container_repositories_shared_context.rb b/spec/support/shared_contexts/container_repositories_shared_context.rb index 7f61631dce0..9a9f80a3cbd 100644 --- a/spec/support/shared_contexts/container_repositories_shared_context.rb +++ b/spec/support/shared_contexts/container_repositories_shared_context.rb @@ -1,13 +1,16 @@ # frozen_string_literal: true RSpec.shared_context 'importable repositories' do - let_it_be(:project) { create(:project) } + let_it_be(:root_group) { create(:group) } + let_it_be(:group) { create(:group, parent_id: root_group.id) } + let_it_be(:project) { create(:project, namespace: group) } let_it_be(:valid_container_repository) { create(:container_repository, project: project, created_at: 2.days.ago) } let_it_be(:valid_container_repository2) { create(:container_repository, project: project, created_at: 1.year.ago) } let_it_be(:importing_container_repository) { create(:container_repository, :importing, project: project, created_at: 2.days.ago) } let_it_be(:new_container_repository) { create(:container_repository, project: project) } - let_it_be(:denied_group) { create(:group) } + let_it_be(:denied_root_group) { create(:group) } + let_it_be(:denied_group) { create(:group, parent_id: denied_root_group.id) } let_it_be(:denied_project) { create(:project, group: denied_group) } let_it_be(:denied_container_repository) { create(:container_repository, project: denied_project, created_at: 2.days.ago) } @@ -21,7 +24,7 @@ RSpec.shared_context 'importable repositories' do Feature::FlipperGate.create!( feature_key: 'container_registry_phase_2_deny_list', key: 'actors', - value: "Group:#{denied_group.id}" + value: "Group:#{denied_root_group.id}" ) end end diff --git a/spec/support/shared_contexts/lib/container_registry/client_stubs_shared_context.rb b/spec/support/shared_contexts/lib/container_registry/client_stubs_shared_context.rb new file mode 100644 index 00000000000..d857e683aa2 --- /dev/null +++ b/spec/support/shared_contexts/lib/container_registry/client_stubs_shared_context.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +RSpec.shared_context 'container registry client stubs' do + def stub_container_registry_gitlab_api_support(supported: true) + allow_next_instance_of(ContainerRegistry::GitlabApiClient) do |client| + allow(client).to receive(:supports_gitlab_api?).and_return(supported) + yield client if block_given? + end + end + + def stub_container_registry_gitlab_api_repository_details(client, path:, size_bytes:) + allow(client).to receive(:repository_details).with(path, with_size: true).and_return('size_bytes' => size_bytes) + end + + def stub_container_registry_gitlab_api_network_error(client_method: :supports_gitlab_api?) + allow_next_instance_of(ContainerRegistry::GitlabApiClient) do |client| + allow(client).to receive(client_method).and_raise(::Faraday::Error, nil, nil) + end + end +end diff --git a/spec/support/shared_contexts/navbar_structure_context.rb b/spec/support/shared_contexts/navbar_structure_context.rb index 576a8aa44fa..b4a71f52092 100644 --- a/spec/support/shared_contexts/navbar_structure_context.rb +++ b/spec/support/shared_contexts/navbar_structure_context.rb @@ -22,7 +22,6 @@ RSpec.shared_context 'project navbar structure' do nav_sub_items: [ _('Activity'), _('Labels'), - _('Planning hierarchy'), _('Members') ] }, @@ -204,7 +203,7 @@ RSpec.shared_context 'group navbar structure' do nav_sub_items: [] }, { - nav_item: _('Group information'), + nav_item: group.root? ? _('Group information') : _('Subgroup information'), nav_sub_items: [ _('Activity'), _('Labels'), diff --git a/spec/support/shared_contexts/spam_constants.rb b/spec/support/shared_contexts/spam_constants.rb index e88a7c1b0df..03c5caa13b2 100644 --- a/spec/support/shared_contexts/spam_constants.rb +++ b/spec/support/shared_contexts/spam_constants.rb @@ -2,10 +2,11 @@ RSpec.shared_context 'includes Spam constants' do before do - stub_const('CONDITIONAL_ALLOW', Spam::SpamConstants::CONDITIONAL_ALLOW) + stub_const('BLOCK_USER', Spam::SpamConstants::BLOCK_USER) stub_const('DISALLOW', Spam::SpamConstants::DISALLOW) + stub_const('CONDITIONAL_ALLOW', Spam::SpamConstants::CONDITIONAL_ALLOW) + stub_const('OVERRIDE_VIA_ALLOW_POSSIBLE_SPAM', Spam::SpamConstants::OVERRIDE_VIA_ALLOW_POSSIBLE_SPAM) stub_const('ALLOW', Spam::SpamConstants::ALLOW) - stub_const('BLOCK_USER', Spam::SpamConstants::BLOCK_USER) stub_const('NOOP', Spam::SpamConstants::NOOP) end end diff --git a/spec/support/shared_examples/attention_request_cache_invalidation_examples.rb b/spec/support/shared_examples/attention_request_cache_invalidation_examples.rb new file mode 100644 index 00000000000..7fe696abc69 --- /dev/null +++ b/spec/support/shared_examples/attention_request_cache_invalidation_examples.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'invalidates attention request cache' do + it 'invalidates the merge requests requiring attention count' do + cache_mock = double + + users.each do |user| + expect(cache_mock).to receive(:delete).with(['users', user.id, 'attention_requested_open_merge_requests_count']) + end + + allow(Rails).to receive(:cache).and_return(cache_mock) + + service.execute + end +end diff --git a/spec/support/shared_examples/blocks_unsafe_serialization_shared_examples.rb b/spec/support/shared_examples/blocks_unsafe_serialization_shared_examples.rb new file mode 100644 index 00000000000..db42e41344f --- /dev/null +++ b/spec/support/shared_examples/blocks_unsafe_serialization_shared_examples.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +require 'spec_helper' + +# Requires a context with: +# - object +# +RSpec.shared_examples 'blocks unsafe serialization' do + it 'blocks as_json' do + expect { object.as_json }.to raise_error(described_class::UnsafeSerializationError, /#{object.class.name}/) + end + + it 'blocks to_json' do + expect { object.to_json }.to raise_error(described_class::UnsafeSerializationError, /#{object.class.name}/) + end +end + +RSpec.shared_examples 'allows unsafe serialization' do + it 'allows as_json' do + expect { object.as_json }.not_to raise_error + end + + it 'allows to_json' do + expect { object.to_json }.not_to raise_error + end +end diff --git a/spec/support/shared_examples/controllers/clusters_controller_shared_examples.rb b/spec/support/shared_examples/controllers/clusters_controller_shared_examples.rb index aa17e72d08e..9fab7f3f94e 100644 --- a/spec/support/shared_examples/controllers/clusters_controller_shared_examples.rb +++ b/spec/support/shared_examples/controllers/clusters_controller_shared_examples.rb @@ -27,3 +27,33 @@ RSpec.shared_examples 'GET new cluster shared examples' do end end end + +RSpec.shared_examples ':certificate_based_clusters feature flag index responses' do + context 'feature flag is disabled' do + before do + stub_feature_flags(certificate_based_clusters: false) + end + + it 'does not list any clusters' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to render_template(:index) + expect(assigns(:clusters)).to be_empty + end + end +end + +RSpec.shared_examples ':certificate_based_clusters feature flag controller responses' do + context 'feature flag is disabled' do + before do + stub_feature_flags(certificate_based_clusters: false) + end + + it 'responds with :not_found' do + subject + + expect(response).to have_gitlab_http_status(:not_found) + end + end +end diff --git a/spec/support/shared_examples/controllers/rate_limited_endpoint_shared_examples.rb b/spec/support/shared_examples/controllers/rate_limited_endpoint_shared_examples.rb index bb2a4159071..20edca1ee9f 100644 --- a/spec/support/shared_examples/controllers/rate_limited_endpoint_shared_examples.rb +++ b/spec/support/shared_examples/controllers/rate_limited_endpoint_shared_examples.rb @@ -13,10 +13,16 @@ RSpec.shared_examples 'rate limited endpoint' do |rate_limit_key:| env: :"#{rate_limit_key}_request_limit", remote_ip: kind_of(String), request_method: kind_of(String), - path: kind_of(String), - user_id: current_user.id, - username: current_user.username - } + path: kind_of(String) + }.merge(expected_user_attributes) + end + + let(:expected_user_attributes) do + if defined?(current_user) && current_user.present? + { user_id: current_user.id, username: current_user.username } + else + {} + end end let(:error_message) { _('This endpoint has been requested too many times. Try again later.') } diff --git a/spec/support/shared_examples/controllers/unique_hll_events_examples.rb b/spec/support/shared_examples/controllers/unique_hll_events_examples.rb index 842ad89bafd..38c3157e898 100644 --- a/spec/support/shared_examples/controllers/unique_hll_events_examples.rb +++ b/spec/support/shared_examples/controllers/unique_hll_events_examples.rb @@ -2,14 +2,14 @@ # # Requires a context containing: # - request -# - expected_type -# - target_id +# - expected_value +# - target_event RSpec.shared_examples 'tracking unique hll events' do it 'tracks unique event' do expect(Gitlab::UsageDataCounters::HLLRedisCounter).to( receive(:track_event) - .with(target_id, values: expected_type) + .with(target_event, values: expected_value) .and_call_original # we call original to trigger additional validations; otherwise the method is stubbed ) diff --git a/spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb b/spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb index 5ed8dc7ce98..6dca94ecf0a 100644 --- a/spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb +++ b/spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb @@ -211,10 +211,22 @@ RSpec.shared_examples 'handle uploads' do stub_feature_flags(enforce_auth_checks_on_uploads: true) end - it "responds with status 302" do + it "responds with appropriate status" do show_upload - expect(response).to have_gitlab_http_status(:redirect) + # We're switching here based on the class due to the feature + # flag :enforce_auth_checks_on_uploads switching on project. + # When it is enabled fully, we will apply the code it guards + # to both Projects::UploadsController as well as + # Groups::UploadsController. + # + # https://gitlab.com/gitlab-org/gitlab/-/issues/352291 + # + if model.instance_of?(Group) + expect(response).to have_gitlab_http_status(:ok) + else + expect(response).to have_gitlab_http_status(:redirect) + end end end @@ -305,7 +317,19 @@ RSpec.shared_examples 'handle uploads' do it "responds with status 404" do show_upload - expect(response).to have_gitlab_http_status(:not_found) + # We're switching here based on the class due to the feature + # flag :enforce_auth_checks_on_uploads switching on + # project. When it is enabled fully, we will apply the + # code it guards to both Projects::UploadsController as + # well as Groups::UploadsController. + # + # https://gitlab.com/gitlab-org/gitlab/-/issues/352291 + # + if model.instance_of?(Group) + expect(response).to have_gitlab_http_status(:ok) + else + expect(response).to have_gitlab_http_status(:not_found) + end end end diff --git a/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb b/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb index 1cb52c07069..bf26922d9c5 100644 --- a/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb +++ b/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb @@ -220,8 +220,8 @@ RSpec.shared_examples 'wiki controller actions' do context 'page view tracking' do it_behaves_like 'tracking unique hll events' do - let(:target_id) { 'wiki_action' } - let(:expected_type) { instance_of(String) } + let(:target_event) { 'wiki_action' } + let(:expected_value) { instance_of(String) } end it 'increases the page view counter' do diff --git a/spec/support/shared_examples/features/clusters_shared_examples.rb b/spec/support/shared_examples/features/clusters_shared_examples.rb new file mode 100644 index 00000000000..6ee60f20b2e --- /dev/null +++ b/spec/support/shared_examples/features/clusters_shared_examples.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +RSpec.shared_examples "user disables a cluster" do + context 'when user disables the cluster' do + before do + page.find(:css, '.js-cluster-enable-toggle-area .js-project-feature-toggle').click + page.within('.js-cluster-details-form') { click_button 'Save changes' } + end + + it 'user sees the successful message' do + expect(page).to have_content('Kubernetes cluster was successfully updated.') + end + end +end diff --git a/spec/support/shared_examples/features/container_registry_shared_examples.rb b/spec/support/shared_examples/features/container_registry_shared_examples.rb index 06b2b8c621c..6aa7e6e6270 100644 --- a/spec/support/shared_examples/features/container_registry_shared_examples.rb +++ b/spec/support/shared_examples/features/container_registry_shared_examples.rb @@ -7,3 +7,20 @@ RSpec.shared_examples 'handling feature network errors with the container regist expect(page).to have_content 'We are having trouble connecting to the Container Registry' end end + +RSpec.shared_examples 'rejecting tags destruction for an importing repository on' do |tags: []| + it 'rejects the tag destruction operation' do + service = instance_double('Projects::ContainerRepository::DeleteTagsService') + expect(service).to receive(:execute).with(container_repository) { { status: :error, message: 'repository importing' } } + expect(Projects::ContainerRepository::DeleteTagsService).to receive(:new).with(container_repository.project, user, tags: tags) { service } + + first('[data-testid="additional-actions"]').click + first('[data-testid="single-delete-button"]').click + expect(find('.modal .modal-title')).to have_content _('Remove tag') + find('.modal .modal-footer .btn-danger').click + + alert_body = find('.gl-alert-body') + expect(alert_body).to have_content('Tags temporarily cannot be marked for deletion. Please try again in a few minutes.') + expect(alert_body).to have_link('More details', href: help_page_path('user/packages/container_registry/index', anchor: 'tags-temporarily-cannot-be-marked-for-deletion')) + end +end diff --git a/spec/support/shared_examples/features/integrations/user_activates_mattermost_slash_command_integration_shared_examples.rb b/spec/support/shared_examples/features/integrations/user_activates_mattermost_slash_command_integration_shared_examples.rb index cfa043322db..4c312b42c0a 100644 --- a/spec/support/shared_examples/features/integrations/user_activates_mattermost_slash_command_integration_shared_examples.rb +++ b/spec/support/shared_examples/features/integrations/user_activates_mattermost_slash_command_integration_shared_examples.rb @@ -18,7 +18,7 @@ RSpec.shared_examples 'user activates the Mattermost Slash Command integration' click_active_checkbox click_save_integration - expect(current_path).to eq(edit_path) + expect(page).to have_current_path(edit_path, ignore_query: true) expect(page).to have_content('Mattermost slash commands settings saved, but not active.') end @@ -28,7 +28,7 @@ RSpec.shared_examples 'user activates the Mattermost Slash Command integration' fill_in 'service_token', with: token click_save_integration - expect(current_path).to eq(edit_path) + expect(page).to have_current_path(edit_path, ignore_query: true) expect(page).to have_content('Mattermost slash commands settings saved and active.') end end diff --git a/spec/support/shared_examples/features/manage_applications_shared_examples.rb b/spec/support/shared_examples/features/manage_applications_shared_examples.rb index 27d50c67f24..3a8267b21da 100644 --- a/spec/support/shared_examples/features/manage_applications_shared_examples.rb +++ b/spec/support/shared_examples/features/manage_applications_shared_examples.rb @@ -5,7 +5,7 @@ RSpec.shared_examples 'manage applications' do let_it_be(:application_name_changed) { "#{application_name} changed" } let_it_be(:application_redirect_uri) { 'https://foo.bar' } - it 'allows user to manage applications' do + it 'allows user to manage applications', :js do visit new_application_path expect(page).to have_content 'Add new application' diff --git a/spec/support/shared_examples/features/multiple_assignees_widget_mr_shared_examples.rb b/spec/support/shared_examples/features/multiple_assignees_widget_mr_shared_examples.rb new file mode 100644 index 00000000000..bbde448a1a1 --- /dev/null +++ b/spec/support/shared_examples/features/multiple_assignees_widget_mr_shared_examples.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'multiple assignees widget merge request' do |action, save_button_title| + it "#{action} a MR with multiple assignees", :js do + find('.js-assignee-search').click + page.within '.dropdown-menu-user' do + click_link user.name + click_link user2.name + end + + # Extra click needed in order to toggle the dropdown + find('.js-assignee-search').click + + expect(all('input[name="merge_request[assignee_ids][]"]', visible: false).map(&:value)) + .to match_array([user.id.to_s, user2.id.to_s]) + + page.within '.js-assignee-search' do + expect(page).to have_content "#{user2.name} + 1 more" + end + + click_button save_button_title + + page.within '.issuable-sidebar' do + page.within '.assignee' do + expect(page).to have_content '2 Assignees' + + click_button('Edit') + + expect(page).to have_content user.name + expect(page).to have_content user2.name + end + end + + page.within '.dropdown-menu-user' do + click_link user.name + end + + page.within '.issuable-sidebar' do + page.within '.assignee' do + # Closing dropdown to persist + click_button('Apply') + + expect(page).to have_content user2.name + end + end + end +end diff --git a/spec/support/shared_examples/features/project_upload_files_shared_examples.rb b/spec/support/shared_examples/features/project_upload_files_shared_examples.rb index 85434ba7afd..066c3e17a09 100644 --- a/spec/support/shared_examples/features/project_upload_files_shared_examples.rb +++ b/spec/support/shared_examples/features/project_upload_files_shared_examples.rb @@ -24,7 +24,7 @@ RSpec.shared_examples 'it uploads and commits a new text file' do |drop: false| click_button('Upload file') expect(page).to have_content('New commit message') - expect(current_path).to eq(project_new_merge_request_path(project)) + expect(page).to have_current_path(project_new_merge_request_path(project), ignore_query: true) click_link('Changes') find("a[data-action='diffs']", text: 'Changes').click @@ -129,7 +129,7 @@ RSpec.shared_examples 'it uploads and commits a new file to a forked project' do fork = user.fork_of(project2.reload) - expect(current_path).to eq(project_new_merge_request_path(fork)) + expect(page).to have_current_path(project_new_merge_request_path(fork), ignore_query: true) find("a[data-action='diffs']", text: 'Changes').click diff --git a/spec/support/shared_examples/features/wiki/user_creates_wiki_page_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_creates_wiki_page_shared_examples.rb index dfc9a45bd0d..f676b6aa60d 100644 --- a/spec/support/shared_examples/features/wiki/user_creates_wiki_page_shared_examples.rb +++ b/spec/support/shared_examples/features/wiki/user_creates_wiki_page_shared_examples.rb @@ -50,7 +50,7 @@ RSpec.shared_examples 'User creates wiki page' do click_on("Create page") end - expect(current_path).to include("one/two/three-test") + expect(page).to have_current_path(%r(one/two/three-test), ignore_query: true) expect(page).to have_link(href: wiki_page_path(wiki, 'one/two/three-test')) end @@ -68,7 +68,7 @@ RSpec.shared_examples 'User creates wiki page' do click_button("Create page") end - expect(current_path).to eq(wiki_page_path(wiki, "home")) + expect(page).to have_current_path(wiki_page_path(wiki, "home"), ignore_query: true) expect(page).to have_content("test GitLab API doc Rake tasks Wiki header") .and have_content("Home") .and have_content("Last edited by #{user.name}") @@ -76,7 +76,7 @@ RSpec.shared_examples 'User creates wiki page' do click_link("test") - expect(current_path).to eq(wiki_page_path(wiki, "test")) + expect(page).to have_current_path(wiki_page_path(wiki, "test"), ignore_query: true) page.within(:css, ".wiki-page-header") do expect(page).to have_content("Create New Page") @@ -84,11 +84,11 @@ RSpec.shared_examples 'User creates wiki page' do click_link("Home") - expect(current_path).to eq(wiki_page_path(wiki, "home")) + expect(page).to have_current_path(wiki_page_path(wiki, "home"), ignore_query: true) click_link("GitLab API") - expect(current_path).to eq(wiki_page_path(wiki, "api")) + expect(page).to have_current_path(wiki_page_path(wiki, "api"), ignore_query: true) page.within(:css, ".wiki-page-header") do expect(page).to have_content("Create") @@ -96,11 +96,11 @@ RSpec.shared_examples 'User creates wiki page' do click_link("Home") - expect(current_path).to eq(wiki_page_path(wiki, "home")) + expect(page).to have_current_path(wiki_page_path(wiki, "home"), ignore_query: true) click_link("Rake tasks") - expect(current_path).to eq(wiki_page_path(wiki, "raketasks")) + expect(page).to have_current_path(wiki_page_path(wiki, "raketasks"), ignore_query: true) page.within(:css, ".wiki-page-header") do expect(page).to have_content("Create") diff --git a/spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb index a456b76b324..85490bffc0e 100644 --- a/spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb +++ b/spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb @@ -25,7 +25,7 @@ RSpec.shared_examples 'User updates wiki page' do click_on('Cancel') end - expect(current_path).to eq wiki_path(wiki) + expect(page).to have_current_path wiki_path(wiki), ignore_query: true end it 'updates a page that has a path', :js do @@ -36,7 +36,7 @@ RSpec.shared_examples 'User updates wiki page' do click_on('Create page') end - expect(current_path).to include('one/two/three-test') + expect(page).to have_current_path(%r(one/two/three-test), ignore_query: true) expect(find('.wiki-pages')).to have_content('three') first(:link, text: 'three').click @@ -45,7 +45,7 @@ RSpec.shared_examples 'User updates wiki page' do click_on('Edit') - expect(current_path).to include('one/two/three-test') + expect(page).to have_current_path(%r(one/two/three-test), ignore_query: true) expect(page).to have_content('Edit Page') fill_in('Content', with: 'Updated Wiki Content') @@ -120,7 +120,7 @@ RSpec.shared_examples 'User updates wiki page' do click_on('Cancel') end - expect(current_path).to eq(wiki_page_path(wiki, wiki_page)) + expect(page).to have_current_path(wiki_page_path(wiki, wiki_page), ignore_query: true) end it_behaves_like 'wiki file attachments' @@ -175,7 +175,7 @@ RSpec.shared_examples 'User updates wiki page' do click_button('Save changes') - expect(current_path).to eq(wiki_page_path(wiki, page_name)) + expect(page).to have_current_path(wiki_page_path(wiki, page_name), ignore_query: true) end it 'moves the page to other dir', :js do @@ -185,7 +185,7 @@ RSpec.shared_examples 'User updates wiki page' do click_button('Save changes') - expect(current_path).to eq(wiki_page_path(wiki, new_page_dir)) + expect(page).to have_current_path(wiki_page_path(wiki, new_page_dir), ignore_query: true) end it 'remains in the same place if title has not changed', :js do @@ -195,7 +195,7 @@ RSpec.shared_examples 'User updates wiki page' do click_button('Save changes') - expect(current_path).to eq(original_path) + expect(page).to have_current_path(original_path, ignore_query: true) end it 'can be moved to a different dir with a different name', :js do @@ -205,7 +205,7 @@ RSpec.shared_examples 'User updates wiki page' do click_button('Save changes') - expect(current_path).to eq(wiki_page_path(wiki, new_page_dir)) + expect(page).to have_current_path(wiki_page_path(wiki, new_page_dir), ignore_query: true) end it 'can be renamed and moved to the root folder', :js do @@ -215,7 +215,7 @@ RSpec.shared_examples 'User updates wiki page' do click_button('Save changes') - expect(current_path).to eq(wiki_page_path(wiki, new_name)) + expect(page).to have_current_path(wiki_page_path(wiki, new_name), ignore_query: true) end it 'squishes the title before creating the page', :js do @@ -225,7 +225,7 @@ RSpec.shared_examples 'User updates wiki page' do click_button('Save changes') - expect(current_path).to eq(wiki_page_path(wiki, "foo1/bar1/#{page_name}")) + expect(page).to have_current_path(wiki_page_path(wiki, "foo1/bar1/#{page_name}"), ignore_query: true) end it_behaves_like 'wiki file attachments' diff --git a/spec/support/shared_examples/features/wiki/user_views_wiki_page_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_views_wiki_page_shared_examples.rb index eec911f3b6f..a7c32932ba7 100644 --- a/spec/support/shared_examples/features/wiki/user_views_wiki_page_shared_examples.rb +++ b/spec/support/shared_examples/features/wiki/user_views_wiki_page_shared_examples.rb @@ -37,12 +37,12 @@ RSpec.shared_examples 'User views a wiki page' do end it 'shows the history of a page that has a path' do - expect(current_path).to include('one/two/three-test') + expect(page).to have_current_path(%r(one/two/three-test)) first(:link, text: 'three').click click_on('Page history') - expect(current_path).to include('one/two/three-test') + expect(page).to have_current_path(%r(one/two/three-test)) page.within(:css, '.wiki-page-header') do expect(page).to have_content('History') @@ -50,7 +50,7 @@ RSpec.shared_examples 'User views a wiki page' do end it 'shows an old version of a page', :js do - expect(current_path).to include('one/two/three-test') + expect(page).to have_current_path(%r(one/two/three-test)) expect(find('.wiki-pages')).to have_content('three') first(:link, text: 'three').click @@ -59,7 +59,7 @@ RSpec.shared_examples 'User views a wiki page' do click_on('Edit') - expect(current_path).to include('one/two/three-test') + expect(page).to have_current_path(%r(one/two/three-test)) expect(page).to have_content('Edit Page') fill_in('Content', with: 'Updated Wiki Content') @@ -93,13 +93,12 @@ RSpec.shared_examples 'User views a wiki page' do let(:path) { upload_file_to_wiki(wiki, user, 'dk.png') } it do - expect(page).to have_xpath("//img[@data-src='#{wiki.wiki_base_path}/#{path}']") + expect(page).to have_xpath("//img[@src='#{wiki.wiki_base_path}/#{path}']") expect(page).to have_link('image', href: "#{wiki.wiki_base_path}/#{path}") click_on('image') - expect(current_path).to match("wikis/#{path}") - expect(page).not_to have_xpath('/html') # Page should render the image which means there is no html involved + expect(page).to have_current_path(%r(wikis/#{path})) end end @@ -108,7 +107,7 @@ RSpec.shared_examples 'User views a wiki page' do click_on('image') - expect(current_path).to match("wikis/#{path}") + expect(page).to have_current_path(%r(wikis/#{path})) expect(page).to have_content('Create New Page') end end diff --git a/spec/support/shared_examples/features/wiki/user_views_wiki_pages_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_views_wiki_pages_shared_examples.rb index 314c2074eee..32cb2b1d187 100644 --- a/spec/support/shared_examples/features/wiki/user_views_wiki_pages_shared_examples.rb +++ b/spec/support/shared_examples/features/wiki/user_views_wiki_pages_shared_examples.rb @@ -60,7 +60,7 @@ RSpec.shared_examples 'User views wiki pages' do before do page.within('.wiki-sort-dropdown') do click_button('Title') - click_link('Created date') + click_button('Created date') end end diff --git a/spec/support/shared_examples/graphql/members_shared_examples.rb b/spec/support/shared_examples/graphql/members_shared_examples.rb index b0bdd27a95f..8e9e22f4359 100644 --- a/spec/support/shared_examples/graphql/members_shared_examples.rb +++ b/spec/support/shared_examples/graphql/members_shared_examples.rb @@ -76,8 +76,10 @@ RSpec.shared_examples 'querying members with a group' do resolve(described_class, obj: resource, args: base_args.merge(args), ctx: { current_user: other_user }) end - it 'raises an error' do - expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) + it 'generates an error' do + expect_graphql_error_to_be_created(Gitlab::Graphql::Errors::ResourceNotAvailable) do + subject + end end end end diff --git a/spec/support/shared_examples/graphql/mutations/security/ci_configuration_shared_examples.rb b/spec/support/shared_examples/graphql/mutations/security/ci_configuration_shared_examples.rb index 14b2663a72c..21260e4d954 100644 --- a/spec/support/shared_examples/graphql/mutations/security/ci_configuration_shared_examples.rb +++ b/spec/support/shared_examples/graphql/mutations/security/ci_configuration_shared_examples.rb @@ -29,8 +29,10 @@ RSpec.shared_examples_for 'graphql mutations security ci configuration' do describe '#resolve' do let(:result) { subject } - it 'raises an error if the resource is not accessible to the user' do - expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) + it 'generates an error if the resource is not accessible to the user' do + expect_graphql_error_to_be_created(Gitlab::Graphql::Errors::ResourceNotAvailable) do + subject + end end context 'when user does not have enough permissions' do @@ -38,8 +40,10 @@ RSpec.shared_examples_for 'graphql mutations security ci configuration' do project.add_guest(user) end - it 'raises an error' do - expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) + it 'generates an error' do + expect_graphql_error_to_be_created(Gitlab::Graphql::Errors::ResourceNotAvailable) do + subject + end end end @@ -48,8 +52,10 @@ RSpec.shared_examples_for 'graphql mutations security ci configuration' do create(:project_empty_repo).add_maintainer(user) end - it 'raises an error' do - expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) + it 'generates an error' do + expect_graphql_error_to_be_created(Gitlab::Graphql::Errors::ResourceNotAvailable) do + subject + end end end diff --git a/spec/support/shared_examples/graphql/types/merge_request_interactions_type_shared_examples.rb b/spec/support/shared_examples/graphql/types/merge_request_interactions_type_shared_examples.rb new file mode 100644 index 00000000000..0d0dbb112de --- /dev/null +++ b/spec/support/shared_examples/graphql/types/merge_request_interactions_type_shared_examples.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.shared_examples "a user type with merge request interaction type" do + specify { expect(described_class).to require_graphql_authorizations(:read_user) } + + it 'has the expected fields' do + expected_fields = %w[ + id + bot + user_permissions + snippets + name + username + email + publicEmail + avatarUrl + webUrl + webPath + todos + state + status + location + authoredMergeRequests + assignedMergeRequests + reviewRequestedMergeRequests + groupMemberships + groupCount + projectMemberships + starredProjects + callouts + merge_request_interaction + namespace + timelogs + groups + gitpodEnabled + preferencesGitpodPath + profileEnableGitpodPath + savedReplies + ] + + expect(described_class).to have_graphql_fields(*expected_fields) + end + + describe '#merge_request_interaction' do + subject { described_class.fields['mergeRequestInteraction'] } + + it 'returns the correct type' do + is_expected.to have_graphql_type(Types::UserMergeRequestInteractionType) + end + + it 'has the correct arguments' do + is_expected.to have_attributes(arguments: be_empty) + end + end +end diff --git a/spec/support/shared_examples/integrations/integration_settings_form.rb b/spec/support/shared_examples/integrations/integration_settings_form.rb index d0bb40e43ee..d8a46180796 100644 --- a/spec/support/shared_examples/integrations/integration_settings_form.rb +++ b/spec/support/shared_examples/integrations/integration_settings_form.rb @@ -22,10 +22,7 @@ RSpec.shared_examples 'integration settings form' do events = parse_json(trigger_events_for_integration(integration)) events.each do |trigger| - # normalizing the title because capybara location is case sensitive - title = normalize_title trigger[:title], integration - - expect(page).to have_field(title, type: 'checkbox', wait: 0), + expect(page).to have_field(trigger[:title], type: 'checkbox', wait: 0), "#{integration.title} field #{title} checkbox not present" end end @@ -35,12 +32,6 @@ RSpec.shared_examples 'integration settings form' do private - def normalize_title(title, integration) - return 'Merge request' if integration.is_a?(Integrations::Jira) && title == 'merge_request' - - title.titlecase - end - def parse_json(json) Gitlab::Json.parse(json, symbolize_names: true) end diff --git a/spec/support/shared_examples/lib/gitlab/database/background_migration_job_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/database/background_migration_job_shared_examples.rb index 213f084be17..771ab89972c 100644 --- a/spec/support/shared_examples/lib/gitlab/database/background_migration_job_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/database/background_migration_job_shared_examples.rb @@ -2,7 +2,7 @@ RSpec.shared_examples 'marks background migration job records' do it 'marks each job record as succeeded after processing' do - create(:background_migration_job, class_name: "::#{described_class.name}", + create(:background_migration_job, class_name: "::#{described_class.name.demodulize}", arguments: arguments) expect(::Gitlab::Database::BackgroundMigrationJob).to receive(:mark_all_as_succeeded).and_call_original @@ -13,7 +13,7 @@ RSpec.shared_examples 'marks background migration job records' do end it 'returns the number of job records marked as succeeded' do - create(:background_migration_job, class_name: "::#{described_class.name}", + create(:background_migration_job, class_name: "::#{described_class.name.demodulize}", arguments: arguments) jobs_updated = subject.perform(*arguments) diff --git a/spec/support/shared_examples/lib/gitlab/usage_data_counters/usage_counter_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/usage_data_counters/usage_counter_shared_examples.rb new file mode 100644 index 00000000000..848437577d7 --- /dev/null +++ b/spec/support/shared_examples/lib/gitlab/usage_data_counters/usage_counter_shared_examples.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'a usage counter' do + describe '.increment' do + let(:project_id) { 12 } + + it 'intializes and increments the counter for the project by 1' do + expect do + described_class.increment(project_id) + end.to change { described_class.usage_totals[project_id] }.from(nil).to(1) + end + end + + describe '.usage_totals' do + let(:usage_totals) { described_class.usage_totals } + + context 'when the feature has not been used' do + it 'returns the total counts and counts per project' do + expect(usage_totals.keys).to eq([:total]) + expect(usage_totals[:total]).to eq(0) + end + end + + context 'when the feature has been used in multiple projects' do + let(:project1_id) { 12 } + let(:project2_id) { 16 } + + before do + described_class.increment(project1_id) + described_class.increment(project2_id) + end + + it 'returns the total counts and counts per project' do + expect(usage_totals[project1_id]).to eq(1) + expect(usage_totals[project2_id]).to eq(1) + expect(usage_totals[:total]).to eq(2) + end + end + end +end diff --git a/spec/support/shared_examples/lib/wikis_api_examples.rb b/spec/support/shared_examples/lib/wikis_api_examples.rb index 2e4c667d37e..f068a7676ad 100644 --- a/spec/support/shared_examples/lib/wikis_api_examples.rb +++ b/spec/support/shared_examples/lib/wikis_api_examples.rb @@ -44,13 +44,70 @@ RSpec.shared_examples_for 'wikis API returns list of wiki pages' do end RSpec.shared_examples_for 'wikis API returns wiki page' do - it 'returns the wiki page' do - expect(response).to have_gitlab_http_status(:ok) - expect(json_response.size).to eq(4) - expect(json_response.keys).to match_array(expected_keys_with_content) - expect(json_response['content']).to eq(page.content) - expect(json_response['slug']).to eq(page.slug) - expect(json_response['title']).to eq(page.title) + subject(:request) { get api(url, user), params: params } + + shared_examples 'returns wiki page' do + before do + request + end + + specify do + expect(response).to have_gitlab_http_status(:ok) + expect(json_response.size).to eq(5) + expect(json_response.keys).to match_array(expected_keys_with_content) + expect(json_response['content']).to eq(expected_content) + expect(json_response['slug']).to eq(page.slug) + expect(json_response['title']).to eq(page.title) + end + end + + let(:expected_content) { page.content } + + it_behaves_like 'returns wiki page' + + context 'when render param is false' do + let(:params) { { render_html: false } } + + it_behaves_like 'returns wiki page' + end + + context 'when render param is true' do + let(:params) { { render_html: true } } + let(:expected_content) { '<p data-sourcepos="1:1-1:21" dir="auto">Content for wiki page</p>' } + + it_behaves_like 'returns wiki page' + end + + context 'when wiki page has versions' do + let(:new_content) { 'New content' } + + before do + wiki.update_page(page.page, content: new_content, message: 'updated page') + + expect(page.count_versions).to eq(2) + + request + end + + context 'when version param is not present' do + it 'retrieves the last version' do + expect(json_response['content']).to eq(new_content) + end + end + + context 'when version param is set' do + let(:params) { { version: page.version.id } } + + it 'retrieves the specific page version' do + expect(json_response['content']).to eq(page.content) + end + + context 'when version param is not valid or inexistent' do + let(:params) { { version: 'foobar' } } + + it_behaves_like 'wiki API 404 Wiki Page Not Found' + end + end end end @@ -59,12 +116,13 @@ RSpec.shared_examples_for 'wikis API creates wiki page' do post(api(url, user), params: payload) expect(response).to have_gitlab_http_status(:created) - expect(json_response.size).to eq(4) + expect(json_response.size).to eq(5) expect(json_response.keys).to match_array(expected_keys_with_content) expect(json_response['content']).to eq(payload[:content]) expect(json_response['slug']).to eq(payload[:title].tr(' ', '-')) expect(json_response['title']).to eq(payload[:title]) expect(json_response['rdoc']).to eq(payload[:rdoc]) + expect(json_response['encoding']).to eq('UTF-8') end [:title, :content].each do |part| @@ -85,7 +143,7 @@ RSpec.shared_examples_for 'wikis API updates wiki page' do put(api(url, user), params: payload) expect(response).to have_gitlab_http_status(:ok) - expect(json_response.size).to eq(4) + expect(json_response.size).to eq(5) expect(json_response.keys).to match_array(expected_keys_with_content) expect(json_response['content']).to eq(payload[:content]) expect(json_response['slug']).to eq(payload[:title].tr(' ', '-')) diff --git a/spec/support/shared_examples/metrics/active_record_subscriber_shared_examples.rb b/spec/support/shared_examples/metrics/active_record_subscriber_shared_examples.rb index 6e8c340582a..3f187a7e9e4 100644 --- a/spec/support/shared_examples/metrics/active_record_subscriber_shared_examples.rb +++ b/spec/support/shared_examples/metrics/active_record_subscriber_shared_examples.rb @@ -91,21 +91,6 @@ RSpec.shared_examples 'store ActiveRecord info in RequestStore' do |db_role| end end end - - context 'when the GITLAB_MULTIPLE_DATABASE_METRICS env var is disabled' do - before do - stub_env('GITLAB_MULTIPLE_DATABASE_METRICS', nil) - end - - it 'does not include per database metrics' do - Gitlab::WithRequestStore.with_request_store do - subscriber.sql(event) - - expect(described_class.db_counter_payload).not_to include(:"db_replica_#{db_config_name}_duration_s") - expect(described_class.db_counter_payload).not_to include(:"db_replica_#{db_config_name}_count") - end - end - end end RSpec.shared_examples 'record ActiveRecord metrics in a metrics transaction' do |db_role| @@ -160,26 +145,6 @@ RSpec.shared_examples 'record ActiveRecord metrics in a metrics transaction' do subscriber.sql(event) end - - context 'when the GITLAB_MULTIPLE_DATABASE_METRICS env var is disabled' do - before do - stub_env('GITLAB_MULTIPLE_DATABASE_METRICS', nil) - end - - it 'does not include db_config_name label' do - allow(transaction).to receive(:increment) do |*args| - labels = args[2] || {} - expect(labels).not_to include(:db_config_name) - end - - allow(transaction).to receive(:observe) do |*args| - labels = args[2] || {} - expect(labels).not_to include(:db_config_name) - end - - subscriber.sql(event) - end - end end RSpec.shared_examples 'record ActiveRecord metrics' do |db_role| diff --git a/spec/support/shared_examples/models/atomic_internal_id_shared_examples.rb b/spec/support/shared_examples/models/atomic_internal_id_shared_examples.rb index fe85daa7235..bb15a3054ac 100644 --- a/spec/support/shared_examples/models/atomic_internal_id_shared_examples.rb +++ b/spec/support/shared_examples/models/atomic_internal_id_shared_examples.rb @@ -155,7 +155,7 @@ RSpec.shared_examples 'AtomicInternalId' do |validate_presence: true| end def expect_iid_to_be_set_and_rollback - ActiveRecord::Base.transaction(requires_new: true) do + instance.transaction(requires_new: true) do instance.save! expect(read_internal_id).not_to be_nil diff --git a/spec/support/shared_examples/models/concerns/integrations/slack_mattermost_notifier_shared_examples.rb b/spec/support/shared_examples/models/concerns/integrations/slack_mattermost_notifier_shared_examples.rb index 2a976fb7421..d6415e98289 100644 --- a/spec/support/shared_examples/models/concerns/integrations/slack_mattermost_notifier_shared_examples.rb +++ b/spec/support/shared_examples/models/concerns/integrations/slack_mattermost_notifier_shared_examples.rb @@ -692,16 +692,6 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |integration_name context 'notification enabled for all branches' do it_behaves_like "triggered #{integration_name} integration", event_type: "pipeline", branches_to_be_notified: "all" end - - context 'when chat_notification_deployment_protected_branch_filter is disabled' do - before do - stub_feature_flags(chat_notification_deployment_protected_branch_filter: false) - end - - context 'notification enabled only for default branch' do - it_behaves_like "triggered #{integration_name} integration", event_type: "pipeline", branches_to_be_notified: "default" - end - end end end end diff --git a/spec/support/shared_examples/models/concerns/limitable_shared_examples.rb b/spec/support/shared_examples/models/concerns/limitable_shared_examples.rb index 07d687147bc..0ff0895b861 100644 --- a/spec/support/shared_examples/models/concerns/limitable_shared_examples.rb +++ b/spec/support/shared_examples/models/concerns/limitable_shared_examples.rb @@ -23,7 +23,7 @@ RSpec.shared_examples 'includes Limitable concern' do context 'with an existing model' do before do - subject.dup.save! + subject.clone.save! end it 'cannot create new models exceeding the plan limits' do diff --git a/spec/support/shared_examples/models/concerns/timebox_shared_examples.rb b/spec/support/shared_examples/models/concerns/timebox_shared_examples.rb index 39121b73bc5..a2b4cdc33d0 100644 --- a/spec/support/shared_examples/models/concerns/timebox_shared_examples.rb +++ b/spec/support/shared_examples/models/concerns/timebox_shared_examples.rb @@ -66,17 +66,6 @@ RSpec.shared_examples 'a timebox' do |timebox_type| end end - describe 'title' do - it { is_expected.to validate_presence_of(:title) } - - it 'is invalid if title would be empty after sanitation' do - timebox = build(timebox_type, *timebox_args, project: project, title: '<img src=x onerror=prompt(1)>') - - expect(timebox).not_to be_valid - expect(timebox.errors[:title]).to include("can't be blank") - end - end - describe '#timebox_type_check' do it 'is invalid if it has both project_id and group_id' do timebox = build(timebox_type, *timebox_args, group: group) diff --git a/spec/support/shared_examples/models/concerns/update_namespace_statistics_shared_examples.rb b/spec/support/shared_examples/models/concerns/update_namespace_statistics_shared_examples.rb new file mode 100644 index 00000000000..255b6efa518 --- /dev/null +++ b/spec/support/shared_examples/models/concerns/update_namespace_statistics_shared_examples.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'updates namespace statistics' do + let(:namespace_statistics_name) { described_class.namespace_statistics_name } + let(:statistic_attribute) { described_class.statistic_attribute } + + context 'when creating' do + before do + statistic_source.send("#{statistic_attribute}=", 10) + end + + it 'schedules a statistic refresh' do + expect(Groups::UpdateStatisticsWorker) + .to receive(:perform_async) + + statistic_source.save! + end + end + + context 'when updating' do + before do + statistic_source.save! + + expect(statistic_source).to be_persisted + end + + context 'when the statistic attribute has not changed' do + it 'does not schedule a statistic refresh' do + expect(Groups::UpdateStatisticsWorker) + .not_to receive(:perform_async) + + statistic_source.update!(file_name: 'new-file-name.txt') + end + end + + context 'when the statistic attribute has changed' do + it 'schedules a statistic refresh' do + expect(Groups::UpdateStatisticsWorker) + .to receive(:perform_async) + + statistic_source.update!(statistic_attribute => 20) + end + end + end + + context 'when deleting' do + it 'schedules a statistic refresh' do + expect(Groups::UpdateStatisticsWorker) + .to receive(:perform_async) + + statistic_source.destroy! + end + end +end diff --git a/spec/support/shared_examples/models/issuable_link_shared_examples.rb b/spec/support/shared_examples/models/issuable_link_shared_examples.rb new file mode 100644 index 00000000000..ca98c2597a2 --- /dev/null +++ b/spec/support/shared_examples/models/issuable_link_shared_examples.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +# This shared example requires the following variables +# issuable_link +# issuable +# issuable_class +# issuable_link_factory +RSpec.shared_examples 'issuable link' do + describe 'Associations' do + it { is_expected.to belong_to(:source).class_name(issuable.class.name) } + it { is_expected.to belong_to(:target).class_name(issuable.class.name) } + end + + describe 'Validation' do + subject { issuable_link } + + it { is_expected.to validate_presence_of(:source) } + it { is_expected.to validate_presence_of(:target) } + it do + is_expected.to validate_uniqueness_of(:source) + .scoped_to(:target_id) + .with_message(/already related/) + end + + it 'is not valid if an opposite link already exists' do + issuable_link = create_issuable_link(subject.target, subject.source) + + expect(issuable_link).to be_invalid + expect(issuable_link.errors[:source]).to include("is already related to this #{issuable.class.name.downcase}") + end + + context 'when it relates to itself' do + context 'when target is nil' do + it 'does not invalidate object with self relation error' do + issuable_link = create_issuable_link(issuable, nil) + + issuable_link.valid? + + expect(issuable_link.errors[:source]).to be_empty + end + end + + context 'when source and target are present' do + it 'invalidates object' do + issuable_link = create_issuable_link(issuable, issuable) + + expect(issuable_link).to be_invalid + expect(issuable_link.errors[:source]).to include('cannot be related to itself') + end + end + end + + def create_issuable_link(source, target) + build(issuable_link_factory, source: source, target: target) + end + end + + describe '.link_type' do + it { is_expected.to define_enum_for(:link_type).with_values(relates_to: 0, blocks: 1) } + + it 'provides the "related" as default link_type' do + expect(issuable_link.link_type).to eq 'relates_to' + end + end +end diff --git a/spec/support/shared_examples/models/member_shared_examples.rb b/spec/support/shared_examples/models/member_shared_examples.rb index f7e09cfca62..17026f085bb 100644 --- a/spec/support/shared_examples/models/member_shared_examples.rb +++ b/spec/support/shared_examples/models/member_shared_examples.rb @@ -371,8 +371,7 @@ RSpec.shared_examples_for "bulk member creation" do it 'returns a Member objects' do members = described_class.add_users(source, [user1, user2], :maintainer) - expect(members).to be_a Array - expect(members.size).to eq(2) + expect(members.map(&:user)).to contain_exactly(user1, user2) expect(members).to all(be_a(member_type)) expect(members).to all(be_persisted) end @@ -394,20 +393,18 @@ RSpec.shared_examples_for "bulk member creation" do end context 'with de-duplication' do - it 'with the same user by id and user' do + it 'has the same user by id and user' do members = described_class.add_users(source, [user1.id, user1, user1.id, user2, user2.id, user2], :maintainer) - expect(members).to be_a Array - expect(members.size).to eq(2) + expect(members.map(&:user)).to contain_exactly(user1, user2) expect(members).to all(be_a(member_type)) expect(members).to all(be_persisted) end - it 'with the same user sent more than once' do + it 'has the same user sent more than once' do members = described_class.add_users(source, [user1, user1], :maintainer) - expect(members).to be_a Array - expect(members.size).to eq(1) + expect(members.map(&:user)).to contain_exactly(user1) expect(members).to all(be_a(member_type)) expect(members).to all(be_persisted) end @@ -418,15 +415,35 @@ RSpec.shared_examples_for "bulk member creation" do source.add_user(user1, :developer) end - it 'supports existing users as expected' do + it 'has the same user sent more than once with the member already existing' do + expect do + members = described_class.add_users(source, [user1, user1, user2], :maintainer) + expect(members.map(&:user)).to contain_exactly(user1, user2) + expect(members).to all(be_a(member_type)) + expect(members).to all(be_persisted) + end.to change { Member.count }.by(1) + end + + it 'supports existing users as expected with user_ids passed' do user3 = create(:user) - members = described_class.add_users(source, [user1.id, user2, user3.id], :maintainer) + expect do + members = described_class.add_users(source, [user1.id, user2, user3.id], :maintainer) + expect(members.map(&:user)).to contain_exactly(user1, user2, user3) + expect(members).to all(be_a(member_type)) + expect(members).to all(be_persisted) + end.to change { Member.count }.by(2) + end + + it 'supports existing users as expected without user ids passed' do + user3 = create(:user) - expect(members).to be_a Array - expect(members.size).to eq(3) - expect(members).to all(be_a(member_type)) - expect(members).to all(be_persisted) + expect do + members = described_class.add_users(source, [user1, user2, user3], :maintainer) + expect(members.map(&:user)).to contain_exactly(user1, user2, user3) + expect(members).to all(be_a(member_type)) + expect(members).to all(be_persisted) + end.to change { Member.count }.by(2) end end diff --git a/spec/support/shared_examples/models/resource_event_shared_examples.rb b/spec/support/shared_examples/models/resource_event_shared_examples.rb index c0158f9b24b..80806ee768a 100644 --- a/spec/support/shared_examples/models/resource_event_shared_examples.rb +++ b/spec/support/shared_examples/models/resource_event_shared_examples.rb @@ -62,15 +62,15 @@ RSpec.shared_examples 'a resource event for issues' do let_it_be(:issue2) { create(:issue, author: user1) } let_it_be(:issue3) { create(:issue, author: user2) } + let_it_be(:event1) { create(described_class.name.underscore.to_sym, issue: issue1) } + let_it_be(:event2) { create(described_class.name.underscore.to_sym, issue: issue2) } + let_it_be(:event3) { create(described_class.name.underscore.to_sym, issue: issue1) } + describe 'associations' do it { is_expected.to belong_to(:issue) } end describe '.by_issue' do - let_it_be(:event1) { create(described_class.name.underscore.to_sym, issue: issue1) } - let_it_be(:event2) { create(described_class.name.underscore.to_sym, issue: issue2) } - let_it_be(:event3) { create(described_class.name.underscore.to_sym, issue: issue1) } - it 'returns the expected records for an issue with events' do events = described_class.by_issue(issue1) @@ -84,21 +84,29 @@ RSpec.shared_examples 'a resource event for issues' do end end - describe '.by_issue_ids_and_created_at_earlier_or_equal_to' do + describe '.by_issue_ids' do + it 'returns the expected events' do + events = described_class.by_issue_ids([issue1.id]) + + expect(events).to contain_exactly(event1, event3) + end + end + + describe '.by_created_at_earlier_or_equal_to' do let_it_be(:event1) { create(described_class.name.underscore.to_sym, issue: issue1, created_at: '2020-03-10') } let_it_be(:event2) { create(described_class.name.underscore.to_sym, issue: issue2, created_at: '2020-03-10') } let_it_be(:event3) { create(described_class.name.underscore.to_sym, issue: issue1, created_at: '2020-03-12') } - it 'returns the expected records for an issue with events' do - events = described_class.by_issue_ids_and_created_at_earlier_or_equal_to([issue1.id, issue2.id], '2020-03-11 23:59:59') + it 'returns the expected events' do + events = described_class.by_created_at_earlier_or_equal_to('2020-03-11 23:59:59') expect(events).to contain_exactly(event1, event2) end - it 'returns the expected records for an issue with no events' do - events = described_class.by_issue_ids_and_created_at_earlier_or_equal_to(issue3, '2020-03-12') + it 'returns the expected events' do + events = described_class.by_created_at_earlier_or_equal_to('2020-03-12') - expect(events).to be_empty + expect(events).to contain_exactly(event1, event2, event3) end end diff --git a/spec/support/shared_examples/models/runners_token_prefix_shared_examples.rb b/spec/support/shared_examples/models/runners_token_prefix_shared_examples.rb deleted file mode 100644 index 4dce445ac73..00000000000 --- a/spec/support/shared_examples/models/runners_token_prefix_shared_examples.rb +++ /dev/null @@ -1,13 +0,0 @@ -# frozen_string_literal: true - -RSpec.shared_examples 'it has a prefixable runners_token' do - describe '#runners_token' do - it 'has a runners_token_prefix' do - expect(subject.runners_token_prefix).not_to be_empty - end - - it 'starts with the runners_token_prefix' do - expect(subject.runners_token).to start_with(subject.runners_token_prefix) - end - end -end diff --git a/spec/support/shared_examples/models/wiki_shared_examples.rb b/spec/support/shared_examples/models/wiki_shared_examples.rb index bc5956e3eec..b3f79d9fe6e 100644 --- a/spec/support/shared_examples/models/wiki_shared_examples.rb +++ b/spec/support/shared_examples/models/wiki_shared_examples.rb @@ -599,36 +599,13 @@ RSpec.shared_examples 'wiki model' do context 'when repository is empty' do let(:wiki_container) { wiki_container_without_repo } - it 'changes the HEAD reference to the default branch' do - wiki.repository.create_if_not_exists - wiki.repository.raw_repository.write_ref('HEAD', 'refs/heads/bar') + it 'creates the repository with the default branch' do + wiki.repository.create_if_not_exists(default_branch) subject expect(File.read(head_path).squish).to eq "ref: refs/heads/#{default_branch}" end end - - context 'when repository is not empty' do - before do - wiki.create_page('index', 'test content') - end - - it 'does nothing when HEAD points to the right branch' do - expect(wiki.repository.raw_repository).not_to receive(:write_ref) - - subject - end - - context 'when HEAD points to the wrong branch' do - it 'rewrites HEAD with the right branch' do - wiki.repository.raw_repository.write_ref('HEAD', 'refs/heads/bar') - - subject - - expect(File.read(head_path).squish).to eq "ref: refs/heads/#{default_branch}" - end - end - end end end diff --git a/spec/support/shared_examples/namespaces/traversal_scope_examples.rb b/spec/support/shared_examples/namespaces/traversal_scope_examples.rb index bcb5464ed5b..f1ace9878e9 100644 --- a/spec/support/shared_examples/namespaces/traversal_scope_examples.rb +++ b/spec/support/shared_examples/namespaces/traversal_scope_examples.rb @@ -90,7 +90,7 @@ RSpec.shared_examples 'namespace traversal scopes' do it_behaves_like '.roots' - it 'make recursive queries' do + it 'makes recursive queries' do expect { described_class.where(id: [nested_group_1]).roots.load }.to make_queries_matching(/WITH RECURSIVE/) end end @@ -126,7 +126,7 @@ RSpec.shared_examples 'namespace traversal scopes' do end context 'with offset and limit' do - subject { described_class.where(id: [deep_nested_group_1, deep_nested_group_2]).offset(1).limit(1).self_and_ancestors } + subject { described_class.where(id: [deep_nested_group_1, deep_nested_group_2]).order(:traversal_ids).offset(1).limit(1).self_and_ancestors } it { is_expected.to contain_exactly(group_2, nested_group_2, deep_nested_group_2) } end @@ -159,7 +159,7 @@ RSpec.shared_examples 'namespace traversal scopes' do it_behaves_like '.self_and_ancestors' - it 'make recursive queries' do + it 'makes recursive queries' do expect { described_class.where(id: [nested_group_1]).self_and_ancestors.load }.to make_queries_matching(/WITH RECURSIVE/) end end @@ -185,6 +185,7 @@ RSpec.shared_examples 'namespace traversal scopes' do subject do described_class .where(id: [deep_nested_group_1, deep_nested_group_2]) + .order(:traversal_ids) .limit(1) .offset(1) .self_and_ancestor_ids @@ -204,7 +205,7 @@ RSpec.shared_examples 'namespace traversal scopes' do it_behaves_like '.self_and_ancestor_ids' - it 'make recursive queries' do + it 'makes recursive queries' do expect { described_class.where(id: [nested_group_1]).self_and_ancestor_ids.load }.not_to make_queries_matching(/WITH RECURSIVE/) end end @@ -216,7 +217,7 @@ RSpec.shared_examples 'namespace traversal scopes' do it_behaves_like '.self_and_ancestor_ids' - it 'make recursive queries' do + it 'makes recursive queries' do expect { described_class.where(id: [nested_group_1]).self_and_ancestor_ids.load }.to make_queries_matching(/WITH RECURSIVE/) end end @@ -240,10 +241,20 @@ RSpec.shared_examples 'namespace traversal scopes' do end context 'with offset and limit' do - subject { described_class.where(id: [group_1, group_2]).offset(1).limit(1).self_and_descendants } + subject { described_class.where(id: [group_1, group_2]).order(:traversal_ids).offset(1).limit(1).self_and_descendants } it { is_expected.to contain_exactly(group_2, nested_group_2, deep_nested_group_2) } end + + context 'with nested query groups' do + let!(:nested_group_1b) { create(:group, parent: group_1) } + let!(:deep_nested_group_1b) { create(:group, parent: nested_group_1b) } + let(:group1_hierarchy) { [group_1, nested_group_1, deep_nested_group_1, nested_group_1b, deep_nested_group_1b] } + + subject { described_class.where(id: [group_1, nested_group_1]).self_and_descendants } + + it { is_expected.to match_array group1_hierarchy } + end end describe '.self_and_descendants' do @@ -278,6 +289,7 @@ RSpec.shared_examples 'namespace traversal scopes' do subject do described_class .where(id: [group_1, group_2]) + .order(:traversal_ids) .limit(1) .offset(1) .self_and_descendant_ids @@ -340,7 +352,7 @@ RSpec.shared_examples 'namespace traversal scopes' do it_behaves_like '.self_and_hierarchy' - it 'make recursive queries' do + it 'makes recursive queries' do base_groups = Group.where(id: nested_group_1) expect { base_groups.self_and_hierarchy.load }.to make_queries_matching(/WITH RECURSIVE/) end diff --git a/spec/support/shared_examples/requests/api/conan_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/conan_packages_shared_examples.rb index b30c4186f0d..82c34f0d6ad 100644 --- a/spec/support/shared_examples/requests/api/conan_packages_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/conan_packages_shared_examples.rb @@ -178,6 +178,25 @@ RSpec.shared_examples 'rejects invalid recipe' do end end +RSpec.shared_examples 'handling validation error for package' do + context 'with validation error' do + before do + allow_next_instance_of(Packages::Package) do |instance| + instance.errors.add(:base, 'validation error') + + allow(instance).to receive(:valid?).and_return(false) + end + end + + it 'returns 400' do + subject + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['message']).to include('Validation failed') + end + end +end + RSpec.shared_examples 'handling empty values for username and channel' do using RSpec::Parameterized::TableSyntax @@ -678,6 +697,7 @@ RSpec.shared_examples 'workhorse recipe file upload endpoint' do it_behaves_like 'uploads a package file' it_behaves_like 'creates build_info when there is a job' it_behaves_like 'handling empty values for username and channel' + it_behaves_like 'handling validation error for package' end RSpec.shared_examples 'workhorse package file upload endpoint' do @@ -700,6 +720,7 @@ RSpec.shared_examples 'workhorse package file upload endpoint' do it_behaves_like 'uploads a package file' it_behaves_like 'creates build_info when there is a job' it_behaves_like 'handling empty values for username and channel' + it_behaves_like 'handling validation error for package' context 'tracking the conan_package.tgz upload' do let(:file_name) { ::Packages::Conan::FileMetadatum::PACKAGE_BINARY } diff --git a/spec/support/shared_examples/requests/api/time_tracking_shared_examples.rb b/spec/support/shared_examples/requests/api/time_tracking_shared_examples.rb index 104e91add8b..381583ff2a9 100644 --- a/spec/support/shared_examples/requests/api/time_tracking_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/time_tracking_shared_examples.rb @@ -86,7 +86,7 @@ RSpec.shared_examples 'time tracking endpoints' do |issuable_name| end it "add spent time for #{issuable_name}" do - Timecop.travel(1.minute.from_now) do + travel_to(2.minutes.from_now) do expect do post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/add_spent_time", user), params: { duration: '2h' } end.to change { issuable.reload.updated_at } @@ -98,7 +98,7 @@ RSpec.shared_examples 'time tracking endpoints' do |issuable_name| context 'when subtracting time' do it 'subtracts time of the total spent time' do - Timecop.travel(1.minute.from_now) do + travel_to(2.minutes.from_now) do expect do issuable.update!(spend_time: { duration: 7200, user_id: user.id }) end.to change { issuable.reload.updated_at } @@ -115,7 +115,7 @@ RSpec.shared_examples 'time tracking endpoints' do |issuable_name| it 'does not modify the total time spent' do issuable.update!(spend_time: { duration: 7200, user_id: user.id }) - Timecop.travel(1.minute.from_now) do + travel_to(2.minutes.from_now) do expect do post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/add_spent_time", user), params: { duration: '-1w' } end.not_to change { issuable.reload.updated_at } @@ -160,7 +160,7 @@ RSpec.shared_examples 'time tracking endpoints' do |issuable_name| end it "resets spent time for #{issuable_name}" do - Timecop.travel(1.minute.from_now) do + travel_to(2.minutes.from_now) do expect do post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/reset_spent_time", user) end.to change { issuable.reload.updated_at } diff --git a/spec/support/shared_examples/requests/clusters/certificate_based_clusters_feature_flag_shared_examples.rb b/spec/support/shared_examples/requests/clusters/certificate_based_clusters_feature_flag_shared_examples.rb new file mode 100644 index 00000000000..24d90bde814 --- /dev/null +++ b/spec/support/shared_examples/requests/clusters/certificate_based_clusters_feature_flag_shared_examples.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +RSpec.shared_examples ':certificate_based_clusters feature flag API responses' do + context 'feature flag is disabled' do + before do + stub_feature_flags(certificate_based_clusters: false) + end + + it 'responds with :not_found' do + subject + + expect(response).to have_gitlab_http_status(:not_found) + end + end +end diff --git a/spec/support/shared_examples/row_lock_shared_examples.rb b/spec/support/shared_examples/row_lock_shared_examples.rb index 5e003172215..24fb2d41bdf 100644 --- a/spec/support/shared_examples/row_lock_shared_examples.rb +++ b/spec/support/shared_examples/row_lock_shared_examples.rb @@ -7,7 +7,7 @@ RSpec.shared_examples 'locked row' do it "has locked row" do table_name = row.class.table_name - ids_regex = /SELECT.*FROM.*#{table_name}.*"#{table_name}"."id" = #{row.id}.+FOR UPDATE/m + ids_regex = /SELECT.*FROM.*#{table_name}.*"#{table_name}"."id" = #{row.id}.+FOR NO KEY UPDATE/m expect(recorded_queries.log).to include a_string_matching 'SAVEPOINT' expect(recorded_queries.log).to include a_string_matching ids_regex diff --git a/spec/support/shared_examples/sends_git_audit_streaming_event_shared_examples.rb b/spec/support/shared_examples/sends_git_audit_streaming_event_shared_examples.rb new file mode 100644 index 00000000000..2c2be0152a0 --- /dev/null +++ b/spec/support/shared_examples/sends_git_audit_streaming_event_shared_examples.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'sends git audit streaming event' do + let_it_be(:user) { create(:user) } + + before do + stub_licensed_features(external_audit_events: true) + end + + subject {} + + context 'for public groups and projects' do + let(:group) { create(:group, :public) } + let(:project) { create(:project, :public, :repository, namespace: group) } + + before do + group.external_audit_event_destinations.create!(destination_url: 'http://example.com') + project.add_developer(user) + end + + context 'when user not logged in' do + let(:key) { create(:key) } + + before do + if request + request.headers.merge! auth_env(user.username, nil, nil) + end + end + it 'sends the audit streaming event' do + expect(AuditEvents::AuditEventStreamingWorker).not_to receive(:perform_async) + subject + end + end + end + + context 'for private groups and projects' do + let(:group) { create(:group, :private) } + let(:project) { create(:project, :private, :repository, namespace: group) } + + before do + group.external_audit_event_destinations.create!(destination_url: 'http://example.com') + project.add_developer(user) + sign_in(user) + end + + context 'when user logged in' do + let(:key) { create(:key, user: user) } + + before do + if request + password = user.try(:password) || user.try(:token) + request.headers.merge! auth_env(user.username, password, nil) + end + end + it 'sends the audit streaming event' do + expect(AuditEvents::AuditEventStreamingWorker).to receive(:perform_async).once + subject + end + end + end +end diff --git a/spec/support/shared_examples/serializers/environment_serializer_shared_examples.rb b/spec/support/shared_examples/serializers/environment_serializer_shared_examples.rb index 9d7ae6bcb3d..87a33060435 100644 --- a/spec/support/shared_examples/serializers/environment_serializer_shared_examples.rb +++ b/spec/support/shared_examples/serializers/environment_serializer_shared_examples.rb @@ -1,13 +1,19 @@ # frozen_string_literal: true -RSpec.shared_examples 'avoid N+1 on environments serialization' do +RSpec.shared_examples 'avoid N+1 on environments serialization' do |ee: false| + # Investigating in https://gitlab.com/gitlab-org/gitlab/-/issues/353209 + let(:query_threshold) { 1 + (ee ? 4 : 0) } + it 'avoids N+1 database queries with grouping', :request_store do create_environment_with_associations(project) control = ActiveRecord::QueryRecorder.new { serialize(grouping: true) } create_environment_with_associations(project) + create_environment_with_associations(project) - expect { serialize(grouping: true) }.not_to exceed_query_limit(control.count) + expect { serialize(grouping: true) } + .not_to exceed_query_limit(control.count) + .with_threshold(query_threshold) end it 'avoids N+1 database queries without grouping', :request_store do @@ -16,8 +22,11 @@ RSpec.shared_examples 'avoid N+1 on environments serialization' do control = ActiveRecord::QueryRecorder.new { serialize(grouping: false) } create_environment_with_associations(project) + create_environment_with_associations(project) - expect { serialize(grouping: false) }.not_to exceed_query_limit(control.count) + expect { serialize(grouping: false) } + .not_to exceed_query_limit(control.count) + .with_threshold(query_threshold) end it 'does not preload for environments that does not exist in the page', :request_store do @@ -35,7 +44,7 @@ RSpec.shared_examples 'avoid N+1 on environments serialization' do end def serialize(grouping:, query: nil) - query ||= { page: 1, per_page: 1 } + query ||= { page: 1, per_page: 20 } request = double(url: "#{Gitlab.config.gitlab.url}:8080/api/v4/projects?#{query.to_query}", query_parameters: query) EnvironmentSerializer.new(current_user: user, project: project).yield_self do |serializer| diff --git a/spec/support/shared_examples/serializers/note_entity_shared_examples.rb b/spec/support/shared_examples/serializers/note_entity_shared_examples.rb index 9af6ec45e49..2e557ca090c 100644 --- a/spec/support/shared_examples/serializers/note_entity_shared_examples.rb +++ b/spec/support/shared_examples/serializers/note_entity_shared_examples.rb @@ -68,6 +68,29 @@ RSpec.shared_examples 'note entity' do end end + describe ':outdated_line_change_path' do + before do + allow(note).to receive(:show_outdated_changes?).and_return(show_outdated_changes) + end + + context 'when note shows outdated changes' do + let(:show_outdated_changes) { true } + + it 'returns correct outdated_line_change_namespace_project_note_path' do + path = "/#{note.project.namespace.path}/#{note.project.path}/notes/#{note.id}/outdated_line_change" + expect(subject[:outdated_line_change_path]).to eq(path) + end + end + + context 'when note does not show outdated changes' do + let(:show_outdated_changes) { false } + + it 'does not expose outdated_line_change_path' do + expect(subject).not_to include(:outdated_line_change_path) + end + end + end + context 'when note was edited' do before do note.update!(updated_at: 1.minute.from_now, updated_by: user) diff --git a/spec/support/shared_examples/services/container_registry_auth_service_shared_examples.rb b/spec/support/shared_examples/services/container_registry_auth_service_shared_examples.rb index c808b9a5318..a780952d51b 100644 --- a/spec/support/shared_examples/services/container_registry_auth_service_shared_examples.rb +++ b/spec/support/shared_examples/services/container_registry_auth_service_shared_examples.rb @@ -69,10 +69,6 @@ RSpec.shared_examples 'a browsable' do end RSpec.shared_examples 'an accessible' do - before do - stub_feature_flags(container_registry_migration_phase1: false) - end - let(:access) do [{ 'type' => 'repository', 'name' => project.full_path, @@ -161,10 +157,6 @@ end RSpec.shared_examples 'a container registry auth service' do include_context 'container registry auth service context' - before do - stub_feature_flags(container_registry_migration_phase1: false) - end - describe '.full_access_token' do let_it_be(:project) { create(:project) } diff --git a/spec/support/shared_examples/services/incident_shared_examples.rb b/spec/support/shared_examples/services/incident_shared_examples.rb index cc26cf87322..b533b095aac 100644 --- a/spec/support/shared_examples/services/incident_shared_examples.rb +++ b/spec/support/shared_examples/services/incident_shared_examples.rb @@ -70,7 +70,7 @@ RSpec.shared_examples 'incident management label service' do expect(execute).to be_success expect(execute.payload).to eq(label: label) expect(label.title).to eq(title) - expect(label.color).to eq(color) + expect(label.color).to be_color(color) expect(label.description).to eq(description) end end diff --git a/spec/support/shared_examples/services/issuable_links/create_links_shared_examples.rb b/spec/support/shared_examples/services/issuable_links/create_links_shared_examples.rb new file mode 100644 index 00000000000..6146aae6b9b --- /dev/null +++ b/spec/support/shared_examples/services/issuable_links/create_links_shared_examples.rb @@ -0,0 +1,133 @@ +# frozen_string_literal: true + +shared_examples 'issuable link creation' do + describe '#execute' do + subject { described_class.new(issuable, user, params).execute } + + context 'when the reference list is empty' do + let(:params) do + { issuable_references: [] } + end + + it 'returns error' do + is_expected.to eq(message: "No matching #{issuable_type} found. Make sure that you are adding a valid #{issuable_type} URL.", status: :error, http_status: 404) + end + end + + context 'when Issuable not found' do + let(:params) do + { issuable_references: ["##{non_existing_record_iid}"] } + end + + it 'returns error' do + is_expected.to eq(message: "No matching #{issuable_type} found. Make sure that you are adding a valid #{issuable_type} URL.", status: :error, http_status: 404) + end + + it 'no relationship is created' do + expect { subject }.not_to change(issuable_link_class, :count) + end + end + + context 'when user has no permission to target issuable' do + let(:params) do + { issuable_references: [guest_issuable.to_reference(issuable_parent)] } + end + + it 'returns error' do + is_expected.to eq(message: "No matching #{issuable_type} found. Make sure that you are adding a valid #{issuable_type} URL.", status: :error, http_status: 404) + end + + it 'no relationship is created' do + expect { subject }.not_to change(issuable_link_class, :count) + end + end + + context 'source and target are the same issuable' do + let(:params) do + { issuable_references: [issuable.to_reference] } + end + + it 'does not create notes' do + expect(SystemNoteService).not_to receive(:relate_issuable) + + subject + end + + it 'no relationship is created' do + expect { subject }.not_to change(issuable_link_class, :count) + end + end + + context 'when there is an issuable to relate' do + let(:params) do + { issuable_references: [issuable2.to_reference, issuable3.to_reference(issuable_parent)] } + end + + it 'creates relationships' do + expect { subject }.to change(issuable_link_class, :count).by(2) + + expect(issuable_link_class.find_by!(target: issuable2)).to have_attributes(source: issuable, link_type: 'relates_to') + expect(issuable_link_class.find_by!(target: issuable3)).to have_attributes(source: issuable, link_type: 'relates_to') + end + + it 'returns success status' do + is_expected.to eq(status: :success) + end + + it 'creates notes' do + # First two-way relation notes + expect(SystemNoteService).to receive(:relate_issuable) + .with(issuable, issuable2, user) + expect(SystemNoteService).to receive(:relate_issuable) + .with(issuable2, issuable, user) + + # Second two-way relation notes + expect(SystemNoteService).to receive(:relate_issuable) + .with(issuable, issuable3, user) + expect(SystemNoteService).to receive(:relate_issuable) + .with(issuable3, issuable, user) + + subject + end + end + + context 'when reference of any already related issue is present' do + let(:params) do + { + issuable_references: [ + issuable_a.to_reference, + issuable_b.to_reference + ], + link_type: IssueLink::TYPE_RELATES_TO + } + end + + it 'creates notes only for new relations' do + expect(SystemNoteService).to receive(:relate_issuable).with(issuable, issuable_a, anything) + expect(SystemNoteService).to receive(:relate_issuable).with(issuable_a, issuable, anything) + expect(SystemNoteService).not_to receive(:relate_issuable).with(issuable, issuable_b, anything) + expect(SystemNoteService).not_to receive(:relate_issuable).with(issuable_b, issuable, anything) + + subject + end + end + + context 'when there are invalid references' do + let(:params) do + { issuable_references: [issuable.to_reference, issuable_a.to_reference] } + end + + it 'creates links only for valid references' do + expect { subject }.to change { issuable_link_class.count }.by(1) + end + + it 'returns error status' do + expect(subject).to eq( + status: :error, + http_status: 422, + message: "#{issuable.to_reference} cannot be added: cannot be related to itself" + ) + end + end + end +end 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 new file mode 100644 index 00000000000..53d637a9094 --- /dev/null +++ b/spec/support/shared_examples/services/issuable_links/destroyable_issuable_links_shared_examples.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +shared_examples 'a destroyable issuable link' do + context 'when successfully removes an issuable link' do + before do + issuable_link.source.resource_parent.add_reporter(user) + issuable_link.target.resource_parent.add_reporter(user) + end + + it 'removes related issue' do + expect { subject }.to change(issuable_link.class, :count).by(-1) + end + + it 'creates notes' do + # Two-way notes creation + expect(SystemNoteService).to receive(:unrelate_issuable) + .with(issuable_link.source, issuable_link.target, user) + expect(SystemNoteService).to receive(:unrelate_issuable) + .with(issuable_link.target, issuable_link.source, user) + + subject + end + + it 'returns success message' do + is_expected.to eq(message: 'Relation was removed', status: :success) + end + end + + context 'when failing to remove an issuable link' do + it 'does not remove relation' do + expect { subject }.not_to change(issuable_link.class, :count).from(1) + end + + it 'does not create notes' do + expect(SystemNoteService).not_to receive(:unrelate_issuable) + end + + it 'returns error message' do + is_expected.to eq(message: "No #{issuable_link.class.model_name.human.titleize} found", status: :error, http_status: 404) + end + end +end diff --git a/spec/support/shared_examples/services/rate_limited_service_shared_examples.rb b/spec/support/shared_examples/services/rate_limited_service_shared_examples.rb new file mode 100644 index 00000000000..b79f1a332a6 --- /dev/null +++ b/spec/support/shared_examples/services/rate_limited_service_shared_examples.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true + +# shared examples for testing rate limited functionality of a service +# +# following resources are expected to be set (example): +# it_behaves_like 'rate limited service' do +# let(:key) { :issues_create } +# let(:key_scope) { %i[project current_user external_author] } +# let(:application_limit_key) { :issues_create_limit } +# let(:service) { described_class.new(project: project, current_user: user, params: { title: 'title' }, spam_params: double) } +# let(:created_model) { Issue } +# end + +RSpec.shared_examples 'rate limited service' do + describe '.rate_limiter_scoped_and_keyed' do + it 'is set via the rate_limit call' do + expect(described_class.rate_limiter_scoped_and_keyed).to be_a(RateLimitedService::RateLimiterScopedAndKeyed) + + expect(described_class.rate_limiter_scoped_and_keyed.key).to eq(key) + expect(described_class.rate_limiter_scoped_and_keyed.opts[:scope]).to eq(key_scope) + expect(described_class.rate_limiter_scoped_and_keyed.rate_limiter).to eq(Gitlab::ApplicationRateLimiter) + end + end + + describe '#rate_limiter_bypassed' do + it 'is nil by default' do + expect(service.rate_limiter_bypassed).to be_nil + end + end + + describe '#execute' do + before do + stub_spam_services + end + + context 'when rate limiting is in effect', :freeze_time, :clean_gitlab_redis_rate_limiting do + let(:user) { create(:user) } + + before do + stub_application_setting(application_limit_key => 1) + end + + subject do + 2.times { service.execute } + end + + context 'when too many requests are sent by one user' do + it 'raises an error' do + expect do + subject + end.to raise_error(RateLimitedService::RateLimitedError) + end + + it 'creates 1 issue' do + expect do + subject + rescue RateLimitedService::RateLimitedError + end.to change { created_model.count }.by(1) + end + end + + context 'when limit is higher than count of issues being created' do + before do + stub_application_setting(issues_create_limit: 2) + end + + it 'creates 2 issues' do + expect { subject }.to change { created_model.count }.by(2) + end + end + end + end +end diff --git a/spec/support/shared_examples/services/security/ci_configuration/create_service_shared_examples.rb b/spec/support/shared_examples/services/security/ci_configuration/create_service_shared_examples.rb index 538fd2bb513..105c4247ff7 100644 --- a/spec/support/shared_examples/services/security/ci_configuration/create_service_shared_examples.rb +++ b/spec/support/shared_examples/services/security/ci_configuration/create_service_shared_examples.rb @@ -76,6 +76,18 @@ RSpec.shared_examples_for 'services security ci configuration create service' do end end + context 'when the project has a non-default ci config file' do + before do + project.ci_config_path = 'non-default/.gitlab-ci.yml' + end + + it 'does track the snowplow event' do + subject + + expect_snowplow_event(**snowplow_event) + end + end + unless skip_w_params context 'with parameters' do let(:params) { non_empty_params } 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 new file mode 100644 index 00000000000..d202c4e00f0 --- /dev/null +++ b/spec/support/shared_examples/workers/batched_background_migration_worker_shared_examples.rb @@ -0,0 +1,198 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'it runs batched background migration jobs' do |tracking_database| + include ExclusiveLeaseHelpers + + describe 'defining the job attributes' do + it 'defines the data_consistency as always' do + expect(described_class.get_data_consistency).to eq(:always) + end + + it 'defines the feature_category as database' do + expect(described_class.get_feature_category).to eq(:database) + end + + it 'defines the idempotency as true' do + expect(described_class.idempotent?).to be_truthy + end + end + + describe '.tracking_database' do + it 'does not raise an error' do + expect { described_class.tracking_database }.not_to raise_error + end + + it 'overrides the method to return the tracking database' do + expect(described_class.tracking_database).to eq(tracking_database) + end + end + + describe '.lease_key' do + let(:lease_key) { described_class.name.demodulize.underscore } + + it 'does not raise an error' do + expect { described_class.lease_key }.not_to raise_error + end + + it 'returns the lease key' do + expect(described_class.lease_key).to eq(lease_key) + end + end + + describe '#perform' do + subject(:worker) { described_class.new } + + context 'when the base model does not exist' do + before do + if Gitlab::Database.has_config?(tracking_database) + skip "because the base model for #{tracking_database} exists" + end + end + + it 'does nothing' do + expect(worker).not_to receive(:active_migration) + expect(worker).not_to receive(:run_active_migration) + + expect { worker.perform }.not_to raise_error + end + + it 'logs a message indicating execution is skipped' do + 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 migration execution/) + end + + expect { worker.perform }.not_to raise_error + end + end + + context 'when the base model does exist' do + before do + unless Gitlab::Database.has_config?(tracking_database) + skip "because the base model for #{tracking_database} does not exist" + end + end + + context 'when the feature flag is disabled' do + before do + stub_feature_flags(execute_batched_migrations_on_schedule: false) + end + + it 'does nothing' do + expect(worker).not_to receive(:active_migration) + expect(worker).not_to receive(:run_active_migration) + + worker.perform + end + end + + context 'when the feature flag is enabled' do + before do + stub_feature_flags(execute_batched_migrations_on_schedule: true) + + allow(Gitlab::Database::BackgroundMigration::BatchedMigration).to receive(:active_migration).and_return(nil) + end + + context 'when no active migrations exist' do + it 'does nothing' do + expect(worker).not_to receive(:run_active_migration) + + worker.perform + end + end + + context 'when active migrations exist' do + let(:job_interval) { 5.minutes } + let(:lease_timeout) { 15.minutes } + let(:lease_key) { described_class.name.demodulize.underscore } + let(:migration) { build(:batched_background_migration, :active, interval: job_interval) } + let(:interval_variance) { described_class::INTERVAL_VARIANCE } + + before do + allow(Gitlab::Database::BackgroundMigration::BatchedMigration).to receive(:active_migration) + .and_return(migration) + + allow(migration).to receive(:interval_elapsed?).with(variance: interval_variance).and_return(true) + allow(migration).to receive(:reload) + end + + context 'when the reloaded migration is no longer active' do + it 'does not run the migration' do + expect_to_obtain_exclusive_lease(lease_key, timeout: lease_timeout) + + expect(migration).to receive(:reload) + expect(migration).to receive(:active?).and_return(false) + + expect(worker).not_to receive(:run_active_migration) + + worker.perform + end + end + + context 'when the interval has not elapsed' do + it 'does not run the migration' do + expect_to_obtain_exclusive_lease(lease_key, timeout: lease_timeout) + + expect(migration).to receive(:interval_elapsed?).with(variance: interval_variance).and_return(false) + + expect(worker).not_to receive(:run_active_migration) + + worker.perform + end + end + + context 'when the reloaded migration is still active and the interval has elapsed' do + it 'runs the migration' do + expect_to_obtain_exclusive_lease(lease_key, timeout: lease_timeout) + + expect_next_instance_of(Gitlab::Database::BackgroundMigration::BatchedMigrationRunner) do |instance| + expect(instance).to receive(:run_migration_job).with(migration) + end + + expect(worker).to receive(:run_active_migration).and_call_original + + worker.perform + end + end + + context 'when the calculated timeout is less than the minimum allowed' do + let(:minimum_timeout) { described_class::MINIMUM_LEASE_TIMEOUT } + let(:job_interval) { 2.minutes } + + it 'sets the lease timeout to the minimum value' do + expect_to_obtain_exclusive_lease(lease_key, timeout: minimum_timeout) + + expect_next_instance_of(Gitlab::Database::BackgroundMigration::BatchedMigrationRunner) do |instance| + expect(instance).to receive(:run_migration_job).with(migration) + end + + expect(worker).to receive(:run_active_migration).and_call_original + + worker.perform + end + end + + it 'always cleans up the exclusive lease' do + lease = stub_exclusive_lease_taken(lease_key, timeout: lease_timeout) + + expect(lease).to receive(:try_obtain).and_return(true) + + expect(worker).to receive(:run_active_migration).and_raise(RuntimeError, 'I broke') + expect(lease).to receive(:cancel) + + expect { worker.perform }.to raise_error(RuntimeError, 'I broke') + end + + it 'receives the correct connection' do + base_model = Gitlab::Database.database_base_models[tracking_database] + + expect(Gitlab::Database::SharedModel).to receive(:using_connection).with(base_model.connection).and_yield + + worker.perform + end + end + end + end + end +end diff --git a/spec/support/shared_examples/workers/concerns/git_garbage_collect_methods_shared_examples.rb b/spec/support/shared_examples/workers/concerns/git_garbage_collect_methods_shared_examples.rb index f2314793cb4..202606c6aa6 100644 --- a/spec/support/shared_examples/workers/concerns/git_garbage_collect_methods_shared_examples.rb +++ b/spec/support/shared_examples/workers/concerns/git_garbage_collect_methods_shared_examples.rb @@ -19,14 +19,28 @@ RSpec.shared_examples 'can collect git garbage' do |update_statistics: true| end shared_examples 'it calls Gitaly' do - specify do - repository_service = instance_double(Gitlab::GitalyClient::RepositoryService) + let(:repository_service) { instance_double(Gitlab::GitalyClient::RepositoryService) } - expect(subject).to receive(:get_gitaly_client).with(task, repository.raw_repository).and_return(repository_service) - expect(repository_service).to receive(gitaly_task) + specify do + expect_next_instance_of(Gitlab::GitalyClient::RepositoryService, repository.raw_repository) do |instance| + expect(instance).to receive(:optimize_repository).and_call_original + end subject.perform(*params) end + + context 'when optimized_housekeeping feature is disabled' do + before do + stub_feature_flags(optimized_housekeeping: false) + end + + specify do + expect(subject).to receive(:get_gitaly_client).with(task, repository.raw_repository).and_return(repository_service) + expect(repository_service).to receive(gitaly_task) + + subject.perform(*params) + end + end end shared_examples 'it updates the resource statistics' do @@ -70,12 +84,31 @@ RSpec.shared_examples 'can collect git garbage' do |update_statistics: true| end it 'handles gRPC errors' do - allow_next_instance_of(Gitlab::GitalyClient::RepositoryService, repository.raw_repository) do |instance| - allow(instance).to receive(:garbage_collect).and_raise(GRPC::NotFound) + repository_service = instance_double(Gitlab::GitalyClient::RepositoryService) + + allow_next_instance_of(Projects::GitDeduplicationService) do |instance| + allow(instance).to receive(:execute) end + allow(repository.raw_repository).to receive(:gitaly_repository_client).and_return(repository_service) + allow(repository_service).to receive(:optimize_repository).and_raise(GRPC::NotFound) + expect { subject.perform(*params) }.to raise_exception(Gitlab::Git::Repository::NoRepository) end + + context 'when optimized_housekeeping feature flag is disabled' do + before do + stub_feature_flags(optimized_housekeeping: false) + end + + it 'handles gRPC errors' do + allow_next_instance_of(Gitlab::GitalyClient::RepositoryService, repository.raw_repository) do |instance| + allow(instance).to receive(:garbage_collect).and_raise(GRPC::NotFound) + end + + expect { subject.perform(*params) }.to raise_exception(Gitlab::Git::Repository::NoRepository) + end + end end context 'with different lease than the active one' do @@ -152,13 +185,8 @@ RSpec.shared_examples 'can collect git garbage' do |update_statistics: true| expect(subject).to receive(:get_lease_uuid).and_return(lease_uuid) end - it 'calls Gitaly' do - repository_service = instance_double(Gitlab::GitalyClient::RefService) - - expect(subject).to receive(:get_gitaly_client).with(task, repository.raw_repository).and_return(repository_service) - expect(repository_service).to receive(gitaly_task) - - subject.perform(*params) + it_behaves_like 'it calls Gitaly' do + let(:repository_service) { instance_double(Gitlab::GitalyClient::RefService) } end it 'does not update the resource statistics' do @@ -180,10 +208,26 @@ RSpec.shared_examples 'can collect git garbage' do |update_statistics: true| it_behaves_like 'it updates the resource statistics' if update_statistics end + context 'prune' do + before do + expect(subject).to receive(:get_lease_uuid).and_return(lease_uuid) + end + + specify do + expect_next_instance_of(Gitlab::GitalyClient::RepositoryService, repository.raw_repository) do |instance| + expect(instance).to receive(:prune_unreachable_objects).and_call_original + end + + subject.perform(resource.id, 'prune', lease_key, lease_uuid) + end + end + shared_examples 'gc tasks' do before do allow(subject).to receive(:get_lease_uuid).and_return(lease_uuid) allow(subject).to receive(:bitmaps_enabled?).and_return(bitmaps_enabled) + + stub_feature_flags(optimized_housekeeping: false) end it 'incremental repack adds a new packfile' do diff --git a/spec/support/shared_examples/workers/concerns/reenqueuer_shared_examples.rb b/spec/support/shared_examples/workers/concerns/reenqueuer_shared_examples.rb index d6e96ef37d6..d9105981b4b 100644 --- a/spec/support/shared_examples/workers/concerns/reenqueuer_shared_examples.rb +++ b/spec/support/shared_examples/workers/concerns/reenqueuer_shared_examples.rb @@ -30,18 +30,11 @@ end # `job_args` to be arguments to #perform if it takes arguments RSpec.shared_examples '#perform is rate limited to 1 call per' do |minimum_duration| before do - # Allow Timecop freeze and travel without the block form - Timecop.safe_mode = false - Timecop.freeze + freeze_time time_travel_during_perform(actual_duration) end - after do - Timecop.return - Timecop.safe_mode = true - end - let(:subject_perform) { defined?(job_args) ? subject.perform(job_args) : subject.perform } context 'when the work finishes in 0 seconds' do @@ -58,7 +51,7 @@ RSpec.shared_examples '#perform is rate limited to 1 call per' do |minimum_durat let(:actual_duration) { 0.1 * minimum_duration } it 'sleeps 90% of minimum duration' do - expect(subject).to receive(:sleep).with(a_value_within(0.01).of(0.9 * minimum_duration)) + expect(subject).to receive(:sleep).with(a_value_within(1).of(0.9 * minimum_duration)) subject_perform end @@ -68,7 +61,7 @@ RSpec.shared_examples '#perform is rate limited to 1 call per' do |minimum_durat let(:actual_duration) { 0.9 * minimum_duration } it 'sleeps 10% of minimum duration' do - expect(subject).to receive(:sleep).with(a_value_within(0.01).of(0.1 * minimum_duration)) + expect(subject).to receive(:sleep).with(a_value_within(1).of(0.1 * minimum_duration)) subject_perform end @@ -111,7 +104,7 @@ RSpec.shared_examples '#perform is rate limited to 1 call per' do |minimum_durat allow(subject).to receive(:ensure_minimum_duration) do |minimum_duration, &block| original_ensure_minimum_duration.call(minimum_duration) do # Time travel inside the block inside ensure_minimum_duration - Timecop.travel(actual_duration) if actual_duration && actual_duration > 0 + travel_to(actual_duration.from_now) if actual_duration && actual_duration > 0 end end end diff --git a/spec/support/silence_stdout.rb b/spec/support/silence_stdout.rb new file mode 100644 index 00000000000..b2bc65c5cda --- /dev/null +++ b/spec/support/silence_stdout.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +RSpec.configure do |config| + # Allows stdout to be redirected to reduce noise + config.before(:each, :silence_stdout) do + $stdout = StringIO.new + end + + config.after(:each, :silence_stdout) do + $stdout = STDOUT + end +end diff --git a/spec/support/view_component.rb b/spec/support/view_component.rb new file mode 100644 index 00000000000..9166a06fc8c --- /dev/null +++ b/spec/support/view_component.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true +require 'view_component/test_helpers' + +RSpec.configure do |config| + config.include ViewComponent::TestHelpers, type: :component + config.include Capybara::RSpecMatchers, type: :component +end |