diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-11-17 14:33:21 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-11-17 14:33:21 +0300 |
commit | 7021455bd1ed7b125c55eb1b33c5a01f2bc55ee0 (patch) | |
tree | 5bdc2229f5198d516781f8d24eace62fc7e589e9 /spec/support | |
parent | 185b095e93520f96e9cfc31d9c3e69b498cdab7c (diff) |
Add latest changes from gitlab-org/gitlab@15-6-stable-eev15.6.0-rc42
Diffstat (limited to 'spec/support')
106 files changed, 2089 insertions, 596 deletions
diff --git a/spec/support/database/query_recorder.rb b/spec/support/database/query_recorder.rb new file mode 100644 index 00000000000..1050120e528 --- /dev/null +++ b/spec/support/database/query_recorder.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +RSpec.configure do |config| + # Truncate the query_recorder log file before starting the suite + config.before(:suite) do + log_path = Rails.root.join(Gitlab::Database::QueryAnalyzers::QueryRecorder::LOG_FILE) + File.write(log_path, '') if File.exist?(log_path) + end +end diff --git a/spec/support/gitlab/usage/metrics_instrumentation_shared_examples.rb b/spec/support/gitlab/usage/metrics_instrumentation_shared_examples.rb index e02bf66507a..e9a13f7bf63 100644 --- a/spec/support/gitlab/usage/metrics_instrumentation_shared_examples.rb +++ b/spec/support/gitlab/usage/metrics_instrumentation_shared_examples.rb @@ -10,8 +10,8 @@ RSpec.shared_examples 'a correct instrumented metric value' do |params| end before do - if described_class.respond_to?(:relation) && described_class.relation.respond_to?(:connection) - allow(described_class.relation.connection).to receive(:transaction_open?).and_return(false) + if metric.respond_to?(:relation, true) && metric.send(:relation).respond_to?(:connection) + allow(metric.send(:relation).connection).to receive(:transaction_open?).and_return(false) end end diff --git a/spec/support/google_api/cloud_platform_helpers.rb b/spec/support/google_api/cloud_platform_helpers.rb index 840f948e377..b9752577c76 100644 --- a/spec/support/google_api/cloud_platform_helpers.rb +++ b/spec/support/google_api/cloud_platform_helpers.rb @@ -157,7 +157,7 @@ module GoogleApi def cloud_platform_projects_billing_info_body(project_id, billing_enabled) { "name": "projects/#{project_id}/billingInfo", - "projectId": "#{project_id}", + "projectId": project_id.to_s, "billingAccountName": "account-name", "billingEnabled": billing_enabled } diff --git a/spec/support/helpers/bare_repo_operations.rb b/spec/support/helpers/bare_repo_operations.rb deleted file mode 100644 index e29e12a15f6..00000000000 --- a/spec/support/helpers/bare_repo_operations.rb +++ /dev/null @@ -1,44 +0,0 @@ -# frozen_string_literal: true - -require 'zlib' - -class BareRepoOperations - include Gitlab::Popen - - def initialize(path_to_repo) - @path_to_repo = path_to_repo - end - - def commit_tree(tree_id, msg, parent: Gitlab::Git::EMPTY_TREE_ID) - commit_tree_args = ['commit-tree', tree_id, '-m', msg] - commit_tree_args += ['-p', parent] unless parent == Gitlab::Git::EMPTY_TREE_ID - commit_id = execute(commit_tree_args) - - commit_id[0] - end - - private - - def execute(args, allow_failure: false) - output, status = popen(base_args + args, nil) do |stdin| - yield stdin if block_given? - end - - unless status == 0 - if allow_failure - return [] - else - raise "Got a non-zero exit code while calling out `#{args.join(' ')}`: #{output}" - end - end - - output.split("\n") - end - - def base_args - [ - Gitlab.config.git.bin_path, - "--git-dir=#{@path_to_repo}" - ] - end -end diff --git a/spec/support/helpers/ci/template_helpers.rb b/spec/support/helpers/ci/template_helpers.rb index 2e9b6f748cd..2cdd242ac22 100644 --- a/spec/support/helpers/ci/template_helpers.rb +++ b/spec/support/helpers/ci/template_helpers.rb @@ -5,6 +5,51 @@ module Ci def template_registry_host 'registry.gitlab.com' end + + def public_image_exist?(registry, repository, image) + public_image_manifest(registry, repository, image).present? + end + + def public_image_manifest(registry, repository, reference) + token = public_image_repository_token(registry, repository) + + response = with_net_connect_allowed do + Gitlab::HTTP.get(image_manifest_url(registry, repository, reference), + headers: { 'Authorization' => "Bearer #{token}" }) + end + + return unless response.success? + + Gitlab::Json.parse(response.body) + end + + def public_image_repository_token(registry, repository) + @public_image_repository_tokens ||= {} + @public_image_repository_tokens[[registry, repository]] ||= + begin + response = with_net_connect_allowed do + Gitlab::HTTP.get(image_manifest_url(registry, repository, 'latest')) + end + + return unless response.unauthorized? + + www_authenticate = response.headers['www-authenticate'] + return unless www_authenticate + + realm, service, scope = www_authenticate.split(',').map { |s| s[/\w+="(.*)"/, 1] } + token_response = with_net_connect_allowed do + Gitlab::HTTP.get(realm, query: { service: service, scope: scope }) + end + + return unless token_response.success? + + token_response['token'] + end + end + + def image_manifest_url(registry, repository, reference) + "#{registry}/v2/#{repository}/manifests/#{reference}" + end end end diff --git a/spec/support/helpers/content_security_policy_helpers.rb b/spec/support/helpers/content_security_policy_helpers.rb index c9f15e65c74..230075ead70 100644 --- a/spec/support/helpers/content_security_policy_helpers.rb +++ b/spec/support/helpers/content_security_policy_helpers.rb @@ -1,20 +1,14 @@ # 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) + # Expecting 2 calls to current_content_security_policy by default: + # 1. call that's being tested + # 2. call in ApplicationController + def setup_csp_for_controller(controller_class, csp = ActionDispatch::ContentSecurityPolicy.new, 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 + expect(controller) + .to receive(:current_content_security_policy).exactly(times).times + .and_return(csp) end end end diff --git a/spec/support/database/multiple_databases.rb b/spec/support/helpers/database/multiple_databases_helpers.rb index b6341c2caec..16f5168ca29 100644 --- a/spec/support/database/multiple_databases.rb +++ b/spec/support/helpers/database/multiple_databases_helpers.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module Database - module MultipleDatabases + module MultipleDatabasesHelpers def skip_if_multiple_databases_not_setup skip 'Skipping because multiple databases not set up' unless Gitlab::Database.has_config?(:ci) end @@ -52,17 +52,17 @@ module Database # # rubocop:disable Database/MultipleDatabases def with_reestablished_active_record_base(reconnect: true) - connection_classes = ActiveRecord::Base.connection_handler.connection_pool_names.map(&:constantize).to_h do |klass| - [klass, klass.connection_db_config] - end + connection_classes = ActiveRecord::Base + .connection_handler + .connection_pool_names + .map(&:constantize) + .index_with(&:connection_db_config) original_handler = ActiveRecord::Base.connection_handler new_handler = ActiveRecord::ConnectionAdapters::ConnectionHandler.new ActiveRecord::Base.connection_handler = new_handler - if reconnect - connection_classes.each { |klass, db_config| klass.establish_connection(db_config) } - end + connection_classes.each { |klass, db_config| klass.establish_connection(db_config) } if reconnect yield ensure @@ -95,9 +95,12 @@ module Database module ActiveRecordBaseEstablishConnection def establish_connection(*args) # rubocop:disable Database/MultipleDatabases - if connected? && connection&.transaction_open? && ActiveRecord::Base.connection_handler == ActiveRecord::Base.default_connection_handler - raise "Cannot re-establish '#{self}.establish_connection' within an open transaction (#{connection&.open_transactions.to_i}). " \ - "Use `with_reestablished_active_record_base` instead or add `:reestablished_active_record_base` to rspec context." + if connected? && + connection&.transaction_open? && + ActiveRecord::Base.connection_handler == ActiveRecord::Base.default_connection_handler + raise "Cannot re-establish '#{self}.establish_connection' within an open transaction " \ + "(#{connection&.open_transactions.to_i}). Use `with_reestablished_active_record_base` " \ + "instead or add `:reestablished_active_record_base` to rspec context." end # rubocop:enable Database/MultipleDatabases @@ -106,56 +109,4 @@ module Database end end -RSpec.configure do |config| - # Ensure database versions are memoized to prevent query counts from - # being affected by version checks. Note that - # Gitlab::Database.check_postgres_version_and_print_warning is called - # at startup, but that generates its own - # `Gitlab::Database::Reflection` so the result is not memoized by - # callers of `ApplicationRecord.database.version`, such as - # `Gitlab::Database::AsWithMaterialized.materialized_supported?`. - # TODO This can be removed once https://gitlab.com/gitlab-org/gitlab/-/issues/325639 is completed. - [ApplicationRecord, ::Ci::ApplicationRecord].each { |record| record.database.version } - - config.around(:each, :reestablished_active_record_base) do |example| - with_reestablished_active_record_base(reconnect: example.metadata.fetch(:reconnect, true)) do - example.run - end - end - - config.around(:each, :add_ci_connection) do |example| - with_added_ci_connection do - example.run - end - end - - config.append_after(:context, :migration) do - recreate_databases_and_seed_if_needed || ensure_schema_and_empty_tables - end - - config.around(:each, :migration) do |example| - self.class.use_transactional_tests = false - - migration_schema = example.metadata[:migration] - migration_schema = :gitlab_main if migration_schema == true - base_model = Gitlab::Database.schemas_to_base_models.fetch(migration_schema).first - - # Migration require an `ActiveRecord::Base` to point to desired database - if base_model != ActiveRecord::Base - with_reestablished_active_record_base do - reconfigure_db_connection( - model: ActiveRecord::Base, - config_model: base_model - ) - - example.run - end - else - example.run - end - - self.class.use_transactional_tests = true - end -end - ActiveRecord::Base.singleton_class.prepend(::Database::ActiveRecordBaseEstablishConnection) # rubocop:disable Database/MultipleDatabases diff --git a/spec/support/helpers/features/access_token_helpers.rb b/spec/support/helpers/features/access_token_helpers.rb new file mode 100644 index 00000000000..f4bdb70c160 --- /dev/null +++ b/spec/support/helpers/features/access_token_helpers.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true +module Spec + module Support + module Helpers + module AccessTokenHelpers + def active_access_tokens + find("[data-testid='active-tokens']") + end + + def created_access_token + within('[data-testid=access-token-section]') do + find('[data-testid=toggle-visibility-button]').click + find_field('new-access-token').value + end + end + end + end + end +end diff --git a/spec/support/helpers/features/releases_helpers.rb b/spec/support/helpers/features/releases_helpers.rb index 9cce9c4882d..a24b99bbe61 100644 --- a/spec/support/helpers/features/releases_helpers.rb +++ b/spec/support/helpers/features/releases_helpers.rb @@ -39,7 +39,7 @@ module Spec wait_for_all_requests - click_button("#{branch_name}") + click_button(branch_name.to_s) end end diff --git a/spec/support/helpers/filter_spec_helper.rb b/spec/support/helpers/filter_spec_helper.rb index ca844b33ba8..7beed9c7755 100644 --- a/spec/support/helpers/filter_spec_helper.rb +++ b/spec/support/helpers/filter_spec_helper.rb @@ -90,10 +90,11 @@ module FilterSpecHelper # # Returns a String def invalidate_reference(reference) - if reference =~ /\A(.+)?[^\d]\d+\z/ + case reference + when /\A(.+)?[^\d]\d+\z/ # Integer-based reference with optional project prefix reference.gsub(/\d+\z/) { |i| i.to_i + 10_000 } - elsif reference =~ /\A(.+@)?(\h{7,40}\z)/ + when /\A(.+@)?(\h{7,40}\z)/ # SHA-based reference with optional prefix reference.gsub(/\h{7,40}\z/) { |v| v.reverse } else diff --git a/spec/support/helpers/filtered_search_helpers.rb b/spec/support/helpers/filtered_search_helpers.rb index 93122ca3d0c..677cea7b804 100644 --- a/spec/support/helpers/filtered_search_helpers.rb +++ b/spec/support/helpers/filtered_search_helpers.rb @@ -254,6 +254,10 @@ module FilteredSearchHelpers expect(page).to have_css '.gl-filtered-search-token', text: "Assignee = #{value}" end + def expect_unioned_assignee_token(value) + expect(page).to have_css '.gl-filtered-search-token', text: "Assignee is one of #{value}" + end + def expect_author_token(value) expect(page).to have_css '.gl-filtered-search-token', text: "Author = #{value}" end diff --git a/spec/support/helpers/full_name_helper.rb b/spec/support/helpers/full_name_helper.rb new file mode 100644 index 00000000000..a41c0da74d4 --- /dev/null +++ b/spec/support/helpers/full_name_helper.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module FullNameHelper + def full_name(first_name, last_name) + "#{first_name} #{last_name}" + end +end + +FullNameHelper.prepend_mod diff --git a/spec/support/helpers/git_helpers.rb b/spec/support/helpers/git_helpers.rb deleted file mode 100644 index 72bba419116..00000000000 --- a/spec/support/helpers/git_helpers.rb +++ /dev/null @@ -1,11 +0,0 @@ -# frozen_string_literal: true - -module GitHelpers - def rugged_repo(repository) - path = Gitlab::GitalyClient::StorageSettings.allow_disk_access do - File.join(TestEnv.repos_path, repository.disk_path + '.git') - end - - Rugged::Repository.new(path) - end -end diff --git a/spec/support/helpers/graphql_helpers.rb b/spec/support/helpers/graphql_helpers.rb index b2fc6ae3286..bd0efc96bd8 100644 --- a/spec/support/helpers/graphql_helpers.rb +++ b/spec/support/helpers/graphql_helpers.rb @@ -378,7 +378,7 @@ module GraphqlHelpers def field_with_params(name, attributes = {}) namerized = GraphqlHelpers.fieldnamerize(name.to_s) - return "#{namerized}" if attributes.blank? + return namerized.to_s if attributes.blank? field_params = if attributes.is_a?(Hash) "(#{attributes_to_graphql(attributes)})" diff --git a/spec/support/helpers/kubernetes_helpers.rb b/spec/support/helpers/kubernetes_helpers.rb index 912e7d24b25..72524453f34 100644 --- a/spec/support/helpers/kubernetes_helpers.rb +++ b/spec/support/helpers/kubernetes_helpers.rb @@ -142,6 +142,29 @@ module KubernetesHelpers WebMock.stub_request(method, ingresses_url).to_return(response) end + def stub_server_min_version_failed_request + WebMock.stub_request(:get, service.api_url + '/version').to_return( + status: [500, "Internal Server Error"], + body: {}.to_json) + end + + def stub_server_min_version(min_version) + response = kube_response({ + "major": "1", # not used, just added here to be a bit more realistic purposes + "minor": min_version.to_s + }) + + WebMock.stub_request( :get, service.api_url + '/version') + .with( + headers: { + 'Accept' => '*/*', + 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', + 'Authorization' => 'Bearer aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', + 'User-Agent' => 'Ruby' + }) + .to_return(response) + end + def stub_kubeclient_knative_services(options = {}) namespace_path = options[:namespace].present? ? "namespaces/#{options[:namespace]}/" : "" @@ -537,7 +560,7 @@ module KubernetesHelpers }, "spec" => { "containers" => [ - { "name" => "#{container_name}" }, + { "name" => container_name.to_s }, { "name" => "#{container_name}-1" } ] }, diff --git a/spec/support/helpers/navbar_structure_helper.rb b/spec/support/helpers/navbar_structure_helper.rb index b44552d6479..e1ed3ffacec 100644 --- a/spec/support/helpers/navbar_structure_helper.rb +++ b/spec/support/helpers/navbar_structure_helper.rb @@ -90,7 +90,11 @@ module NavbarStructureHelper _('Kubernetes'), new_nav_item: { nav_item: _('Observability'), - nav_sub_items: [] + nav_sub_items: [ + _('Dashboards'), + _('Explore'), + _('Manage Dashboards') + ] } ) end diff --git a/spec/support/helpers/reference_parser_helpers.rb b/spec/support/helpers/reference_parser_helpers.rb index b9796ebbe62..370dedabd9b 100644 --- a/spec/support/helpers/reference_parser_helpers.rb +++ b/spec/support/helpers/reference_parser_helpers.rb @@ -12,14 +12,14 @@ module ReferenceParserHelpers end RSpec.shared_examples 'no project N+1 queries' do - it 'avoids N+1 queries in #nodes_visible_to_user' do + it 'avoids N+1 queries in #nodes_visible_to_user', :use_sql_query_cache do context = Banzai::RenderContext.new(project, user) request = lambda do |links| described_class.new(context).nodes_visible_to_user(user, links) end - control = ActiveRecord::QueryRecorder.new { request.call(control_links) } + control = ActiveRecord::QueryRecorder.new(skip_cached: false) { request.call(control_links) } create(:group_member, group: project.group) if project.group create(:project_member, project: project) diff --git a/spec/support/helpers/search_helpers.rb b/spec/support/helpers/search_helpers.rb index 581ef07752e..7d0f8c09933 100644 --- a/spec/support/helpers/search_helpers.rb +++ b/spec/support/helpers/search_helpers.rb @@ -33,13 +33,13 @@ module SearchHelpers end def select_search_scope(scope) - page.within '.search-filter' do + page.within '[data-testid="search-filter"]' do click_link scope end end def has_search_scope?(scope) - page.within '.search-filter' do + page.within '[data-testid="search-filter"]' do has_link?(scope) end end diff --git a/spec/support/helpers/stub_configuration.rb b/spec/support/helpers/stub_configuration.rb index f41457d2420..24c768258a1 100644 --- a/spec/support/helpers/stub_configuration.rb +++ b/spec/support/helpers/stub_configuration.rb @@ -38,6 +38,10 @@ module StubConfiguration allow(Rails.application.routes).to receive(:default_url_options).and_return(url_options) end + def stub_dependency_proxy_setting(messages) + allow(Gitlab.config.dependency_proxy).to receive_messages(to_settings(messages)) + end + def stub_gravatar_setting(messages) allow(Gitlab.config.gravatar).to receive_messages(to_settings(messages)) end diff --git a/spec/support/helpers/stub_feature_flags.rb b/spec/support/helpers/stub_feature_flags.rb index f1654e55b7e..e301e29afc2 100644 --- a/spec/support/helpers/stub_feature_flags.rb +++ b/spec/support/helpers/stub_feature_flags.rb @@ -37,6 +37,10 @@ module StubFeatureFlags # Enable `ci_live_trace` feature flag only on the specified projects. def stub_feature_flags(features) features.each do |feature_name, actors| + unless Feature::Definition.get(feature_name) + ActiveSupport::Deprecation.warn "Invalid Feature Flag #{feature_name} stubbed" + end + # Remove feature flag overwrite feature = Feature.get(feature_name) # rubocop:disable Gitlab/AvoidFeatureGet feature.remove diff --git a/spec/support/helpers/test_env.rb b/spec/support/helpers/test_env.rb index c58353558df..e1b461cf37e 100644 --- a/spec/support/helpers/test_env.rb +++ b/spec/support/helpers/test_env.rb @@ -11,6 +11,8 @@ module TestEnv # When developing the seed repository, comment out the branch you will modify. BRANCH_SHA = { 'signed-commits' => 'c7794c1', + 'gpg-signed' => '8a852d5', + 'x509-signed' => 'a4df3c8', 'not-merged-branch' => 'b83d6e3', 'branch-merged' => '498214d', 'empty-branch' => '7efb185', @@ -43,7 +45,7 @@ module TestEnv 'video' => '8879059', 'crlf-diff' => '5938907', 'conflict-start' => '824be60', - 'conflict-resolvable' => '1450cd6', + 'conflict-resolvable' => '1450cd639e0bc6721eb02800169e464f212cde06', 'conflict-binary-file' => '259a6fb', 'conflict-contains-conflict-markers' => '78a3086', 'conflict-missing-side' => 'eb227b3', @@ -282,15 +284,30 @@ module TestEnv unless File.directory?(repo_path) start = Time.now system(*%W(#{Gitlab.config.git.bin_path} clone --quiet -- #{clone_url} #{repo_path})) - system(*%W(#{Gitlab.config.git.bin_path} -C #{repo_path} remote remove origin)) puts "==> #{repo_path} set up in #{Time.now - start} seconds...\n" end - set_repo_refs(repo_path, refs) + create_bundle = !File.file?(repo_bundle_path) - unless File.file?(repo_bundle_path) + unless set_repo_refs(repo_path, refs) + # Prefer not to fetch over the network. Only fetch when we have failed to + # set all the required local branches. This would happen when a new + # branch is added to BRANCH_SHA, in which case we want to update + # everything. + unless system(*%W(#{Gitlab.config.git.bin_path} -C #{repo_path} fetch origin)) + raise 'Could not fetch test seed repository.' + end + + unless set_repo_refs(repo_path, refs) + raise "Could not update test seed repository, please delete #{repo_path} and try again" + end + + create_bundle = true + end + + if create_bundle start = Time.now - system(git_env, *%W(#{Gitlab.config.git.bin_path} -C #{repo_path} bundle create #{repo_bundle_path} --all)) + system(git_env, *%W(#{Gitlab.config.git.bin_path} -C #{repo_path} bundle create #{repo_bundle_path} --exclude refs/remotes/* --all)) puts "==> #{repo_bundle_path} generated in #{Time.now - start} seconds...\n" end end @@ -392,20 +409,13 @@ module TestEnv end def set_repo_refs(repo_path, branch_sha) - instructions = branch_sha.map { |branch, sha| "update refs/heads/#{branch}\x00#{sha}\x00" }.join("\x00") << "\x00" - update_refs = %W(#{Gitlab.config.git.bin_path} update-ref --stdin -z) - reset = proc do - Dir.chdir(repo_path) do - IO.popen(update_refs, "w") { |io| io.write(instructions) } - $?.success? + IO.popen(%W[#{Gitlab.config.git.bin_path} -C #{repo_path} update-ref --stdin -z], "w") do |io| + branch_sha.each do |branch, sha| + io.write("update refs/heads/#{branch}\x00#{sha}\x00\x00") end end - # Try to reset without fetching to avoid using the network. - unless reset.call - raise 'Could not fetch test seed repository.' unless system(*%W(#{Gitlab.config.git.bin_path} -C #{repo_path} fetch origin)) - raise "Could not update test seed repository, please delete #{repo_path} and try again" unless reset.call - end + $?.success? end def component_timed_setup(component, install_dir:, version:, task:, fresh_install: true, task_args: []) diff --git a/spec/support/helpers/usage_data_helpers.rb b/spec/support/helpers/usage_data_helpers.rb index b4f0cbd8527..92a946db337 100644 --- a/spec/support/helpers/usage_data_helpers.rb +++ b/spec/support/helpers/usage_data_helpers.rb @@ -67,24 +67,12 @@ module UsageDataHelpers projects_with_repositories_enabled projects_with_error_tracking_enabled projects_with_enabled_alert_integrations - projects_with_expiration_policy_enabled - projects_with_expiration_policy_enabled_with_keep_n_unset - projects_with_expiration_policy_enabled_with_keep_n_set_to_1 - projects_with_expiration_policy_enabled_with_keep_n_set_to_5 - projects_with_expiration_policy_enabled_with_keep_n_set_to_10 - projects_with_expiration_policy_enabled_with_keep_n_set_to_25 - projects_with_expiration_policy_enabled_with_keep_n_set_to_50 projects_with_expiration_policy_enabled_with_older_than_unset projects_with_expiration_policy_enabled_with_older_than_set_to_7d projects_with_expiration_policy_enabled_with_older_than_set_to_14d projects_with_expiration_policy_enabled_with_older_than_set_to_30d projects_with_expiration_policy_enabled_with_older_than_set_to_60d projects_with_expiration_policy_enabled_with_older_than_set_to_90d - projects_with_expiration_policy_enabled_with_cadence_set_to_1d - projects_with_expiration_policy_enabled_with_cadence_set_to_7d - projects_with_expiration_policy_enabled_with_cadence_set_to_14d - projects_with_expiration_policy_enabled_with_cadence_set_to_1month - projects_with_expiration_policy_enabled_with_cadence_set_to_3month projects_with_terraform_reports projects_with_terraform_states pages_domains diff --git a/spec/support/matchers/exceed_query_limit.rb b/spec/support/matchers/exceed_query_limit.rb index bfcaf9552b3..6d7658b7c33 100644 --- a/spec/support/matchers/exceed_query_limit.rb +++ b/spec/support/matchers/exceed_query_limit.rb @@ -333,7 +333,7 @@ RSpec::Matchers.define :issue_same_number_of_queries_as do or_fewer_msg = "or fewer" if @or_fewer threshold_msg = "(+/- #{threshold})" unless threshold == 0 - ["#{expected_count}", or_fewer_msg, threshold_msg].compact.join(' ') + [expected_count.to_s, or_fewer_msg, threshold_msg].compact.join(' ') end def skip_cached diff --git a/spec/support/matchers/graphql_matchers.rb b/spec/support/matchers/graphql_matchers.rb index db7d4269945..155a6dba52c 100644 --- a/spec/support/matchers/graphql_matchers.rb +++ b/spec/support/matchers/graphql_matchers.rb @@ -169,7 +169,11 @@ RSpec::Matchers.define :have_graphql_type do |expected, opts = {}| include GraphQLTypeHelpers match do |object| - expect(object.type).to eq(nullified(expected, opts[:null])) + if object.type.list? + expect(object.type.unwrap).to eq(nullified(expected, opts[:null])) + else + expect(object.type).to eq(nullified(expected, opts[:null])) + end end failure_message do |object| diff --git a/spec/support/migration.rb b/spec/support/migration.rb index 490aa836d74..2a69630a29a 100644 --- a/spec/support/migration.rb +++ b/spec/support/migration.rb @@ -16,14 +16,42 @@ RSpec.configure do |config| schema_migrate_down! end + config.after(:context, :migration) do + Gitlab::CurrentSettings.clear_in_memory_application_settings! + end + + config.append_after(:context, :migration) do + recreate_databases_and_seed_if_needed || ensure_schema_and_empty_tables + end + + config.around(:each, :migration) do |example| + self.class.use_transactional_tests = false + + migration_schema = example.metadata[:migration] + migration_schema = :gitlab_main if migration_schema == true + base_model = Gitlab::Database.schemas_to_base_models.fetch(migration_schema).first + + # Migration require an `ActiveRecord::Base` to point to desired database + if base_model != ActiveRecord::Base + with_reestablished_active_record_base do + reconfigure_db_connection( + model: ActiveRecord::Base, + config_model: base_model + ) + + example.run + end + else + example.run + end + + self.class.use_transactional_tests = true + end + # Each example may call `migrate!`, so we must ensure we are migrated down every time config.before(:each, :migration) do use_fake_application_settings schema_migrate_down! end - - config.after(:context, :migration) do - Gitlab::CurrentSettings.clear_in_memory_application_settings! - end end diff --git a/spec/support/models/partitionable_check.rb b/spec/support/models/ci/partitioning_testing/cascade_check.rb index 2c09c1b3408..f553a47ef4f 100644 --- a/spec/support/models/partitionable_check.rb +++ b/spec/support/models/ci/partitioning_testing/cascade_check.rb @@ -10,37 +10,18 @@ module PartitioningTesting def check_partition_cascade_value raise 'Partition value not found' unless partition_scope_value - raise 'Default value detected' if partition_id == 100 return if partition_id == partition_scope_value raise "partition_id was expected to equal #{partition_scope_value} but it was #{partition_id}." end end - - module DefaultPartitionValue - extend ActiveSupport::Concern - - class_methods do - def current_partition_value - current = super - - if current == 100 - 54321 - else - current - end - end - end - end end Ci::Partitionable::Testing::PARTITIONABLE_MODELS.each do |klass| + next if klass == 'Ci::Pipeline' + model = klass.safe_constantize - if klass == 'Ci::Pipeline' - model.prepend(PartitioningTesting::DefaultPartitionValue) - else - model.include(PartitioningTesting::CascadeCheck) - end + model.include(PartitioningTesting::CascadeCheck) end diff --git a/spec/support/models/ci/partitioning_testing/partition_identifiers.rb b/spec/support/models/ci/partitioning_testing/partition_identifiers.rb new file mode 100644 index 00000000000..aa091095fb6 --- /dev/null +++ b/spec/support/models/ci/partitioning_testing/partition_identifiers.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Ci + module PartitioningTesting + module PartitionIdentifiers + module_function + + def ci_testing_partition_id + 99999 + end + end + end +end diff --git a/spec/support/models/ci/partitioning_testing/rspec_hooks.rb b/spec/support/models/ci/partitioning_testing/rspec_hooks.rb new file mode 100644 index 00000000000..39b15ba8721 --- /dev/null +++ b/spec/support/models/ci/partitioning_testing/rspec_hooks.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +RSpec.configure do |config| + config.include Ci::PartitioningTesting::PartitionIdentifiers + + config.around(:each, :ci_partitionable) do |example| + Ci::PartitioningTesting::SchemaHelpers.with_routing_tables do + example.run + end + end + + config.before(:all) do + Ci::PartitioningTesting::SchemaHelpers.setup + end + + config.after(:all) do + Ci::PartitioningTesting::SchemaHelpers.teardown + end +end diff --git a/spec/support/models/ci/partitioning_testing/schema_helpers.rb b/spec/support/models/ci/partitioning_testing/schema_helpers.rb new file mode 100644 index 00000000000..712178710da --- /dev/null +++ b/spec/support/models/ci/partitioning_testing/schema_helpers.rb @@ -0,0 +1,86 @@ +# frozen_string_literal: true + +module Ci + module PartitioningTesting + module SchemaHelpers + DEFAULT_PARTITION = 100 + + module_function + + def with_routing_tables + Ci::BuildMetadata.table_name = :p_ci_builds_metadata + yield + ensure + Ci::BuildMetadata.table_name = :ci_builds_metadata + end + + # We're dropping the default values here to ensure that the application code + # populates the `partition_id` value and it's not falling back on the + # database default one. We should be able to clean this up after + # partitioning the tables and substituting the routing table in the model: + # https://gitlab.com/gitlab-org/gitlab/-/issues/377822 + # + def setup(connection: Ci::ApplicationRecord.connection) + each_partitionable_table do |table_name| + change_column_default(table_name, from: DEFAULT_PARTITION, to: nil, connection: connection) + change_column_default("p_#{table_name}", from: DEFAULT_PARTITION, to: nil, connection: connection) + create_test_partition(table_name, connection: connection) + end + end + + def teardown(connection: Ci::ApplicationRecord.connection) + each_partitionable_table do |table_name| + drop_test_partition(table_name, connection: connection) + change_column_default(table_name, from: nil, to: DEFAULT_PARTITION, connection: connection) + change_column_default("p_#{table_name}", from: nil, to: DEFAULT_PARTITION, connection: connection) + end + end + + def each_partitionable_table + ::Ci::Partitionable::Testing::PARTITIONABLE_MODELS.each do |klass| + model = klass.safe_constantize + table_name = model.table_name.delete_prefix('p_') + + yield(table_name) + + model.reset_column_information if model.connected? + end + end + + def change_column_default(table_name, from:, to:, connection:) + return unless table_available?(table_name, connection: connection) + + connection.change_column_default(table_name, :partition_id, from: from, to: to) + end + + def create_test_partition(table_name, connection:) + return unless table_available?("p_#{table_name}", connection: connection) + + drop_test_partition(table_name, connection: connection) + + connection.execute(<<~SQL) + CREATE TABLE #{full_partition_name(table_name)} + PARTITION OF p_#{table_name} + FOR VALUES IN (#{PartitioningTesting::PartitionIdentifiers.ci_testing_partition_id}); + SQL + end + + def drop_test_partition(table_name, connection:) + return unless table_available?(table_name, connection: connection) + + connection.execute(<<~SQL) + DROP TABLE IF EXISTS #{full_partition_name(table_name)}; + SQL + end + + def table_available?(table_name, connection:) + connection.table_exists?(table_name) && + connection.column_exists?(table_name, :partition_id) + end + + def full_partition_name(table_name) + "#{Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA}._test_gitlab_#{table_name}_partition" + end + end + end +end diff --git a/spec/support/multiple_databases.rb b/spec/support/multiple_databases.rb new file mode 100644 index 00000000000..616cf00269c --- /dev/null +++ b/spec/support/multiple_databases.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +RSpec.configure do |config| + # Ensure database versions are memoized to prevent query counts from + # being affected by version checks. Note that + # Gitlab::Database.check_postgres_version_and_print_warning is called + # at startup, but that generates its own + # `Gitlab::Database::Reflection` so the result is not memoized by + # callers of `ApplicationRecord.database.version`, such as + # `Gitlab::Database::AsWithMaterialized.materialized_supported?`. + # TODO This can be removed once https://gitlab.com/gitlab-org/gitlab/-/issues/325639 is completed. + [ApplicationRecord, ::Ci::ApplicationRecord].each { |record| record.database.version } + + config.around(:each, :reestablished_active_record_base) do |example| + with_reestablished_active_record_base(reconnect: example.metadata.fetch(:reconnect, true)) do + example.run + end + end + + config.around(:each, :add_ci_connection) do |example| + with_added_ci_connection do + example.run + end + end +end diff --git a/spec/support/rate_limiter.rb b/spec/support/rate_limiter.rb new file mode 100644 index 00000000000..525d593c293 --- /dev/null +++ b/spec/support/rate_limiter.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +RSpec.configure do |config| + config.before(:each, :disable_rate_limiter) do + allow(Gitlab::ApplicationRateLimiter).to receive(:throttled?).and_return(false) + end +end diff --git a/spec/support/redis.rb b/spec/support/redis.rb index d00d6562966..6d313c8aa16 100644 --- a/spec/support/redis.rb +++ b/spec/support/redis.rb @@ -19,4 +19,10 @@ RSpec.configure do |config| public_send("redis_#{underscored_name}_cleanup!") end end + + config.before(:suite) do + Gitlab::Redis::ALL_CLASSES.each do |instance_class| + instance_class.with(&:flushdb) + end + end end diff --git a/spec/support/rspec.rb b/spec/support/rspec.rb index 6795d2f6d2a..71dfc3fd5a3 100644 --- a/spec/support/rspec.rb +++ b/spec/support/rspec.rb @@ -11,6 +11,9 @@ require_relative "helpers/fast_rails_root" RSpec::Expectations.configuration.on_potential_false_positives = :raise RSpec.configure do |config| + # See https://gitlab.com/gitlab-org/gitlab/-/issues/379686 + config.threadsafe = false + # Re-run failures locally with `--only-failures` config.example_status_persistence_file_path = ENV.fetch('RSPEC_LAST_RUN_RESULTS_FILE', './spec/examples.txt') diff --git a/spec/support/rspec_order_todo.yml b/spec/support/rspec_order_todo.yml index 30220b04fa2..67b7023f1ff 100644 --- a/spec/support/rspec_order_todo.yml +++ b/spec/support/rspec_order_todo.yml @@ -997,7 +997,6 @@ - './ee/spec/graphql/types/vulnerability_severity_enum_spec.rb' - './ee/spec/graphql/types/vulnerability_sort_enum_spec.rb' - './ee/spec/graphql/types/vulnerability_state_enum_spec.rb' -- './ee/spec/graphql/types/vulnerability_type_spec.rb' - './ee/spec/graphql/types/vulnerable_dependency_type_spec.rb' - './ee/spec/graphql/types/vulnerable_kubernetes_resource_type_spec.rb' - './ee/spec/graphql/types/vulnerable_package_type_spec.rb' @@ -1870,7 +1869,6 @@ - './ee/spec/models/ee/ci/secure_file_spec.rb' - './ee/spec/models/ee/clusters/agent_spec.rb' - './ee/spec/models/ee/description_version_spec.rb' -- './ee/spec/models/ee/event_collection_spec.rb' - './ee/spec/models/ee/event_spec.rb' - './ee/spec/models/ee/gpg_key_spec.rb' - './ee/spec/models/ee/group_group_link_spec.rb' @@ -3223,7 +3221,6 @@ - './ee/spec/services/security/security_orchestration_policies/sync_opened_merge_requests_service_spec.rb' - './ee/spec/services/security/security_orchestration_policies/sync_open_merge_requests_head_pipeline_service_spec.rb' - './ee/spec/services/security/security_orchestration_policies/validate_policy_service_spec.rb' -- './ee/spec/services/security/store_findings_metadata_service_spec.rb' - './ee/spec/services/security/store_grouped_scans_service_spec.rb' - './ee/spec/services/security/store_scan_service_spec.rb' - './ee/spec/services/security/store_scans_service_spec.rb' @@ -3379,7 +3376,6 @@ - './ee/spec/views/registrations/welcome/continuous_onboarding_getting_started.html.haml_spec.rb' - './ee/spec/views/registrations/welcome/show.html.haml_spec.rb' - './ee/spec/views/search/_category.html.haml_spec.rb' -- './ee/spec/views/shared/access_tokens/_table.html.haml_spec.rb' - './ee/spec/views/shared/billings/_billing_plan_actions.html.haml_spec.rb' - './ee/spec/views/shared/billings/_billing_plan.html.haml_spec.rb' - './ee/spec/views/shared/billings/_billing_plans.html.haml_spec.rb' @@ -4091,7 +4087,6 @@ - './spec/features/incidents/user_creates_new_incident_spec.rb' - './spec/features/incidents/user_filters_incidents_by_status_spec.rb' - './spec/features/incidents/user_searches_incidents_spec.rb' -- './spec/features/incidents/user_views_incident_spec.rb' - './spec/features/invites_spec.rb' - './spec/features/issuables/issuable_list_spec.rb' - './spec/features/issuables/markdown_references/internal_references_spec.rb' @@ -5873,14 +5868,6 @@ - './spec/lib/error_tracking/collector/payload_validator_spec.rb' - './spec/lib/error_tracking/collector/sentry_auth_parser_spec.rb' - './spec/lib/error_tracking/collector/sentry_request_parser_spec.rb' -- './spec/lib/error_tracking/sentry_client/api_urls_spec.rb' -- './spec/lib/error_tracking/sentry_client/event_spec.rb' -- './spec/lib/error_tracking/sentry_client/issue_link_spec.rb' -- './spec/lib/error_tracking/sentry_client/issue_spec.rb' -- './spec/lib/error_tracking/sentry_client/pagination_parser_spec.rb' -- './spec/lib/error_tracking/sentry_client/projects_spec.rb' -- './spec/lib/error_tracking/sentry_client/repo_spec.rb' -- './spec/lib/error_tracking/sentry_client_spec.rb' - './spec/lib/error_tracking/stacktrace_builder_spec.rb' - './spec/lib/event_filter_spec.rb' - './spec/lib/expand_variables_spec.rb' @@ -8400,7 +8387,6 @@ - './spec/models/error_tracking/error_event_spec.rb' - './spec/models/error_tracking/error_spec.rb' - './spec/models/error_tracking/project_error_tracking_setting_spec.rb' -- './spec/models/event_collection_spec.rb' - './spec/models/event_spec.rb' - './spec/models/experiment_spec.rb' - './spec/models/experiment_subject_spec.rb' @@ -9464,16 +9450,6 @@ - './spec/routing/projects/security/configuration_controller_routing_spec.rb' - './spec/routing/routing_spec.rb' - './spec/routing/uploads_routing_spec.rb' -- './spec/scripts/changed-feature-flags_spec.rb' -- './spec/scripts/determine-qa-tests_spec.rb' -- './spec/scripts/failed_tests_spec.rb' -- './spec/scripts/lib/glfm/parse_examples_spec.rb' -- './spec/scripts/lib/glfm/shared_spec.rb' -- './spec/scripts/lib/glfm/update_example_snapshots_spec.rb' -- './spec/scripts/lib/glfm/update_specification_spec.rb' -- './spec/scripts/pipeline_test_report_builder_spec.rb' -- './spec/scripts/setup/find_jh_branch_spec.rb' -- './spec/scripts/trigger-build_spec.rb' - './spec/serializers/accessibility_error_entity_spec.rb' - './spec/serializers/accessibility_reports_comparer_entity_spec.rb' - './spec/serializers/accessibility_reports_comparer_serializer_spec.rb' @@ -10082,7 +10058,6 @@ - './spec/services/members/request_access_service_spec.rb' - './spec/services/members/standard_member_builder_spec.rb' - './spec/services/members/unassign_issuables_service_spec.rb' -- './spec/services/members/update_service_spec.rb' - './spec/services/merge_requests/add_context_service_spec.rb' - './spec/services/merge_requests/add_spent_time_service_spec.rb' - './spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb' @@ -10768,7 +10743,6 @@ - './spec/views/registrations/welcome/show.html.haml_spec.rb' - './spec/views/search/_results.html.haml_spec.rb' - './spec/views/search/show.html.haml_spec.rb' -- './spec/views/shared/access_tokens/_table.html.haml_spec.rb' - './spec/views/shared/deploy_tokens/_form.html.haml_spec.rb' - './spec/views/shared/groups/_dropdown.html.haml_spec.rb' - './spec/views/shared/issuable/_sidebar.html.haml_spec.rb' @@ -10922,7 +10896,6 @@ - './spec/workers/environments/canary_ingress/update_worker_spec.rb' - './spec/workers/error_tracking_issue_link_worker_spec.rb' - './spec/workers/every_sidekiq_worker_spec.rb' -- './spec/workers/experiments/record_conversion_event_worker_spec.rb' - './spec/workers/expire_build_artifacts_worker_spec.rb' - './spec/workers/export_csv_worker_spec.rb' - './spec/workers/external_service_reactive_caching_worker_spec.rb' diff --git a/spec/support/services/issuable_update_service_shared_examples.rb b/spec/support/services/issuable_update_service_shared_examples.rb index 94061b140f4..b85c3904127 100644 --- a/spec/support/services/issuable_update_service_shared_examples.rb +++ b/spec/support/services/issuable_update_service_shared_examples.rb @@ -6,18 +6,48 @@ RSpec.shared_examples 'issuable update service' do end context 'changing state' do - before do - expect(project).to receive(:execute_hooks).once - end + let(:hook_event) { :"#{closed_issuable.class.name.underscore.to_sym}_hooks" } context 'to reopened' do - it 'executes hooks only once' do + let(:expected_payload) do + include( + changes: include( + state_id: { current: 1, previous: 2 }, + updated_at: { current: kind_of(Time), previous: kind_of(Time) } + ), + object_attributes: include( + state: 'opened', + action: 'reopen' + ) + ) + end + + it 'executes hooks' do + expect(project).to receive(:execute_hooks).with(expected_payload, hook_event) + expect(project).to receive(:execute_integrations).with(expected_payload, hook_event) + described_class.new(project: project, current_user: user, params: { state_event: 'reopen' }).execute(closed_issuable) end end context 'to closed' do - it 'executes hooks only once' do + let(:expected_payload) do + include( + changes: include( + state_id: { current: 2, previous: 1 }, + updated_at: { current: kind_of(Time), previous: kind_of(Time) } + ), + object_attributes: include( + state: 'closed', + action: 'close' + ) + ) + end + + it 'executes hooks' do + expect(project).to receive(:execute_hooks).with(expected_payload, hook_event) + expect(project).to receive(:execute_integrations).with(expected_payload, hook_event) + described_class.new(project: project, current_user: user, params: { state_event: 'close' }).execute(open_issuable) end end diff --git a/spec/support/services/migrate_to_ghost_user_service_shared_examples.rb b/spec/support/services/migrate_to_ghost_user_service_shared_examples.rb index 1e291a90163..ae98ce689e3 100644 --- a/spec/support/services/migrate_to_ghost_user_service_shared_examples.rb +++ b/spec/support/services/migrate_to_ghost_user_service_shared_examples.rb @@ -57,7 +57,7 @@ RSpec.shared_examples "migrating a deleted user's associated records to the ghos context "race conditions" do context "when #{record_class_name} migration fails and is rolled back" do before do - expect_any_instance_of(ActiveRecord::Associations::CollectionProxy) + allow_any_instance_of(ActiveRecord::Associations::CollectionProxy) .to receive(:update_all).and_raise(ActiveRecord::StatementTimeout) end @@ -68,6 +68,7 @@ RSpec.shared_examples "migrating a deleted user's associated records to the ghos end it "doesn't unblock a previously-blocked user" do + expect(user.starred_projects).to receive(:update_all).and_call_original user.block expect { service.execute }.to raise_error(ActiveRecord::StatementTimeout) diff --git a/spec/support/shared_contexts/container_repositories_shared_context.rb b/spec/support/shared_contexts/container_repositories_shared_context.rb index a74b09d38bd..c3040a77517 100644 --- a/spec/support/shared_contexts/container_repositories_shared_context.rb +++ b/spec/support/shared_contexts/container_repositories_shared_context.rb @@ -14,7 +14,6 @@ RSpec.shared_context 'importable repositories' do before do stub_application_setting(container_registry_import_created_before: 1.day.ago) stub_feature_flags( - container_registry_phase_2_deny_list: false, container_registry_migration_limit_gitlab_org: false, container_registry_migration_phase2_all_plans: false ) diff --git a/spec/support/shared_contexts/graphql/resolvers/runners_resolver_shared_context.rb b/spec/support/shared_contexts/graphql/resolvers/runners_resolver_shared_context.rb index aa857cfdb70..1480b5f98e7 100644 --- a/spec/support/shared_contexts/graphql/resolvers/runners_resolver_shared_context.rb +++ b/spec/support/shared_contexts/graphql/resolvers/runners_resolver_shared_context.rb @@ -17,6 +17,6 @@ RSpec.shared_context 'runners resolver setup' do end let_it_be(:group_runner) { create(:ci_runner, :group, groups: [group], token: 'mnopqr', description: 'group runner', contacted_at: 2.seconds.ago) } - let_it_be(:subgroup_runner) { create(:ci_runner, :group, groups: [subgroup], token: 'mnopqr', description: 'subgroup runner', contacted_at: 1.second.ago) } + let_it_be(:subgroup_runner) { create(:ci_runner, :group, groups: [subgroup], token: '123456', description: 'subgroup runner', contacted_at: 1.second.ago) } let_it_be(:instance_runner) { create(:ci_runner, :instance, description: 'shared runner', token: 'stuvxz', contacted_at: 2.minutes.ago, tag_list: %w(instance_runner active_runner)) } end diff --git a/spec/support/shared_contexts/jobs/handling_retried_jobs_shared_context.rb b/spec/support/shared_contexts/jobs/handling_retried_jobs_shared_context.rb new file mode 100644 index 00000000000..428eff77373 --- /dev/null +++ b/spec/support/shared_contexts/jobs/handling_retried_jobs_shared_context.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +RSpec.shared_context 'when handling retried jobs' do |url| + let(:set_name) { 'retry' } + # Account for Sidekiq retry jitter + # https://github.com/mperham/sidekiq/blob/3575ccb44c688dd08bfbfd937696260b12c622fb/lib/sidekiq/job_retry.rb#L217 + let(:schedule_jitter) { 10 } + + # Try to mimic as closely as possible what Sidekiq will actually + # do to retry a job. + def retry_in(klass, time, args = 0) + message = Gitlab::Json.generate( + 'class' => klass.name, + 'args' => [args], + 'retry' => true + ) + + allow(klass).to receive(:sidekiq_retry_in_block).and_return(proc { time.to_i }) + + begin + Sidekiq::JobRetry.new(Sidekiq).local(klass, message, klass.queue) { raise 'boom' } + rescue Sidekiq::JobRetry::Skip + # Sidekiq scheduled the retry + end + end +end diff --git a/spec/support/shared_contexts/lib/sbom/package_url_shared_contexts.rb b/spec/support/shared_contexts/lib/sbom/package_url_shared_contexts.rb new file mode 100644 index 00000000000..263cf9f5e19 --- /dev/null +++ b/spec/support/shared_contexts/lib/sbom/package_url_shared_contexts.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +require 'oj' + +def parameterized_test_matrix(invalid: false) + test_cases_path = File.join( + File.expand_path(__dir__), '..', '..', '..', '..', 'fixtures', 'lib', 'sbom', 'package-url-test-cases.json') + test_cases = Gitlab::Json.parse(File.read(test_cases_path)) + + test_cases.filter { _1.delete('is_invalid') == invalid }.each_with_object({}) do |test_case, memo| + description = test_case.delete('description') + memo[description] = test_case.symbolize_keys + end +end + +RSpec.shared_context 'with valid purl examples' do + where do + parameterized_test_matrix(invalid: false) + end +end + +RSpec.shared_context 'with invalid purl examples' do + where do + parameterized_test_matrix(invalid: true) + end +end diff --git a/spec/support/shared_contexts/navbar_structure_context.rb b/spec/support/shared_contexts/navbar_structure_context.rb index 064e40287be..af35a5ff068 100644 --- a/spec/support/shared_contexts/navbar_structure_context.rb +++ b/spec/support/shared_contexts/navbar_structure_context.rb @@ -67,8 +67,8 @@ RSpec.shared_context 'project navbar structure' do { nav_item: _('Deployments'), nav_sub_items: [ - _('Feature Flags'), _('Environments'), + _('Feature Flags'), _('Releases') ] }, @@ -85,8 +85,7 @@ RSpec.shared_context 'project navbar structure' do _('Metrics'), _('Error Tracking'), _('Alerts'), - _('Incidents'), - _('Product Analytics') + _('Incidents') ] }, { diff --git a/spec/support/shared_contexts/policies/group_policy_shared_context.rb b/spec/support/shared_contexts/policies/group_policy_shared_context.rb index bb1b794c2b6..a6226fe903b 100644 --- a/spec/support/shared_contexts/policies/group_policy_shared_context.rb +++ b/spec/support/shared_contexts/policies/group_policy_shared_context.rb @@ -76,6 +76,7 @@ RSpec.shared_context 'GroupPolicy context' do register_group_runners read_billing edit_billing + admin_member_access_request ] end diff --git a/spec/support/shared_contexts/policies/project_policy_shared_context.rb b/spec/support/shared_contexts/policies/project_policy_shared_context.rb index fc7255a4a20..6e2caa853f8 100644 --- a/spec/support/shared_contexts/policies/project_policy_shared_context.rb +++ b/spec/support/shared_contexts/policies/project_policy_shared_context.rb @@ -68,7 +68,7 @@ RSpec.shared_context 'ProjectPolicy context' do admin_project admin_project_member admin_snippet admin_terraform_state admin_wiki create_deploy_token destroy_deploy_token push_to_delete_protected_branch read_deploy_token update_snippet - destroy_upload + destroy_upload admin_member_access_request rename_project ] end @@ -83,7 +83,7 @@ RSpec.shared_context 'ProjectPolicy context' do let(:base_owner_permissions) do %i[ archive_project change_namespace change_visibility_level destroy_issue - destroy_merge_request manage_owners remove_fork_project remove_project rename_project + destroy_merge_request manage_owners remove_fork_project remove_project set_issue_created_at set_issue_iid set_issue_updated_at set_note_created_at ] diff --git a/spec/support/shared_contexts/policies/project_policy_table_shared_context.rb b/spec/support/shared_contexts/policies/project_policy_table_shared_context.rb index b18ce14eba6..d9ea7bc7f82 100644 --- a/spec/support/shared_contexts/policies/project_policy_table_shared_context.rb +++ b/spec/support/shared_contexts/policies/project_policy_table_shared_context.rb @@ -126,6 +126,63 @@ RSpec.shared_context 'ProjectPolicyTable context' do end # This table is based on permission_table_for_guest_feature_access, + # but takes into account note confidentiality. It is required on the context + # to have one regular note and one confidential note. + # + # project_level, :feature_access_level, :membership, :admin_mode, :expected_count + def permission_table_for_notes_feature_access + :public | :enabled | :admin | true | 2 + :public | :enabled | :admin | false | 1 + :public | :enabled | :reporter | nil | 2 + :public | :enabled | :guest | nil | 1 + :public | :enabled | :non_member | nil | 1 + :public | :enabled | :anonymous | nil | 1 + + :public | :private | :admin | true | 2 + :public | :private | :admin | false | 0 + :public | :private | :reporter | nil | 2 + :public | :private | :guest | nil | 1 + :public | :private | :non_member | nil | 0 + :public | :private | :anonymous | nil | 0 + + :public | :disabled | :reporter | nil | 0 + :public | :disabled | :guest | nil | 0 + :public | :disabled | :non_member | nil | 0 + :public | :disabled | :anonymous | nil | 0 + + :internal | :enabled | :admin | true | 2 + :internal | :enabled | :admin | false | 1 + :internal | :enabled | :reporter | nil | 2 + :internal | :enabled | :guest | nil | 1 + :internal | :enabled | :non_member | nil | 1 + :internal | :enabled | :anonymous | nil | 0 + + :internal | :private | :admin | true | 2 + :internal | :private | :admin | false | 0 + :internal | :private | :reporter | nil | 2 + :internal | :private | :guest | nil | 1 + :internal | :private | :non_member | nil | 0 + :internal | :private | :anonymous | nil | 0 + + :internal | :disabled | :reporter | nil | 0 + :internal | :disabled | :guest | nil | 0 + :internal | :disabled | :non_member | nil | 0 + :internal | :disabled | :anonymous | nil | 0 + + :private | :private | :admin | true | 2 + :private | :private | :admin | false | 0 + :private | :private | :reporter | nil | 2 + :private | :private | :guest | nil | 1 + :private | :private | :non_member | nil | 0 + :private | :private | :anonymous | nil | 0 + + :private | :disabled | :reporter | nil | 0 + :private | :disabled | :guest | nil | 0 + :private | :disabled | :non_member | nil | 0 + :private | :disabled | :anonymous | nil | 0 + end + + # This table is based on permission_table_for_guest_feature_access, # but with a slight twist. # Some features can be hidden away to GUEST, when project is private. # (see ProjectFeature::PRIVATE_FEATURES_MIN_ACCESS_LEVEL_FOR_PRIVATE_PROJECT) diff --git a/spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb b/spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb index fa048b76e18..7df4b7635d3 100644 --- a/spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb +++ b/spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb @@ -3,6 +3,7 @@ RSpec.shared_examples 'multiple issue boards' do context 'authorized user' do before do + stub_feature_flags(apollo_boards: false) parent.add_maintainer(user) login_as(user) @@ -121,6 +122,7 @@ RSpec.shared_examples 'multiple issue boards' do context 'unauthorized user' do before do + stub_feature_flags(apollo_boards: false) visit boards_path wait_for_requests end diff --git a/spec/support/shared_examples/bulk_imports/common/pipelines/wiki_pipeline_examples.rb b/spec/support/shared_examples/bulk_imports/common/pipelines/wiki_pipeline_examples.rb index cd4432af4ed..a9edf18d562 100644 --- a/spec/support/shared_examples/bulk_imports/common/pipelines/wiki_pipeline_examples.rb +++ b/spec/support/shared_examples/bulk_imports/common/pipelines/wiki_pipeline_examples.rb @@ -35,7 +35,7 @@ RSpec.shared_examples 'wiki pipeline imports a wiki for an entity' do it 'does not import wiki' do expect(subject).to receive(:source_wiki_exists?).and_return(false) - expect(parent.wiki).not_to receive(:ensure_repository) + expect(parent.wiki).not_to receive(:create_wiki_repository) expect(parent.wiki.repository).not_to receive(:ensure_repository) expect { subject.run }.not_to raise_error @@ -75,7 +75,7 @@ RSpec.shared_examples 'wiki pipeline imports a wiki for an entity' do describe 'unsuccessful response' do shared_examples 'does not raise an error' do it 'does not raise an error' do - expect(parent.wiki).not_to receive(:ensure_repository) + expect(parent.wiki).not_to receive(:create_wiki_repository) expect(parent.wiki.repository).not_to receive(:ensure_repository) expect { subject.run }.not_to raise_error diff --git a/spec/support/shared_examples/ci/retryable_shared_examples.rb b/spec/support/shared_examples/ci/retryable_shared_examples.rb new file mode 100644 index 00000000000..4622dbe4e31 --- /dev/null +++ b/spec/support/shared_examples/ci/retryable_shared_examples.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'a retryable job' do + describe '#enqueue_immediately?' do + it 'defaults to false' do + expect(subject.enqueue_immediately?).to eq(false) + end + end + + describe '#set_enqueue_immediately!' do + it 'changes #enqueue_immediately? to true' do + expect { subject.set_enqueue_immediately! } + .to change(subject, :enqueue_immediately?).from(false).to(true) + end + end +end diff --git a/spec/support/shared_examples/controllers/preferred_language_switcher_shared_examples.rb b/spec/support/shared_examples/controllers/preferred_language_switcher_shared_examples.rb new file mode 100644 index 00000000000..74456e62eb8 --- /dev/null +++ b/spec/support/shared_examples/controllers/preferred_language_switcher_shared_examples.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'switches to user preferred language' do |msg_id_example| + context 'with preferred_language in cookies' do + render_views + let(:user_preferred_language) { 'zh_CN' } + + subject { get :new } + + before do + cookies['preferred_language'] = user_preferred_language + end + + it 'renders new template with cookies preferred language' do + expect(subject).to render_template(:new) + expect(response).to have_gitlab_http_status(:ok) + + expected_text = Gitlab::I18n.with_locale(user_preferred_language) { _(msg_id_example) } + expect(response.body).to include(expected_text) + end + end +end diff --git a/spec/support/shared_examples/features/access_tokens_shared_examples.rb b/spec/support/shared_examples/features/access_tokens_shared_examples.rb index cd255abd7a8..32a7b32ac72 100644 --- a/spec/support/shared_examples/features/access_tokens_shared_examples.rb +++ b/spec/support/shared_examples/features/access_tokens_shared_examples.rb @@ -9,13 +9,7 @@ RSpec.shared_examples 'resource access tokens missing access rights' do end RSpec.shared_examples 'resource access tokens creation' do |resource_type| - def active_resource_access_tokens - find("[data-testid='active-tokens']") - end - - def created_resource_access_token - find_field('new-access-token').value - end + include Spec::Support::Helpers::AccessTokenHelpers it 'allows creation of an access token', :aggregate_failures do name = 'My access token' @@ -34,12 +28,12 @@ RSpec.shared_examples 'resource access tokens creation' do |resource_type| click_on "Create #{resource_type} access token" - expect(active_resource_access_tokens).to have_text(name) - expect(active_resource_access_tokens).to have_text('in') - expect(active_resource_access_tokens).to have_text('read_api') - expect(active_resource_access_tokens).to have_text('read_repository') - expect(active_resource_access_tokens).to have_text('Guest') - expect(created_resource_access_token).not_to be_empty + expect(active_access_tokens).to have_text(name) + expect(active_access_tokens).to have_text('in') + expect(active_access_tokens).to have_text('read_api') + expect(active_access_tokens).to have_text('read_repository') + expect(active_access_tokens).to have_text('Guest') + expect(created_access_token).to match(/[\w-]{20}/) end end @@ -105,14 +99,14 @@ RSpec.shared_examples 'resource access tokens creation disallowed' do |error_mes end RSpec.shared_examples 'active resource access tokens' do - def active_resource_access_tokens + def active_access_tokens find("[data-testid='active-tokens']") end it 'shows active access tokens' do visit resource_settings_access_tokens_path - expect(active_resource_access_tokens).to have_text(resource_access_token.name) + expect(active_access_tokens).to have_text(resource_access_token.name) end context 'when User#time_display_relative is false' do @@ -123,13 +117,13 @@ RSpec.shared_examples 'active resource access tokens' do it 'shows absolute times for expires_at' do visit resource_settings_access_tokens_path - expect(active_resource_access_tokens).to have_text(PersonalAccessToken.last.expires_at.strftime('%b %-d')) + expect(active_access_tokens).to have_text(PersonalAccessToken.last.expires_at.strftime('%b %-d')) end end end RSpec.shared_examples 'inactive resource access tokens' do |no_active_tokens_text| - def active_resource_access_tokens + def active_access_tokens find("[data-testid='active-tokens']") end @@ -137,14 +131,14 @@ RSpec.shared_examples 'inactive resource access tokens' do |no_active_tokens_tex visit resource_settings_access_tokens_path accept_gl_confirm(button_text: 'Revoke') { click_on 'Revoke' } - expect(active_resource_access_tokens).to have_text(no_active_tokens_text) + expect(active_access_tokens).to have_text(no_active_tokens_text) end it 'removes expired tokens from active section' do resource_access_token.update!(expires_at: 5.days.ago) visit resource_settings_access_tokens_path - expect(active_resource_access_tokens).to have_text(no_active_tokens_text) + expect(active_access_tokens).to have_text(no_active_tokens_text) end context 'when resource access token creation is not allowed' do @@ -156,7 +150,7 @@ RSpec.shared_examples 'inactive resource access tokens' do |no_active_tokens_tex visit resource_settings_access_tokens_path accept_gl_confirm(button_text: 'Revoke') { click_on 'Revoke' } - expect(active_resource_access_tokens).to have_text(no_active_tokens_text) + expect(active_access_tokens).to have_text(no_active_tokens_text) end end end diff --git a/spec/support/shared_examples/features/confidential_notes_shared_examples.rb b/spec/support/shared_examples/features/confidential_notes_shared_examples.rb new file mode 100644 index 00000000000..289da025af6 --- /dev/null +++ b/spec/support/shared_examples/features/confidential_notes_shared_examples.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.shared_examples 'confidential notes on issuables' do + include Spec::Support::Helpers::Features::NotesHelpers + + context 'when user does not have permissions' do + it 'does not show confidential note checkbox' do + issuable_parent.add_guest(user) + sign_in(user) + visit(issuable_path) + + page.within('.new-note') do + expect(page).not_to have_selector('[data-testid="internal-note-checkbox"]') + end + end + end + + context 'when user has permissions' do + it 'creates confidential note' do + issuable_parent.add_reporter(user) + sign_in(user) + visit(issuable_path) + + find('[data-testid="internal-note-checkbox"]').click + add_note('Confidential note') + + page.within('.note-header') do + expect(page).to have_selector('[data-testid="internal-note-indicator"]') + end + end + end +end diff --git a/spec/support/shared_examples/features/content_editor_shared_examples.rb b/spec/support/shared_examples/features/content_editor_shared_examples.rb index 7863548e7f3..f01e3c88dad 100644 --- a/spec/support/shared_examples/features/content_editor_shared_examples.rb +++ b/spec/support/shared_examples/features/content_editor_shared_examples.rb @@ -313,5 +313,21 @@ RSpec.shared_examples 'edits content using the content editor' do expect(page).not_to have_css(suggestions_dropdown) end + + it 'scrolls selected item into view when navigating with keyboard' do + type_in_content_editor ':' + + expect(find(suggestions_dropdown)).to have_text('hundred points symbol') + + expect(dropdown_scroll_top).to be 0 + + send_keys :arrow_up + + expect(dropdown_scroll_top).to be > 100 + end + + def dropdown_scroll_top + evaluate_script("document.querySelector('#{suggestions_dropdown} .gl-new-dropdown-inner').scrollTop") + end end end diff --git a/spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb b/spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb index 456175e7113..2cfe353d5d7 100644 --- a/spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb +++ b/spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb @@ -21,15 +21,10 @@ RSpec.shared_examples 'a creatable merge request' do expect(page).to have_content user.name end - click_button 'Milestone' - page.within '.issue-milestone' do - click_link milestone.title - end - + click_button 'Select milestone' + click_button milestone.title expect(find('input[name="merge_request[milestone_id]"]', visible: false).value).to match(milestone.id.to_s) - page.within '.js-milestone-select' do - expect(page).to have_content milestone.title - end + expect(page).to have_button milestone.title click_button 'Labels' page.within '.dropdown-menu-labels' do diff --git a/spec/support/shared_examples/features/editable_merge_request_shared_examples.rb b/spec/support/shared_examples/features/editable_merge_request_shared_examples.rb index 2fff4137934..ea6d1655694 100644 --- a/spec/support/shared_examples/features/editable_merge_request_shared_examples.rb +++ b/spec/support/shared_examples/features/editable_merge_request_shared_examples.rb @@ -20,14 +20,10 @@ RSpec.shared_examples 'an editable merge request' do expect(page).to have_content user.name end - click_button 'Milestone' - page.within '.issue-milestone' do - click_link milestone.title - end + click_button 'Select milestone' + click_button milestone.title expect(find('input[name="merge_request[milestone_id]"]', visible: false).value).to match(milestone.id.to_s) - page.within '.js-milestone-select' do - expect(page).to have_content milestone.title - end + expect(page).to have_button milestone.title click_button 'Labels' page.within '.dropdown-menu-labels' do diff --git a/spec/support/shared_examples/features/packages_shared_examples.rb b/spec/support/shared_examples/features/packages_shared_examples.rb index 7aad5e2de80..f09cf0613a1 100644 --- a/spec/support/shared_examples/features/packages_shared_examples.rb +++ b/spec/support/shared_examples/features/packages_shared_examples.rb @@ -14,7 +14,7 @@ RSpec.shared_examples 'packages list' do |check_project_name: false| end def package_table_row(index) - page.all("#{packages_table_selector} > [data-testid=\"package-row\"]")[index].text + page.all("#{packages_table_selector} [data-testid=\"package-row\"]")[index].text end end diff --git a/spec/support/shared_examples/features/runners_shared_examples.rb b/spec/support/shared_examples/features/runners_shared_examples.rb index 1d4af944187..a7bc19da45f 100644 --- a/spec/support/shared_examples/features/runners_shared_examples.rb +++ b/spec/support/shared_examples/features/runners_shared_examples.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true RSpec.shared_examples 'shows and resets runner registration token' do - include Spec::Support::Helpers::ModalHelpers include Spec::Support::Helpers::Features::RunnersHelpers + include Spec::Support::Helpers::ModalHelpers before do click_on dropdown_text @@ -146,6 +146,23 @@ RSpec.shared_examples 'pauses, resumes and deletes a runner' do end end +RSpec.shared_examples 'deletes runners in bulk' do + describe 'when selecting all for deletion', :js do + before do + check s_('Runners|Select all') + click_button s_('Runners|Delete selected') + + within_modal do + click_on "Permanently delete #{runner_count} runners" + end + + wait_for_requests + end + + it_behaves_like 'shows no runners registered' + end +end + RSpec.shared_examples 'filters by tag' do it 'shows correct runner when tag matches' do expect(page).to have_content found_runner diff --git a/spec/support/shared_examples/features/search/redacted_search_results_shared_examples.rb b/spec/support/shared_examples/features/search/redacted_search_results_shared_examples.rb new file mode 100644 index 00000000000..4d242d0e719 --- /dev/null +++ b/spec/support/shared_examples/features/search/redacted_search_results_shared_examples.rb @@ -0,0 +1,304 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'a redacted search results' do + let_it_be(:user) { create(:user) } + + let_it_be(:accessible_group) { create(:group, :private) } + let_it_be(:accessible_project) { create(:project, :repository, :private, name: 'accessible_project') } + + let_it_be(:group_member) { create(:group_member, group: accessible_group, user: user) } + + let_it_be(:inaccessible_group) { create(:group, :private) } + let_it_be(:inaccessible_project) { create(:project, :repository, :private, name: 'inaccessible_project') } + + let(:search) { 'anything' } + + subject(:result) { search_service.search_objects } + + def found_blob(project) + Gitlab::Search::FoundBlob.new(project: project) + end + + def found_wiki_page(project) + Gitlab::Search::FoundWikiPage.new(found_blob(project)) + end + + def ar_relation(klass, *objects) + klass.id_in(objects.map(&:id)) + end + + def kaminari_array(*objects) + Kaminari.paginate_array(objects).page(1).per(20) + end + + before do + accessible_project.add_maintainer(user) + + allow(search_service) + .to receive_message_chain(:search_results, :objects) + .and_return(unredacted_results) + end + + context 'for issues' do + let(:readable) { create(:issue, project: accessible_project) } + let(:unreadable) { create(:issue, project: inaccessible_project) } + let(:unredacted_results) { ar_relation(Issue, readable, unreadable) } + let(:scope) { 'issues' } + + it 'redacts the inaccessible issue' do + expect(search_service.send(:logger)) + .to receive(:error) + .with(hash_including( + message: "redacted_search_results", + current_user_id: user.id, + query: search, + filtered: array_including( + [ + { class_name: 'Issue', id: unreadable.id, ability: :read_issue } + ]))) + + expect(result).to contain_exactly(readable) + end + end + + context 'for notes' do + let(:readable_merge_request) do + create(:merge_request_with_diffs, target_project: accessible_project, source_project: accessible_project) + end + + let(:readable_note_on_commit) { create(:note_on_commit, project: accessible_project) } + let(:readable_diff_note) { create(:diff_note_on_commit, project: accessible_project) } + let(:readable_note_on_mr) do + create(:discussion_note_on_merge_request, noteable: readable_merge_request, project: accessible_project) + end + + let(:readable_diff_note_on_mr) do + create(:diff_note_on_merge_request, noteable: readable_merge_request, project: accessible_project) + end + + let(:readable_note_on_project_snippet) do + create(:note_on_project_snippet, noteable: readable_merge_request, project: accessible_project) + end + + let(:unreadable_merge_request) do + create(:merge_request_with_diffs, target_project: inaccessible_project, source_project: inaccessible_project) + end + + let(:unreadable_note_on_commit) { create(:note_on_commit, project: inaccessible_project) } + let(:unreadable_diff_note) { create(:diff_note_on_commit, project: inaccessible_project) } + let(:unreadable_note_on_mr) do + create(:discussion_note_on_merge_request, noteable: unreadable_merge_request, project: inaccessible_project) + end + + let(:unreadable_note_on_project_snippet) do + create(:note_on_project_snippet, noteable: unreadable_merge_request, project: inaccessible_project) + end + + let(:unredacted_results) do + ar_relation(Note, + readable_note_on_commit, + readable_diff_note, + readable_note_on_mr, + readable_diff_note_on_mr, + readable_note_on_project_snippet, + unreadable_note_on_commit, + unreadable_diff_note, + unreadable_note_on_mr, + unreadable_note_on_project_snippet) + end + + let(:scope) { 'notes' } + + it 'redacts the inaccessible notes' do + expect(search_service.send(:logger)) + .to receive(:error) + .with(hash_including( + message: "redacted_search_results", + current_user_id: user.id, + query: search, + filtered: array_including( + [ + { class_name: 'Note', id: unreadable_note_on_commit.id, ability: :read_note }, + { class_name: 'DiffNote', id: unreadable_diff_note.id, ability: :read_note }, + { class_name: 'DiscussionNote', id: unreadable_note_on_mr.id, ability: :read_note }, + { class_name: 'Note', id: unreadable_note_on_project_snippet.id, ability: :read_note } + ]))) + + expect(result).to contain_exactly(readable_note_on_commit, + readable_diff_note, + readable_note_on_mr, + readable_diff_note_on_mr, + readable_note_on_project_snippet) + end + end + + context 'for merge_requests' do + let(:readable) { create(:merge_request, source_project: accessible_project) } + let(:unreadable) { create(:merge_request, source_project: inaccessible_project) } + let(:unredacted_results) { ar_relation(MergeRequest, readable, unreadable) } + let(:scope) { 'merge_requests' } + + it 'redacts the inaccessible merge request' do + expect(search_service.send(:logger)) + .to receive(:error) + .with(hash_including( + message: "redacted_search_results", + current_user_id: user.id, + query: search, + filtered: array_including( + [ + { class_name: 'MergeRequest', id: unreadable.id, ability: :read_merge_request } + ]))) + + expect(result).to contain_exactly(readable) + end + + context 'with :with_api_entity_associations' do + let(:unredacted_results) { ar_relation(MergeRequest.with_api_entity_associations, readable, unreadable) } + + it_behaves_like "redaction limits N+1 queries", limit: 8 + end + end + + context 'for blobs' do + let(:readable) { found_blob(accessible_project) } + let(:unreadable) { found_blob(inaccessible_project) } + let(:unredacted_results) { kaminari_array(readable, unreadable) } + let(:scope) { 'blobs' } + + it 'redacts the inaccessible blob' do + expect(search_service.send(:logger)) + .to receive(:error) + .with(hash_including( + message: "redacted_search_results", + current_user_id: user.id, + query: search, + filtered: array_including( + [ + { class_name: 'Gitlab::Search::FoundBlob', id: unreadable.id, ability: :read_blob } + ]))) + + expect(result).to contain_exactly(readable) + end + end + + context 'for wiki blobs' do + let(:readable) { found_wiki_page(accessible_project) } + let(:unreadable) { found_wiki_page(inaccessible_project) } + let(:unredacted_results) { kaminari_array(readable, unreadable) } + let(:scope) { 'wiki_blobs' } + + it 'redacts the inaccessible blob' do + expect(search_service.send(:logger)) + .to receive(:error) + .with(hash_including( + message: "redacted_search_results", + current_user_id: user.id, + query: search, + filtered: array_including( + [ + { class_name: 'Gitlab::Search::FoundWikiPage', id: unreadable.id, ability: :read_wiki_page } + ]))) + + expect(result).to contain_exactly(readable) + end + end + + context 'for project snippets' do + let(:readable) { create(:project_snippet, project: accessible_project) } + let(:unreadable) { create(:project_snippet, project: inaccessible_project) } + let(:unredacted_results) { ar_relation(ProjectSnippet, readable, unreadable) } + let(:scope) { 'snippet_titles' } + + it 'redacts the inaccessible snippet' do + expect(search_service.send(:logger)) + .to receive(:error) + .with(hash_including( + message: "redacted_search_results", + current_user_id: user.id, + query: search, + filtered: array_including( + [ + { class_name: 'ProjectSnippet', id: unreadable.id, ability: :read_snippet } + ]))) + + expect(result).to contain_exactly(readable) + end + + context 'with :with_api_entity_associations' do + it_behaves_like "redaction limits N+1 queries", limit: 14 + end + end + + context 'for personal snippets' do + let(:readable) { create(:personal_snippet, :private, author: user) } + let(:unreadable) { create(:personal_snippet, :private) } + let(:unredacted_results) { ar_relation(PersonalSnippet, readable, unreadable) } + let(:scope) { 'snippet_titles' } + + it 'redacts the inaccessible snippet' do + expect(search_service.send(:logger)) + .to receive(:error) + .with(hash_including( + message: "redacted_search_results", + current_user_id: user.id, + query: search, + filtered: array_including( + [ + { class_name: 'PersonalSnippet', id: unreadable.id, ability: :read_snippet } + ]))) + + expect(result).to contain_exactly(readable) + end + + context 'with :with_api_entity_associations' do + it_behaves_like "redaction limits N+1 queries", limit: 4 + end + end + + context 'for commits' do + let(:readable) { accessible_project.commit } + let(:unreadable) { inaccessible_project.commit } + let(:unredacted_results) { kaminari_array(readable, unreadable) } + let(:scope) { 'commits' } + + it 'redacts the inaccessible commit' do + expect(search_service.send(:logger)) + .to receive(:error) + .with(hash_including( + message: "redacted_search_results", + current_user_id: user.id, + query: search, + filtered: array_including( + [ + { class_name: 'Commit', id: unreadable.id, ability: :read_commit } + ]))) + + expect(result).to contain_exactly(readable) + end + end + + context 'for users' do + let(:other_user) { create(:user) } + let(:unredacted_results) { ar_relation(User, user, other_user) } + let(:scope) { 'users' } + + it 'passes the users through' do + # Users are always visible to everyone + expect(result).to contain_exactly(user, other_user) + end + end +end + +RSpec.shared_examples "redaction limits N+1 queries" do |limit:| + it 'does not exceed the query limit' do + # issuing the query to remove the data loading call + unredacted_results.to_a + + # only the calls from the redaction are left + query = ActiveRecord::QueryRecorder.new { result } + + # these are the project authorization calls, which are not preloaded + expect(query.count).to be <= limit + end +end diff --git a/spec/support/shared_examples/features/search/search_timeouts_shared_examples.rb b/spec/support/shared_examples/features/search/search_timeouts_shared_examples.rb index 84dc2b20ddc..cc74c977064 100644 --- a/spec/support/shared_examples/features/search/search_timeouts_shared_examples.rb +++ b/spec/support/shared_examples/features/search/search_timeouts_shared_examples.rb @@ -1,23 +1,23 @@ # frozen_string_literal: true RSpec.shared_examples 'search timeouts' do |scope| + let(:additional_params) { {} } + context 'when search times out' do before do - stub_feature_flags(search_page_vertical_nav: false) allow_next_instance_of(SearchService) do |service| allow(service).to receive(:search_objects).and_raise(ActiveRecord::QueryCanceled) end - visit(search_path(search: 'test', scope: scope)) + visit(search_path(search: 'test', scope: scope, **additional_params)) end it 'renders timeout information' do - # expect(page).to have_content('This endpoint has been requested too many times.') expect(page).to have_content('Your search timed out') end it 'sets tab count to 0' do - expect(page.find('.search-filter .active')).to have_text('0') + expect(page.find('[data-testid="search-filter"] .active')).to have_text('0') end end end diff --git a/spec/support/shared_examples/features/variable_list_shared_examples.rb b/spec/support/shared_examples/features/variable_list_shared_examples.rb index d1e5046a39e..f0b72cfaee3 100644 --- a/spec/support/shared_examples/features/variable_list_shared_examples.rb +++ b/spec/support/shared_examples/features/variable_list_shared_examples.rb @@ -31,8 +31,8 @@ RSpec.shared_examples 'variable list' do |is_admin| wait_for_requests page.within('[data-testid="ci-variable-table"]') do - expect(find('.js-ci-variable-row:nth-child(1) td[data-label="Key"]').text).to eq('key') - expect(find('.js-ci-variable-row:nth-child(1) td[data-label="Protected"] svg[data-testid="mobile-issue-close-icon"]')).to be_present + expect(find(".js-ci-variable-row:nth-child(1) td[data-label='#{s_('CiVariables|Key')}']").text).to eq('key') + expect(find(".js-ci-variable-row:nth-child(1) td[data-label='#{s_('CiVariables|Options')}']")).to have_content(s_('CiVariables|Protected')) end end @@ -46,26 +46,26 @@ RSpec.shared_examples 'variable list' do |is_admin| wait_for_requests page.within('[data-testid="ci-variable-table"]') do - expect(find('.js-ci-variable-row:nth-child(1) td[data-label="Key"]').text).to eq('key') - expect(find('.js-ci-variable-row:nth-child(1) td[data-label="Masked"] svg[data-testid="close-icon"]')).to be_present + expect(find(".js-ci-variable-row:nth-child(1) td[data-label='#{s_('CiVariables|Key')}']").text).to eq('key') + expect(find(".js-ci-variable-row:nth-child(1) td[data-label='#{s_('CiVariables|Options')}']")).not_to have_content(s_('CiVariables|Masked')) end end it 'reveals and hides variables' do page.within('[data-testid="ci-variable-table"]') do expect(first('.js-ci-variable-row td[data-label="Key"]').text).to eq(variable.key) - expect(page).to have_content('*' * 17) + expect(page).to have_content('*' * 5) click_button('Reveal value') expect(first('.js-ci-variable-row td[data-label="Key"]').text).to eq(variable.key) expect(first('.js-ci-variable-row td[data-label="Value"]').text).to eq(variable.value) - expect(page).not_to have_content('*' * 17) + expect(page).not_to have_content('*' * 5) click_button('Hide value') expect(first('.js-ci-variable-row td[data-label="Key"]').text).to eq(variable.key) - expect(page).to have_content('*' * 17) + expect(page).to have_content('*' * 5) end end @@ -116,7 +116,8 @@ RSpec.shared_examples 'variable list' do |is_admin| wait_for_requests page.within('[data-testid="ci-variable-table"]') do - expect(find('.js-ci-variable-row:nth-child(1) td[data-label="Masked"] svg[data-testid="close-icon"]')).to be_present + expect(find(".js-ci-variable-row:nth-child(1) td[data-label='#{s_('CiVariables|Options')}']")).to have_content(s_('CiVariables|Protected')) + expect(find(".js-ci-variable-row:nth-child(1) td[data-label='#{s_('CiVariables|Options')}']")).not_to have_content(s_('CiVariables|Masked')) end end @@ -144,7 +145,7 @@ RSpec.shared_examples 'variable list' do |is_admin| end page.within('[data-testid="ci-variable-table"]') do - expect(find('.js-ci-variable-row:nth-child(1) td[data-label="Masked"] svg[data-testid="mobile-issue-close-icon"]')).to be_present + expect(find(".js-ci-variable-row:nth-child(1) td[data-label='#{s_('CiVariables|Options')}']")).to have_content(s_('CiVariables|Masked')) end end diff --git a/spec/support/shared_examples/finders/issues_finder_shared_examples.rb b/spec/support/shared_examples/finders/issues_finder_shared_examples.rb index f62c9c00006..8b3a344a841 100644 --- a/spec/support/shared_examples/finders/issues_finder_shared_examples.rb +++ b/spec/support/shared_examples/finders/issues_finder_shared_examples.rb @@ -585,7 +585,7 @@ RSpec.shared_examples 'issues or work items finder' do |factory, execute_context end context 'when full-text search is disabled' do - let(:search_term) { 'somet' } + let(:search_term) { 'ometh' } before do stub_feature_flags(issues_full_text_search: false) diff --git a/spec/support/shared_examples/graphql/mutations/incident_management/timeline_events_shared_examples.rb b/spec/support/shared_examples/graphql/mutations/incident_management/timeline_events_shared_examples.rb new file mode 100644 index 00000000000..fbfd1af2601 --- /dev/null +++ b/spec/support/shared_examples/graphql/mutations/incident_management/timeline_events_shared_examples.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'timeline event mutation responds with validation error' do |error_message:| + it 'responds with a validation error' do + post_graphql_mutation(mutation, current_user: user) + + expect(response).to have_gitlab_http_status(:success) + expect(mutation_response['errors']).to match_array([error_message]) + end +end diff --git a/spec/support/shared_examples/graphql/notes_creation_shared_examples.rb b/spec/support/shared_examples/graphql/notes_creation_shared_examples.rb index 0aa3bf8944f..bdd4dbfe209 100644 --- a/spec/support/shared_examples/graphql/notes_creation_shared_examples.rb +++ b/spec/support/shared_examples/graphql/notes_creation_shared_examples.rb @@ -78,7 +78,7 @@ RSpec.shared_examples 'a Note mutation when there are rate limit validation erro context 'when the user is in the allowlist' do before do - stub_application_setting(notes_create_limit_allowlist: ["#{current_user.username}"]) + stub_application_setting(notes_create_limit_allowlist: [current_user.username.to_s]) end it_behaves_like 'a Note mutation that creates a Note' diff --git a/spec/support/shared_examples/graphql/resolvers/packages_resolvers_shared_examples.rb b/spec/support/shared_examples/graphql/resolvers/packages_resolvers_shared_examples.rb index 3017f62a7c9..aadc061f175 100644 --- a/spec/support/shared_examples/graphql/resolvers/packages_resolvers_shared_examples.rb +++ b/spec/support/shared_examples/graphql/resolvers/packages_resolvers_shared_examples.rb @@ -25,7 +25,7 @@ RSpec.shared_examples 'group and projects packages resolver' do end %w[CREATED_DESC NAME_DESC VERSION_DESC TYPE_ASC].each do |order| - context "#{order}" do + context order.to_s do let(:args) { { sort: order } } it { is_expected.to eq([maven_package, conan_package]) } @@ -33,7 +33,7 @@ RSpec.shared_examples 'group and projects packages resolver' do end %w[CREATED_ASC NAME_ASC VERSION_ASC TYPE_DESC].each do |order| - context "#{order}" do + context order.to_s do let(:args) { { sort: order } } it { is_expected.to eq([conan_package, maven_package]) } diff --git a/spec/support/shared_examples/graphql/types/gitlab_style_deprecations_shared_examples.rb b/spec/support/shared_examples/graphql/types/gitlab_style_deprecations_shared_examples.rb index 2d7da9f9f00..67576a18c80 100644 --- a/spec/support/shared_examples/graphql/types/gitlab_style_deprecations_shared_examples.rb +++ b/spec/support/shared_examples/graphql/types/gitlab_style_deprecations_shared_examples.rb @@ -86,4 +86,25 @@ RSpec.shared_examples 'Gitlab-style deprecations' do eq("`alpha` and `deprecated` arguments cannot be passed at the same time") ) end + + describe 'visible?' do + let(:ctx) { {} } + + it 'defaults to true' do + expect(subject).to be_visible(ctx) + end + + context 'when subject is deprecated' do + let(:arguments) { { deprecated: { milestone: '1.10', reason: :renamed } } } + + it 'defaults to true' do + expect(subject(arguments)).to be_visible(ctx) + end + + it 'returns false if `remove_deprecated` is true in context' do + ctx = { remove_deprecated: true } + expect(subject(arguments)).not_to be_visible(ctx) + end + end + end end diff --git a/spec/support/shared_examples/lib/cache_helpers_shared_examples.rb b/spec/support/shared_examples/lib/cache_helpers_shared_examples.rb index 2e00abe2f8e..6cdd7954b5f 100644 --- a/spec/support/shared_examples/lib/cache_helpers_shared_examples.rb +++ b/spec/support/shared_examples/lib/cache_helpers_shared_examples.rb @@ -129,6 +129,7 @@ RSpec.shared_examples_for 'collection cache helper' do before do allow(::Gitlab::Metrics::WebTransaction).to receive(:current).and_return(transaction) allow(transaction).to receive(:increment) + allow(Gitlab::ApplicationContext).to receive(:current_context_attribute).with(any_args).and_call_original allow(Gitlab::ApplicationContext).to receive(:current_context_attribute).with(:caller_id).and_return(caller_id) end diff --git a/spec/support/shared_examples/lib/email/email_shared_examples.rb b/spec/support/shared_examples/lib/email/email_shared_examples.rb new file mode 100644 index 00000000000..26655a71fec --- /dev/null +++ b/spec/support/shared_examples/lib/email/email_shared_examples.rb @@ -0,0 +1,140 @@ +# frozen_string_literal: true + +# Set the particular setting as a key-value pair +# Setting method is different depending on klass and must be defined in the calling spec +def stub_email_setting(key_value_pairs) + case setting_name + when :incoming_email + stub_incoming_email_setting(key_value_pairs) + when :service_desk_email + stub_service_desk_email_setting(key_value_pairs) + end +end + +RSpec.shared_examples_for 'enabled? method for email' do + using RSpec::Parameterized::TableSyntax + + subject { described_class.enabled? } + + where(:value, :address, :result) do + false | nil | false + false | 'replies+%{key}@example.com' | false + true | nil | false + true | 'replies+%{key}@example.com' | true + end + + with_them do + before do + stub_email_setting(enabled: value) + stub_email_setting(address: address) + end + + it { is_expected.to eq result } + end +end + +RSpec.shared_examples_for 'supports_wildcard? method for email' do + subject { described_class.supports_wildcard? } + + before do + stub_incoming_email_setting(address: value) + end + + context 'when address contains the wildcard placeholder' do + let(:value) { 'replies+%{key}@example.com' } + + it 'confirms that wildcard is supported' do + expect(subject).to be_truthy + end + end + + context "when address doesn't contain the wildcard placeholder" do + let(:value) { 'replies@example.com' } + + it 'returns that wildcard is not supported' do + expect(subject).to be_falsey + end + end + + context 'when address is nil' do + let(:value) { nil } + + it 'returns that wildcard is not supported' do + expect(subject).to be_falsey + end + end +end + +RSpec.shared_examples_for 'unsubscribe_address method for email' do + before do + stub_incoming_email_setting(address: 'replies+%{key}@example.com') + end + + it 'returns the address with interpolated reply key and unsubscribe suffix' do + expect(described_class.unsubscribe_address('key')) + .to eq("replies+key#{Gitlab::Email::Common::UNSUBSCRIBE_SUFFIX}@example.com") + end +end + +RSpec.shared_examples_for 'key_from_fallback_message_id method for email' do + it 'returns reply key' do + expect(described_class.key_from_fallback_message_id('reply-key@localhost')).to eq('key') + end +end + +RSpec.shared_examples_for 'supports_issue_creation? method for email' do + using RSpec::Parameterized::TableSyntax + + subject { described_class.supports_issue_creation? } + + where(:enabled_value, :supports_wildcard_value, :result) do + false | false | false + false | true | false + true | false | false + true | true | true + end + + with_them do + before do + allow(described_class).to receive(:enabled?).and_return(enabled_value) + allow(described_class).to receive(:supports_wildcard?).and_return(supports_wildcard_value) + end + + it { is_expected.to eq result } + end +end + +RSpec.shared_examples_for 'reply_address method for email' do + before do + stub_incoming_email_setting(address: "replies+%{key}@example.com") + end + + it "returns the address with an interpolated reply key" do + expect(described_class.reply_address("key")).to eq("replies+key@example.com") + end +end + +RSpec.shared_examples_for 'scan_fallback_references method for email' do + let(:references) do + '<issue_1@localhost>' \ + ' <reply-59d8df8370b7e95c5a49fbf86aeb2c93@localhost>' \ + ',<exchange@microsoft.com>' + end + + it 'returns reply key' do + expect(described_class.scan_fallback_references(references)) + .to eq(%w[issue_1@localhost + reply-59d8df8370b7e95c5a49fbf86aeb2c93@localhost + exchange@microsoft.com]) + end +end + +RSpec.shared_examples_for 'common email methods' do + it_behaves_like 'enabled? method for email' + it_behaves_like 'supports_wildcard? method for email' + it_behaves_like 'key_from_fallback_message_id method for email' + it_behaves_like 'supports_issue_creation? method for email' + it_behaves_like 'reply_address method for email' + it_behaves_like 'unsubscribe_address method for email' + it_behaves_like 'scan_fallback_references method for email' +end diff --git a/spec/support/shared_examples/lib/gitlab/database/reestablished_connection_stack_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/database/reestablished_connection_stack_shared_examples.rb index d14216ec5ff..22b4f9f583c 100644 --- a/spec/support/shared_examples/lib/gitlab/database/reestablished_connection_stack_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/database/reestablished_connection_stack_shared_examples.rb @@ -3,9 +3,6 @@ RSpec.shared_context 'reconfigures connection stack' do |db_config_name| before do skip_if_multiple_databases_not_setup - - # Due to lib/gitlab/database/load_balancing/configuration.rb:92 requiring RequestStore - # we cannot use stub_feature_flags(force_no_sharing_primary_model: true) Gitlab::Database.database_base_models.each do |_, model_class| allow(model_class.load_balancer.configuration).to receive(:use_dedicated_connection?).and_return(true) end diff --git a/spec/support/shared_examples/lib/gitlab/event_store_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/event_store_shared_examples.rb index db2f2f2d0f0..e97b9ad969f 100644 --- a/spec/support/shared_examples/lib/gitlab/event_store_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/event_store_shared_examples.rb @@ -15,6 +15,16 @@ RSpec.shared_examples 'subscribes to event' do it_behaves_like 'an idempotent worker' end +RSpec.shared_examples 'ignores the published event' do + include AfterNextHelpers + + it 'does not consume the published event', :sidekiq_inline do + expect_next(described_class).not_to receive(:handle_event) + + ::Gitlab::EventStore.publish(event) + end +end + def consume_event(subscriber:, event:) subscriber.new.perform(event.class.name, event.data) end diff --git a/spec/support/shared_examples/lib/gitlab/experimentation_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/experimentation_shared_examples.rb deleted file mode 100644 index fdca326dbea..00000000000 --- a/spec/support/shared_examples/lib/gitlab/experimentation_shared_examples.rb +++ /dev/null @@ -1,22 +0,0 @@ -# frozen_string_literal: true - -RSpec.shared_examples 'tracks assignment and records the subject' do |experiment, subject_type| - before do - stub_experiments(experiment => true) - end - - it 'tracks the assignment', :experiment do - expect(experiment(experiment)) - .to track(:assignment) - .with_context(subject_type => subject) - .on_next_instance - - action - end - - it 'records the subject' do - expect(Experiment).to receive(:add_subject).with(experiment.to_s, variant: anything, subject: subject) - - action - end -end diff --git a/spec/support/shared_examples/lib/gitlab/gitaly_client_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/gitaly_client_shared_examples.rb new file mode 100644 index 00000000000..f26b9a4a7bd --- /dev/null +++ b/spec/support/shared_examples/lib/gitlab/gitaly_client_shared_examples.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +def raw_repo_without_container(repository) + Gitlab::Git::Repository.new(repository.shard, + "#{repository.disk_path}.git", + repository.repo_type.identifier_for_container(repository.container), + repository.container.full_path) +end + +RSpec.shared_examples 'Gitaly feature flag actors are inferred from repository' do + it 'captures correct actors' do + service.repository_actor = repository + + expect(service.repository_actor.flipper_id).to eql(repository.flipper_id) + + if expected_project.nil? + expect(service.project_actor).to be(nil) + else + expect(service.project_actor.flipper_id).to eql(expected_project.flipper_id) + end + + if expected_group.nil? + expect(service.group_actor).to be(nil) + else + expect(service.group_actor.flipper_id).to eql(expected_group.flipper_id) + end + end + + it 'does not issues SQL queries after the first invocation' do + service.repository_actor = repository + + service.repository_actor + service.project_actor + service.group_actor + + recorder = ActiveRecord::QueryRecorder.new do + 3.times do + service.repository_actor + service.project_actor + service.group_actor + end + end + + expect(recorder.count).to be(0) + end +end diff --git a/spec/support/shared_examples/lib/gitlab/template/template_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/template/template_shared_examples.rb index 4b4a7f4ce9d..a2a4c57d62e 100644 --- a/spec/support/shared_examples/lib/gitlab/template/template_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/template/template_shared_examples.rb @@ -52,7 +52,7 @@ RSpec.shared_examples 'acts as branch pipeline' do |jobs| context 'when branch pipeline' do let(:pipeline_branch) { default_branch } let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_branch) } - let(:pipeline) { service.execute!(:push).payload } + let(:pipeline) { service.execute(:push).payload } it 'includes a job' do expect(pipeline.builds.pluck(:name)).to match_array(jobs) diff --git a/spec/support/shared_examples/lib/sentry/client_shared_examples.rb b/spec/support/shared_examples/lib/sentry/client_shared_examples.rb index 71b32005c55..e0b411e1e2a 100644 --- a/spec/support/shared_examples/lib/sentry/client_shared_examples.rb +++ b/spec/support/shared_examples/lib/sentry/client_shared_examples.rb @@ -78,8 +78,8 @@ end # Expects to following variables: # - subject # - sentry_api_response -# - sentry_url, token - only if enabled_by_default: false -RSpec.shared_examples 'Sentry API response size limit' do |enabled_by_default: false| +# - sentry_url, token +RSpec.shared_examples 'Sentry API response size limit' do let(:invalid_deep_size) { instance_double(Gitlab::Utils::DeepSize, valid?: false) } before do @@ -89,35 +89,8 @@ RSpec.shared_examples 'Sentry API response size limit' do |enabled_by_default: f .and_return(invalid_deep_size) end - if enabled_by_default - it 'raises an exception when response is too large' do - expect { subject }.to raise_error(ErrorTracking::SentryClient::ResponseInvalidSizeError, - 'Sentry API response is too big. Limit is 1 MB.') - end - else - context 'when guarded by feature flag' do - let(:client) do - ErrorTracking::SentryClient.new(sentry_url, token, validate_size_guarded_by_feature_flag: feature_flag) - end - - context 'with feature flag enabled' do - let(:feature_flag) { true } - - it 'raises an exception when response is too large' do - expect { subject }.to raise_error(ErrorTracking::SentryClient::ResponseInvalidSizeError, - 'Sentry API response is too big. Limit is 1 MB.') - end - end - - context 'with feature flag disabled' do - let(:feature_flag) { false } - - it 'does not check the limit and thus not raise' do - expect { subject }.not_to raise_error - - expect(Gitlab::Utils::DeepSize).not_to have_received(:new) - end - end - end + it 'raises an exception when response is too large' do + expect { subject }.to raise_error(ErrorTracking::SentryClient::ResponseInvalidSizeError, + 'Sentry API response is too big. Limit is 1 MB.') end end diff --git a/spec/support/shared_examples/mailers/notify_shared_examples.rb b/spec/support/shared_examples/mailers/notify_shared_examples.rb index 20ed380fb18..919311adc96 100644 --- a/spec/support/shared_examples/mailers/notify_shared_examples.rb +++ b/spec/support/shared_examples/mailers/notify_shared_examples.rb @@ -44,12 +44,12 @@ end RSpec.shared_examples 'an email with X-GitLab headers containing IDs' do it 'has X-GitLab-*-ID header' do - is_expected.to have_header "X-GitLab-#{model.class.name}-ID", "#{model.id}" + is_expected.to have_header "X-GitLab-#{model.class.name}-ID", model.id.to_s end it 'has X-GitLab-*-IID header if model has iid defined' do if model.respond_to?(:iid) - is_expected.to have_header "X-GitLab-#{model.class.name}-IID", "#{model.iid}" + is_expected.to have_header "X-GitLab-#{model.class.name}-IID", model.iid.to_s else expect(subject.header["X-GitLab-#{model.class.name}-IID"]).to eq nil end diff --git a/spec/support/shared_examples/metrics/active_record_subscriber_shared_examples.rb b/spec/support/shared_examples/metrics/active_record_subscriber_shared_examples.rb index 3f187a7e9e4..ef4b08c7865 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 @@ -37,7 +37,8 @@ RSpec.shared_examples 'store ActiveRecord info in RequestStore' do |db_role| Gitlab::WithRequestStore.with_request_store do subscriber.sql(event) - expected = if db_role == :primary + expected = case db_role + when :primary transform_hash(expected_payload_defaults, { db_count: record_query ? 1 : 0, db_write_count: record_write_query ? 1 : 0, @@ -53,7 +54,7 @@ RSpec.shared_examples 'store ActiveRecord info in RequestStore' do |db_role| db_primary_wal_cached_count: record_wal_query && record_cached_query ? 1 : 0, "db_#{db_config_name}_wal_cached_count": record_wal_query && record_cached_query ? 1 : 0 }) - elsif db_role == :replica + when :replica transform_hash(expected_payload_defaults, { db_count: record_query ? 1 : 0, db_write_count: record_write_query ? 1 : 0, diff --git a/spec/support/shared_examples/models/concerns/integrations/base_slack_notification_shared_examples.rb b/spec/support/shared_examples/models/concerns/integrations/base_slack_notification_shared_examples.rb new file mode 100644 index 00000000000..f0581333b28 --- /dev/null +++ b/spec/support/shared_examples/models/concerns/integrations/base_slack_notification_shared_examples.rb @@ -0,0 +1,150 @@ +# frozen_string_literal: true + +RSpec.shared_examples Integrations::BaseSlackNotification do |factory:| + describe '#execute' do + let_it_be(:project) { create(:project, :repository, :wiki_repo) } + let_it_be(:integration) { create(factory, branches_to_be_notified: 'all', project: project) } + + before do + stub_request(:post, integration.webhook) + end + + it 'uses only known events', :aggregate_failures do + described_class::SUPPORTED_EVENTS_FOR_USAGE_LOG.each do |action| + expect( + Gitlab::UsageDataCounters::HLLRedisCounter.known_event?("i_ecosystem_slack_service_#{action}_notification") + ).to be true + end + end + + context 'when hook data includes a user object' do + let_it_be(:user) { create_default(:user) } + + shared_examples 'increases the usage data counter' do |event_name| + subject(:execute) { integration.execute(data) } + + it 'increases the usage data counter' do + expect(Gitlab::UsageDataCounters::HLLRedisCounter) + .to receive(:track_event).with(event_name, values: user.id).and_call_original + + execute + end + + it_behaves_like 'Snowplow event tracking' do + let(:feature_flag_name) { :route_hll_to_snowplow_phase2 } + let(:category) { described_class.to_s } + let(:action) { 'perform_integrations_action' } + let(:namespace) { project.namespace } + let(:label) { 'redis_hll_counters.ecosystem.ecosystem_total_unique_counts_monthly' } + let(:property) { event_name } + end + end + + context 'when event is not supported for usage log' do + let_it_be(:pipeline) { create(:ci_pipeline, project: project) } + + let(:data) { Gitlab::DataBuilder::Pipeline.build(pipeline) } + + it 'does not increase the usage data counter' do + expect(Gitlab::UsageDataCounters::HLLRedisCounter) + .not_to receive(:track_event).with('i_ecosystem_slack_service_pipeline_notification', values: user.id) + + integration.execute(data) + end + end + + context 'for issue notification' do + let_it_be(:issue) { create(:issue, project: project) } + + let(:data) { issue.to_hook_data(user) } + + it_behaves_like 'increases the usage data counter', 'i_ecosystem_slack_service_issue_notification' + end + + context 'for push notification' do + let(:data) { Gitlab::DataBuilder::Push.build_sample(project, user) } + + it_behaves_like 'increases the usage data counter', 'i_ecosystem_slack_service_push_notification' + end + + context 'for deployment notification' do + let_it_be(:deployment) { create(:deployment, project: project, user: user) } + + let(:data) { Gitlab::DataBuilder::Deployment.build(deployment, deployment.status, Time.current) } + + it_behaves_like 'increases the usage data counter', 'i_ecosystem_slack_service_deployment_notification' + end + + context 'for wiki_page notification' do + let_it_be(:wiki_page) do + create(:wiki_page, wiki: project.wiki, message: 'user created page: Awesome wiki_page') + end + + let(:data) { Gitlab::DataBuilder::WikiPage.build(wiki_page, user, 'create') } + + before do + # Skip this method that is not relevant to this test to prevent having + # to update project which is frozen + allow(project.wiki).to receive(:after_wiki_activity) + end + + it_behaves_like 'increases the usage data counter', 'i_ecosystem_slack_service_wiki_page_notification' + end + + context 'for merge_request notification' do + let_it_be(:merge_request) { create(:merge_request, source_project: project) } + + let(:data) { merge_request.to_hook_data(user) } + + it_behaves_like 'increases the usage data counter', 'i_ecosystem_slack_service_merge_request_notification' + end + + context 'for note notification' do + let_it_be(:issue_note) { create(:note_on_issue, project: project, note: 'issue note') } + + let(:data) { Gitlab::DataBuilder::Note.build(issue_note, user) } + + it_behaves_like 'increases the usage data counter', 'i_ecosystem_slack_service_note_notification' + end + + context 'for tag_push notification' do + let(:oldrev) { Gitlab::Git::BLANK_SHA } + let(:newrev) { '8a2a6eb295bb170b34c24c76c49ed0e9b2eaf34b' } # gitlab-test: git rev-parse refs/tags/v1.1.0 + let(:ref) { 'refs/tags/v1.1.0' } + let(:data) do + Git::TagHooksService.new(project, user, change: { oldrev: oldrev, newrev: newrev, ref: ref }).send(:push_data) + end + + it_behaves_like 'increases the usage data counter', 'i_ecosystem_slack_service_tag_push_notification' + end + + context 'for confidential note notification' do + let_it_be(:confidential_issue_note) do + create(:note_on_issue, project: project, note: 'issue note', confidential: true) + end + + let(:data) { Gitlab::DataBuilder::Note.build(confidential_issue_note, user) } + + it_behaves_like 'increases the usage data counter', 'i_ecosystem_slack_service_confidential_note_notification' + end + + context 'for confidential issue notification' do + let_it_be(:issue) { create(:issue, project: project, confidential: true) } + + let(:data) { issue.to_hook_data(user) } + + it_behaves_like 'increases the usage data counter', 'i_ecosystem_slack_service_confidential_issue_notification' + end + end + + context 'when hook data does not include a user' do + let(:data) { Gitlab::DataBuilder::Pipeline.build(create(:ci_pipeline, project: project)) } + + it 'does not increase the usage data counter' do + expect(Gitlab::UsageDataCounters::HLLRedisCounter).not_to receive(:track_event) + + integration.execute(data) + end + end + end +end diff --git a/spec/support/shared_examples/models/concerns/integrations/slack_mattermost_notifier_shared_examples.rb b/spec/support/shared_examples/models/concerns/integrations/slack_mattermost_notifier_shared_examples.rb index 7512a9f2855..974fc8f402a 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 @@ -152,7 +152,7 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |integration_name end context 'issue events' do - let_it_be(:issue) { create(:issue) } + let_it_be(:issue) { create(:issue, project: project) } let(:data) { issue.to_hook_data(user) } @@ -192,7 +192,7 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |integration_name end context 'merge request events' do - let_it_be(:merge_request) { create(:merge_request) } + let_it_be(:merge_request) { create(:merge_request, source_project: project) } let(:data) { merge_request.to_hook_data(user) } @@ -210,7 +210,7 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |integration_name end context 'wiki page events' do - let_it_be(:wiki_page) { create(:wiki_page, wiki: project.wiki, message: 'user created page: Awesome wiki_page') } + let_it_be(:wiki_page) { create(:wiki_page, wiki: project.wiki, project: project, message: 'user created page: Awesome wiki_page') } let(:data) { Gitlab::DataBuilder::WikiPage.build(wiki_page, user, 'create') } @@ -228,7 +228,7 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |integration_name end context 'deployment events' do - let_it_be(:deployment) { create(:deployment) } + let_it_be(:deployment) { create(:deployment, project: project) } let(:data) { Gitlab::DataBuilder::Deployment.build(deployment, 'created', Time.current) } @@ -275,8 +275,8 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |integration_name end describe 'Push events' do - let(:user) { create(:user) } - let(:project) { create(:project, :repository, creator: user) } + let_it_be(:user) { create(:user) } + let_it_be_with_reload(:project) { create(:project, :repository, creator: user) } before do allow(chat_integration).to receive_messages( @@ -327,7 +327,7 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |integration_name end context 'on a protected branch' do - before do + before(:all) do create(:protected_branch, :create_branch_on_repository, project: project, name: 'a-protected-branch') end @@ -369,7 +369,7 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |integration_name end context 'on a protected branch with protected branches defined using wildcards' do - before do + before(:all) do create(:protected_branch, :create_branch_on_repository, repository_branch_name: '1-stable', project: project, name: '*-stable') end @@ -450,8 +450,8 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |integration_name end describe 'Note events' do - let(:user) { create(:user) } - let(:project) { create(:project, :repository, creator: user) } + let_it_be(:user) { create(:user) } + let_it_be_with_reload(:project) { create(:project, :repository, creator: user) } before do allow(chat_integration).to receive_messages( @@ -519,8 +519,8 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |integration_name end describe 'Pipeline events' do - let(:user) { create(:user) } - let(:project) { create(:project, :repository, creator: user) } + let_it_be(:user) { create(:user) } + let_it_be_with_reload(:project) { create(:project, :repository, creator: user) } let(:pipeline) do create(:ci_pipeline, project: project, status: status, @@ -582,7 +582,7 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |integration_name end context 'on a protected branch' do - before do + before(:all) do create(:protected_branch, :create_branch_on_repository, project: project, name: 'a-protected-branch') end @@ -612,7 +612,7 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |integration_name end context 'on a protected branch with protected branches defined usin wildcards' do - before do + before(:all) do create(:protected_branch, :create_branch_on_repository, repository_branch_name: '1-stable', project: project, name: '*-stable') end @@ -673,7 +673,7 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |integration_name let_it_be(:user) { create(:user) } let_it_be_with_reload(:project) { create(:project, :repository, creator: user) } - let(:deployment) do + let_it_be(:deployment) do create(:deployment, :success, project: project, sha: project.commit.sha, ref: project.default_branch) end @@ -692,11 +692,11 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |integration_name it_behaves_like "triggered #{integration_name} integration", event_type: "deployment" context 'on a protected branch' do - before do + before(:all) do create(:protected_branch, :create_branch_on_repository, project: project, name: 'a-protected-branch') end - let(:deployment) do + let_it_be(:deployment) do create(:deployment, :success, project: project, sha: project.commit.sha, ref: 'a-protected-branch') 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 3d393e6dcb5..c6d6e00c781 100644 --- a/spec/support/shared_examples/models/concerns/limitable_shared_examples.rb +++ b/spec/support/shared_examples/models/concerns/limitable_shared_examples.rb @@ -2,7 +2,7 @@ RSpec.shared_examples 'includes Limitable concern' do describe '#exceeds_limits?' do - let(:plan_limits) { create(:plan_limits, :default_plan) } + let_it_be_with_reload(:plan_limits) { create(:plan_limits, :default_plan) } context 'without plan limits configured' do it { expect(subject.exceeds_limits?).to eq false } @@ -26,7 +26,7 @@ RSpec.shared_examples 'includes Limitable concern' do end describe 'validations' do - let(:plan_limits) { create(:plan_limits, :default_plan) } + let_it_be_with_reload(:plan_limits) { create(:plan_limits, :default_plan) } it { is_expected.to be_a(Limitable) } diff --git a/spec/support/shared_examples/models/concerns/ttl_expirable_shared_examples.rb b/spec/support/shared_examples/models/concerns/ttl_expirable_shared_examples.rb index 174b8609337..ac34ee32c6d 100644 --- a/spec/support/shared_examples/models/concerns/ttl_expirable_shared_examples.rb +++ b/spec/support/shared_examples/models/concerns/ttl_expirable_shared_examples.rb @@ -7,6 +7,11 @@ RSpec.shared_examples 'ttl_expirable' do it_behaves_like 'having unique enum values' + describe 'default values', :freeze_time do + it { expect(described_class.new.read_at).to be_like_time(Time.zone.now) } + it { expect(described_class.new(read_at: 1.day.ago).read_at).to be_like_time(1.day.ago) } + end + describe 'validations' do it { is_expected.to validate_presence_of(:status) } end @@ -38,7 +43,7 @@ RSpec.shared_examples 'ttl_expirable' do end end - describe '#read', :freeze_time do + describe '#read!', :freeze_time do let_it_be(:old_read_at) { 1.day.ago } let_it_be(:item1) { create(class_symbol, read_at: old_read_at) } diff --git a/spec/support/shared_examples/models/integrations/base_ci_shared_examples.rb b/spec/support/shared_examples/models/integrations/base_ci_shared_examples.rb new file mode 100644 index 00000000000..08fab45e41b --- /dev/null +++ b/spec/support/shared_examples/models/integrations/base_ci_shared_examples.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +RSpec.shared_examples Integrations::BaseCi do + describe 'default values' do + it { expect(subject.category).to eq(:ci) } + end +end diff --git a/spec/support/shared_examples/models/integrations/base_monitoring_shared_examples.rb b/spec/support/shared_examples/models/integrations/base_monitoring_shared_examples.rb new file mode 100644 index 00000000000..5d7e7633a23 --- /dev/null +++ b/spec/support/shared_examples/models/integrations/base_monitoring_shared_examples.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +RSpec.shared_examples Integrations::BaseMonitoring do + describe 'default values' do + it { expect(subject.category).to eq(:monitoring) } + end +end diff --git a/spec/support/shared_examples/models/integrations/base_slash_commands_shared_examples.rb b/spec/support/shared_examples/models/integrations/base_slash_commands_shared_examples.rb index e35ac9c0d0d..7dfdd24177e 100644 --- a/spec/support/shared_examples/models/integrations/base_slash_commands_shared_examples.rb +++ b/spec/support/shared_examples/models/integrations/base_slash_commands_shared_examples.rb @@ -6,6 +6,10 @@ RSpec.shared_examples Integrations::BaseSlashCommands do it { is_expected.to have_many :chat_names } end + describe 'default values' do + it { expect(subject.category).to eq(:chat) } + end + describe '#valid_token?' do subject { described_class.new } diff --git a/spec/support/shared_examples/models/integrations/has_web_hook_shared_examples.rb b/spec/support/shared_examples/models/integrations/has_web_hook_shared_examples.rb index 31ec25249d7..a764d47d7c0 100644 --- a/spec/support/shared_examples/models/integrations/has_web_hook_shared_examples.rb +++ b/spec/support/shared_examples/models/integrations/has_web_hook_shared_examples.rb @@ -38,7 +38,7 @@ RSpec.shared_examples Integrations::HasWebHook do end describe '#url_variables' do - it 'returns a string' do + it 'returns a hash' do expect(integration.url_variables).to be_a(Hash) end end diff --git a/spec/support/shared_examples/models/packages/debian/component_file_shared_example.rb b/spec/support/shared_examples/models/packages/debian/component_file_shared_example.rb index 23026167b19..5be0f6349ea 100644 --- a/spec/support/shared_examples/models/packages/debian/component_file_shared_example.rb +++ b/spec/support/shared_examples/models/packages/debian/component_file_shared_example.rb @@ -199,7 +199,7 @@ RSpec.shared_examples 'Debian Component File' do |container_type, can_freeze| expect(component_file) .to receive(:update_column) - .with(:file_store, ::Packages::PackageFileUploader::Store::LOCAL) + .with('file_store', ::Packages::PackageFileUploader::Store::LOCAL) .and_call_original expect { subject }.to change { component_file.size }.from(nil).to(74) diff --git a/spec/support/shared_examples/models/wiki_shared_examples.rb b/spec/support/shared_examples/models/wiki_shared_examples.rb index b1aa90449e1..7e69a6663d5 100644 --- a/spec/support/shared_examples/models/wiki_shared_examples.rb +++ b/spec/support/shared_examples/models/wiki_shared_examples.rb @@ -161,9 +161,10 @@ RSpec.shared_examples 'wiki model' do let(:wiki_pages) { subject.list_pages } before do - subject.create_page('index', 'This is an index') + # The order is intentional subject.create_page('index2', 'This is an index2') - subject.create_page('an index3', 'This is an index3') + subject.create_page('index', 'This is an index') + subject.create_page('index3', 'This is an index3') end it 'returns an array of WikiPage instances' do @@ -183,13 +184,47 @@ RSpec.shared_examples 'wiki model' do context 'with limit option' do it 'returns limited set of pages' do - expect(subject.list_pages(limit: 1).count).to eq(1) + expect( + subject.list_pages(limit: 1).map(&:title) + ).to eql(%w[index]) + end + + it 'returns all set of pages if limit is more than the total pages' do + expect(subject.list_pages(limit: 4).count).to eq(3) + end + + it 'returns all set of pages if limit is 0' do + expect(subject.list_pages(limit: 0).count).to eq(3) + end + end + + context 'with offset option' do + it 'returns offset-ed set of pages' do + expect( + subject.list_pages(offset: 1).map(&:title) + ).to eq(%w[index2 index3]) + + expect( + subject.list_pages(offset: 2).map(&:title) + ).to eq(["index3"]) + expect(subject.list_pages(offset: 3).count).to eq(0) + expect(subject.list_pages(offset: 4).count).to eq(0) + end + + it 'returns all set of pages if offset is 0' do + expect(subject.list_pages(offset: 0).count).to eq(3) + end + + it 'can combines with limit' do + expect( + subject.list_pages(offset: 1, limit: 1).map(&:title) + ).to eq(["index2"]) end end context 'with sorting options' do it 'returns pages sorted by title by default' do - pages = ['an index3', 'index', 'index2'] + pages = %w[index index2 index3] expect(subject.list_pages.map(&:title)).to eq(pages) expect(subject.list_pages(direction: 'desc').map(&:title)).to eq(pages.reverse) @@ -200,24 +235,14 @@ RSpec.shared_examples 'wiki model' do let(:pages) { subject.list_pages(load_content: true) } it 'loads WikiPage content' do - expect(pages.first.content).to eq('This is an index3') - expect(pages.second.content).to eq('This is an index') - expect(pages.third.content).to eq('This is an index2') + expect(pages.first.content).to eq('This is an index') + expect(pages.second.content).to eq('This is an index2') + expect(pages.third.content).to eq('This is an index3') end end end - context 'list pages with legacy wiki rpcs' do - before do - stub_feature_flags(wiki_list_page_with_normal_repository_rpcs: false) - end - - it_behaves_like 'wiki model #list_pages' - end - - context 'list pages with normal repository rpcs' do - it_behaves_like 'wiki model #list_pages' - end + it_behaves_like 'wiki model #list_pages' end describe '#sidebar_entries' do @@ -821,29 +846,6 @@ RSpec.shared_examples 'wiki model' do end end - describe '#ensure_repository' do - context 'if the repository exists' do - it 'does not create the repository' do - expect(subject.repository.exists?).to eq(true) - expect(subject.repository.raw).not_to receive(:create_repository) - - subject.ensure_repository - end - end - - context 'if the repository does not exist' do - let(:wiki_container) { wiki_container_without_repo } - - it 'creates the repository' do - expect(subject.repository.exists?).to eq(false) - - subject.ensure_repository - - expect(subject.repository.exists?).to eq(true) - end - end - end - describe '#hook_attrs' do it 'returns a hash with values' do expect(subject.hook_attrs).to be_a Hash diff --git a/spec/support/shared_examples/quick_actions/issuable/max_issuable_examples.rb b/spec/support/shared_examples/quick_actions/issuable/max_issuable_examples.rb index e725de8ad31..f5431b29ee2 100644 --- a/spec/support/shared_examples/quick_actions/issuable/max_issuable_examples.rb +++ b/spec/support/shared_examples/quick_actions/issuable/max_issuable_examples.rb @@ -12,49 +12,60 @@ RSpec.shared_examples 'does not exceed the issuable size limit' do project.add_maintainer(user3) end - context 'when feature flag is turned on' do - context "when the number of users of issuable does exceed the limit" do - before do - stub_const("Issuable::MAX_NUMBER_OF_ASSIGNEES_OR_REVIEWERS", 2) + context "when the number of users of issuable does exceed the limit" do + before do + stub_const("Issuable::MAX_NUMBER_OF_ASSIGNEES_OR_REVIEWERS", 2) + end + + it 'will not add more than the allowed number of users' do + allow_next_instance_of(update_service) do |service| + expect(service).not_to receive(:execute) end - it 'will not add more than the allowed number of users' do - allow_next_instance_of(update_service) do |service| - expect(service).not_to receive(:execute) - end + note = described_class.new(project, user, opts.merge( + note: note_text, + noteable_type: noteable_type, + noteable_id: issuable.id, + confidential: false + )).execute - note = described_class.new(project, user, opts.merge( - note: note_text, - noteable_type: noteable_type, - noteable_id: issuable.id, - confidential: false - )).execute + expect(note.errors[:validation]).to match_array([validation_message]) + end + end - expect(note.errors[:validation]).to match_array([validation_message]) - end + context "when the number of users does not exceed the limit" do + before do + stub_const("Issuable::MAX_NUMBER_OF_ASSIGNEES_OR_REVIEWERS", 6) end - context "when the number of users does not exceed the limit" do - before do - stub_const("Issuable::MAX_NUMBER_OF_ASSIGNEES_OR_REVIEWERS", 6) + it 'calls execute and does not return an error' do + allow_next_instance_of(update_service) do |service| + expect(service).to receive(:execute).and_call_original end - it 'calls execute and does not return an error' do - allow_next_instance_of(update_service) do |service| - expect(service).to receive(:execute).and_call_original - end - - note = described_class.new(project, user, opts.merge( - note: note_text, - noteable_type: noteable_type, - noteable_id: issuable.id, - confidential: false - )).execute + note = described_class.new(project, user, opts.merge( + note: note_text, + noteable_type: noteable_type, + noteable_id: issuable.id, + confidential: false + )).execute - expect(note.errors[:validation]).to be_empty - end + expect(note.errors[:validation]).to be_empty end end +end + +RSpec.shared_examples 'does not exceed the issuable size limit with ff off' do + let(:user1) { create(:user) } + let(:user2) { create(:user) } + let(:user3) { create(:user) } + + before do + project.add_maintainer(user) + project.add_maintainer(user1) + project.add_maintainer(user2) + project.add_maintainer(user3) + end context 'when feature flag is off' do before do diff --git a/spec/support/shared_examples/requests/access_tokens_controller_shared_examples.rb b/spec/support/shared_examples/requests/access_tokens_controller_shared_examples.rb index 59e641e2af6..2170025824f 100644 --- a/spec/support/shared_examples/requests/access_tokens_controller_shared_examples.rb +++ b/spec/support/shared_examples/requests/access_tokens_controller_shared_examples.rb @@ -1,21 +1,98 @@ # frozen_string_literal: true RSpec.shared_examples 'GET resource access tokens available' do - let_it_be(:active_resource_access_token) { create(:personal_access_token, user: bot_user) } + let_it_be(:active_resource_access_token) { create(:personal_access_token, user: access_token_user) } - it 'retrieves active resource access tokens' do - subject + it 'retrieves active access tokens' do + get_access_tokens - token_entities = assigns(:active_resource_access_tokens) + token_entities = assigns(:active_access_tokens) expect(token_entities.length).to eq(1) expect(token_entities[0][:name]).to eq(active_resource_access_token.name) end it 'lists all available scopes' do - subject + get_access_tokens expect(assigns(:scopes)).to eq(Gitlab::Auth.resource_bot_scopes) end + + it 'returns for json response' do + get_access_tokens_json + + expect(json_response.count).to eq(1) + end +end + +RSpec.shared_examples 'GET access tokens are paginated and ordered' do + before do + create(:personal_access_token, user: access_token_user) + end + + context "when multiple access tokens are returned" do + before do + allow(Kaminari.config).to receive(:default_per_page).and_return(1) + create(:personal_access_token, user: access_token_user) + end + + it "returns paginated response", :aggregate_failures do + get_access_tokens_with_page + expect(assigns(:active_access_tokens).count).to eq(1) + + expect_header('X-Per-Page', '1') + expect_header('X-Page', '1') + expect_header('X-Next-Page', '2') + expect_header('X-Total', '2') + end + end + + context "when access_token_pagination feature flag is disabled" do + before do + stub_feature_flags(access_token_pagination: false) + create(:personal_access_token, user: access_token_user) + end + + it "returns all tokens in system" do + get_access_tokens_with_page + expect(assigns(:active_access_tokens).count).to eq(2) + end + end + + context "when tokens returned are ordered" do + let(:expires_1_day_from_now) { 1.day.from_now.to_date } + let(:expires_2_day_from_now) { 2.days.from_now.to_date } + + before do + create(:personal_access_token, user: access_token_user, name: "Token1", expires_at: expires_1_day_from_now) + create(:personal_access_token, user: access_token_user, name: "Token2", expires_at: expires_2_day_from_now) + end + + it "orders token list ascending on expires_at" do + get_access_tokens + + first_token = assigns(:active_access_tokens).first.as_json + expect(first_token['name']).to eq("Token1") + expect(first_token['expires_at']).to eq(expires_1_day_from_now.strftime("%Y-%m-%d")) + end + + it "orders tokens on id in case token has same expires_at" do + create(:personal_access_token, user: access_token_user, name: "Token3", expires_at: expires_1_day_from_now) + + get_access_tokens + + first_token = assigns(:active_access_tokens).first.as_json + expect(first_token['name']).to eq("Token3") + expect(first_token['expires_at']).to eq(expires_1_day_from_now.strftime("%Y-%m-%d")) + + second_token = assigns(:active_access_tokens).second.as_json + expect(second_token['name']).to eq("Token1") + expect(second_token['expires_at']).to eq(expires_1_day_from_now.strftime("%Y-%m-%d")) + end + end + + def expect_header(header_name, header_val) + expect(response.headers[header_name]).to eq(header_val) + end end RSpec.shared_examples 'POST resource access tokens available' do @@ -83,7 +160,7 @@ end RSpec.shared_examples 'PUT resource access tokens available' do it 'calls delete user worker' do - expect(DeleteUserWorker).to receive(:perform_async).with(user.id, bot_user.id, skip_authorization: true) + expect(DeleteUserWorker).to receive(:perform_async).with(user.id, access_token_user.id, skip_authorization: true) subject end @@ -91,34 +168,12 @@ RSpec.shared_examples 'PUT resource access tokens available' do it 'removes membership of bot user' do subject - expect(resource.reload.bots).not_to include(bot_user) + expect(resource.reload.bots).not_to include(access_token_user) end - context 'when user_destroy_with_limited_execution_time_worker is enabled' do - it 'creates GhostUserMigration records to handle migration in a worker' do - expect { subject }.to( - change { Users::GhostUserMigration.count }.from(0).to(1)) - end - end - - context 'when user_destroy_with_limited_execution_time_worker is disabled' do - before do - stub_feature_flags(user_destroy_with_limited_execution_time_worker: false) - end - - it 'converts issuables of the bot user to ghost user' do - issue = create(:issue, author: bot_user) - - subject - - expect(issue.reload.author.ghost?).to be true - end - - it 'deletes project bot user' do - subject - - expect(User.exists?(bot_user.id)).to be_falsy - end + it 'creates GhostUserMigration records to handle migration in a worker' do + expect { subject }.to( + change { Users::GhostUserMigration.count }.from(0).to(1)) end context 'when unsuccessful' do diff --git a/spec/support/shared_examples/requests/api/discussions_shared_examples.rb b/spec/support/shared_examples/requests/api/discussions_shared_examples.rb index 32562aef8d2..f577e2ad323 100644 --- a/spec/support/shared_examples/requests/api/discussions_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/discussions_shared_examples.rb @@ -15,7 +15,7 @@ RSpec.shared_examples 'with cross-reference system notes' do new_merge_request.project.add_developer(user) hidden_merge_request = create(:merge_request) - new_cross_reference = "test commit #{hidden_merge_request.project.commit}" + new_cross_reference = "test commit #{hidden_merge_request.project.commit.to_reference(project)}" new_note = create(:system_note, noteable: merge_request, project: project, note: new_cross_reference) create(:system_note_metadata, note: new_note, action: 'cross_reference') end diff --git a/spec/support/shared_examples/requests/api/graphql/issuable_search_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/issuable_search_shared_examples.rb index 22805cf7aed..bb492425fd7 100644 --- a/spec/support/shared_examples/requests/api/graphql/issuable_search_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/graphql/issuable_search_shared_examples.rb @@ -1,13 +1,15 @@ # frozen_string_literal: true # Requires `query(params)` , `user`, `issuable_data` and `issuable` bindings -RSpec.shared_examples 'query with a search term' do +RSpec.shared_examples 'query with a search term' do |fields = [:DESCRIPTION]| + let(:search_term) { 'bar' } + let(:ids) { graphql_dig_at(issuable_data, :node, :id) } + it 'returns only matching issuables' do - filter_params = { search: 'bar', in: [:DESCRIPTION] } + filter_params = { search: search_term, in: fields } graphql_query = query(filter_params) post_graphql(graphql_query, current_user: user) - ids = graphql_dig_at(issuable_data, :node, :id) expect(ids).to contain_exactly(issuable.to_global_id.to_s) end diff --git a/spec/support/shared_examples/requests/api/graphql/issue_list_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/issue_list_shared_examples.rb new file mode 100644 index 00000000000..5469fd80a4f --- /dev/null +++ b/spec/support/shared_examples/requests/api/graphql/issue_list_shared_examples.rb @@ -0,0 +1,170 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'graphql issue list request spec' do + it_behaves_like 'a working graphql query' do + before do + post_query + end + end + + describe 'filters' do + context 'when filtering by assignees' do + context 'when both assignee_username filters are provided' do + let(:issue_filter_params) do + { assignee_username: current_user.username, assignee_usernames: [current_user.username] } + end + + it 'returns a mutually exclusive param error' do + post_query + + expect_graphql_errors_to_include( + 'only one of [assigneeUsernames, assigneeUsername] arguments is allowed at the same time.' + ) + end + end + + context 'when filtering by a negated argument' do + let(:issue_filter_params) { { not: { assignee_usernames: [current_user.username] } } } + + it 'returns correctly filtered issues' do + post_query + + expect(issue_ids).to match_array(expected_negated_assignee_issues.map { |i| i.to_gid.to_s }) + end + end + end + + context 'when filtering by unioned arguments' do + let(:issue_filter_params) { { or: { assignee_usernames: [current_user.username, another_user.username] } } } + + it 'returns correctly filtered issues' do + post_query + + expect(issue_ids).to match_array(expected_unioned_assignee_issues.map { |i| i.to_gid.to_s }) + end + + context 'when argument is blank' do + let(:issue_filter_params) { { or: {} } } + + it 'does not raise an error' do + post_query + + expect_graphql_errors_to_be_empty + end + end + + context 'when feature flag is disabled' do + it 'returns an error' do + stub_feature_flags(or_issuable_queries: false) + + post_query + + expect_graphql_errors_to_include( + "'or' arguments are only allowed when the `or_issuable_queries` feature flag is enabled." + ) + end + end + end + + context 'when filtering by a blank negated argument' do + let(:issue_filter_params) { { not: {} } } + + it 'does not raise an error' do + post_query + + expect_graphql_errors_to_be_empty + end + end + + context 'when filtering by reaction emoji' do + using RSpec::Parameterized::TableSyntax + + where(:value, :issue_list) do + 'thumbsup' | lazy { voted_issues } + 'ANY' | lazy { voted_issues } + 'any' | lazy { voted_issues } + 'AnY' | lazy { voted_issues } + 'NONE' | lazy { no_award_issues } + 'thumbsdown' | lazy { [] } + end + + with_them do + let(:issue_filter_params) { { my_reaction_emoji: value } } + let(:gids) { to_gid_list(issue_list) } + + it 'returns correctly filtered issues' do + post_query + + expect(issue_ids).to match_array(gids) + end + end + end + + context 'when filtering by search' do + it_behaves_like 'query with a search term', [:TITLE] do + let(:search_term) { search_title_term } + let(:issuable_data) { issues_data } + let(:user) { current_user } + let(:issuable) { title_search_issue } + let(:ids) { issue_ids } + end + end + end + + describe 'sorting and pagination' do + context 'when sorting by severity' do + context 'when ascending' do + it_behaves_like 'sorted paginated query' do + let(:sort_param) { :SEVERITY_ASC } + let(:first_param) { 2 } + let(:all_records) { to_gid_list(expected_severity_sorted_asc) } + end + end + + context 'when descending' do + it_behaves_like 'sorted paginated query' do + let(:sort_param) { :SEVERITY_DESC } + let(:first_param) { 2 } + let(:all_records) { to_gid_list(expected_severity_sorted_asc.reverse) } + end + end + end + + context 'when sorting by priority' do + context 'when ascending' do + it_behaves_like 'sorted paginated query' do + let(:sort_param) { :PRIORITY_ASC } + let(:first_param) { 2 } + let(:all_records) { to_gid_list(expected_priority_sorted_asc) } + end + end + + context 'when descending' do + it_behaves_like 'sorted paginated query' do + let(:sort_param) { :PRIORITY_DESC } + let(:first_param) { 2 } + let(:all_records) { to_gid_list(expected_priority_sorted_desc) } + end + end + end + end + + it 'includes a web_url' do + post_query + + expect(issues_data[0]['webUrl']).to be_present + end + + it 'includes discussion locked' do + post_query + + expect(issues_data).to contain_exactly( + *locked_discussion_issues.map { |i| hash_including('id' => i.to_gid.to_s, 'discussionLocked' => true) }, + *unlocked_discussion_issues.map { |i| hash_including('id' => i.to_gid.to_s, 'discussionLocked' => false) } + ) + end + + def to_gid_list(instance_list) + instance_list.map { |instance| instance.to_gid.to_s } + end +end diff --git a/spec/support/shared_examples/requests/api/graphql/packages/group_and_project_packages_list_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/packages/group_and_project_packages_list_shared_examples.rb index 1b609915f32..fb4aacfd7a9 100644 --- a/spec/support/shared_examples/requests/api/graphql/packages/group_and_project_packages_list_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/graphql/packages/group_and_project_packages_list_shared_examples.rb @@ -114,7 +114,7 @@ RSpec.shared_examples 'group and project packages query' do end [:CREATED_ASC, :NAME_ASC, :VERSION_ASC, :TYPE_ASC, :CREATED_DESC, :NAME_DESC, :VERSION_DESC, :TYPE_DESC].each do |order| - context "#{order}" do + context order.to_s do let(:sorted_packages) { packages_order_map.fetch(order) } it_behaves_like 'sorted paginated query' do diff --git a/spec/support/shared_examples/requests/api/graphql/projects/branch_protections/access_level_request_examples.rb b/spec/support/shared_examples/requests/api/graphql/projects/branch_protections/access_level_request_examples.rb new file mode 100644 index 00000000000..54cc13fac94 --- /dev/null +++ b/spec/support/shared_examples/requests/api/graphql/projects/branch_protections/access_level_request_examples.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'perform graphql requests for AccessLevel type objects' do |access_level_kind| + include GraphqlHelpers + + let_it_be(:project) { create(:project) } + let_it_be(:current_user) { create(:user, maintainer_projects: [project]) } + let_it_be(:variables) { { path: project.full_path } } + + let(:fields) { all_graphql_fields_for("#{access_level_kind.to_s.classify}AccessLevel", max_depth: 2) } + let(:access_levels) { protected_branch.public_send("#{access_level_kind}_access_levels") } + let(:access_levels_count) { access_levels.size } + let(:maintainer_access_level) { access_levels.for_role.first } + let(:maintainer_access_level_data) { access_levels_data.first } + let(:access_levels_data) do + graphql_data_at('project', + 'branchRules', + 'nodes', + 0, + 'branchProtection', + "#{access_level_kind.to_s.camelize(:lower)}AccessLevels", + 'nodes') + end + + let(:query) do + <<~GQL + query($path: ID!) { + project(fullPath: $path) { + branchRules(first: 1) { + nodes { + branchProtection { + #{access_level_kind.to_s.camelize(:lower)}AccessLevels { + nodes { + #{fields} + } + } + } + } + } + } + } + GQL + end + + context 'when request AccessLevel type objects as a guest user' do + let_it_be(:protected_branch) { create(:protected_branch, project: project) } + + before do + project.add_guest(current_user) + + post_graphql(query, current_user: current_user, variables: variables) + end + + it_behaves_like 'a working graphql query' + + it { expect(access_levels_data).not_to be_present } + end + + context 'when request AccessLevel type objects as a maintainer' do + let_it_be(:protected_branch) do + create(:protected_branch, "maintainers_can_#{access_level_kind}", project: project) + end + + before do + post_graphql(query, current_user: current_user, variables: variables) + end + + it_behaves_like 'a working graphql query' + + it 'returns all the access level attributes' do + expect(maintainer_access_level_data['accessLevel']).to eq(maintainer_access_level.access_level) + expect(maintainer_access_level_data['accessLevelDescription']).to eq(maintainer_access_level.humanize) + expect(maintainer_access_level_data.dig('group', 'name')).to be_nil + expect(maintainer_access_level_data.dig('user', 'name')).to be_nil + end + end +end diff --git a/spec/support/shared_examples/requests/api/issues_shared_examples.rb b/spec/support/shared_examples/requests/api/issues_shared_examples.rb index 991dbced02d..6328fb9cd8a 100644 --- a/spec/support/shared_examples/requests/api/issues_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/issues_shared_examples.rb @@ -37,7 +37,7 @@ RSpec.shared_examples 'labeled issues with labels and label_name params' do context 'negation' do context 'array of labeled issues when all labels match with negation' do - let(:params) { { labels: "#{label.title},#{label_b.title}", not: { labels: "#{label_c.title}" } } } + let(:params) { { labels: "#{label.title},#{label_b.title}", not: { labels: label_c.title.to_s } } } it_behaves_like 'returns negated label names' end diff --git a/spec/support/shared_examples/requests/api/members_shared_examples.rb b/spec/support/shared_examples/requests/api/members_shared_examples.rb index fce75c29971..9136f60eb93 100644 --- a/spec/support/shared_examples/requests/api/members_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/members_shared_examples.rb @@ -11,3 +11,11 @@ RSpec.shared_examples 'a 404 response when source is private' do expect(response).to have_gitlab_http_status(:not_found) end end + +RSpec.shared_examples 'a 403 response when user does not have rights to manage members of a specific access level' do + it 'returns 403' do + route + + expect(response).to have_gitlab_http_status(:forbidden) + end +end diff --git a/spec/support/shared_examples/requests/api/multiple_and_scoped_issue_boards_shared_examples.rb b/spec/support/shared_examples/requests/api/multiple_and_scoped_issue_boards_shared_examples.rb index fa111ca5811..d749479544d 100644 --- a/spec/support/shared_examples/requests/api/multiple_and_scoped_issue_boards_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/multiple_and_scoped_issue_boards_shared_examples.rb @@ -5,6 +5,7 @@ RSpec.shared_examples 'multiple and scoped issue boards' do |route_definition| context 'multiple issue boards' do before do + stub_feature_flags(apollo_boards: false) board_parent.add_reporter(user) stub_licensed_features(multiple_group_issue_boards: true) end diff --git a/spec/support/shared_examples/requests/api/notes_shared_examples.rb b/spec/support/shared_examples/requests/api/notes_shared_examples.rb index 8479493911b..11f9565989f 100644 --- a/spec/support/shared_examples/requests/api/notes_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/notes_shared_examples.rb @@ -179,7 +179,8 @@ RSpec.shared_examples 'noteable API' do |parent_type, noteable_type, id_name| end end - if parent_type == 'projects' + case parent_type + when 'projects' context 'by a project owner' do let(:user) { project.first_owner } @@ -211,7 +212,7 @@ RSpec.shared_examples 'noteable API' do |parent_type, noteable_type, id_name| expect(Time.parse(json_response['updated_at'])).to be_like_time(creation_time) end end - elsif parent_type == 'groups' + when 'groups' context 'by a group owner' do it 'sets the creation time on the new note' do post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", user), params: params @@ -288,7 +289,7 @@ RSpec.shared_examples 'noteable API' do |parent_type, noteable_type, id_name| end it 'allows user in allow-list to create notes' do - stub_application_setting(notes_create_limit_allowlist: ["#{user.username}"]) + stub_application_setting(notes_create_limit_allowlist: [user.username.to_s]) subject expect(response).to have_gitlab_http_status(:created) diff --git a/spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb index 11e19d8d067..a9b44015206 100644 --- a/spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb @@ -221,6 +221,27 @@ RSpec.shared_examples 'rejects PyPI access with unknown group id' do end end +RSpec.shared_examples 'allow access for everyone with public package_registry_access_level' do + context 'with private project but public access to package registry' do + before do + project.update_column(:visibility_level, Gitlab::VisibilityLevel::PRIVATE) + project.project_feature.update!(package_registry_access_level: ProjectFeature::PUBLIC) + end + + context 'as non-member user' do + let(:headers) { basic_auth_header(user.username, personal_access_token.token) } + + it_behaves_like 'returning response status', :success + end + + context 'as anonymous' do + let(:headers) { {} } + + it_behaves_like 'returning response status', :success + end + end +end + RSpec.shared_examples 'pypi simple API endpoint' do using RSpec::Parameterized::TableSyntax diff --git a/spec/support/shared_examples/requests/api/terraform/modules/v1/packages_shared_examples.rb b/spec/support/shared_examples/requests/api/terraform/modules/v1/packages_shared_examples.rb index 544a0ed8fdd..bdff2c65691 100644 --- a/spec/support/shared_examples/requests/api/terraform/modules/v1/packages_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/terraform/modules/v1/packages_shared_examples.rb @@ -63,9 +63,9 @@ RSpec.shared_examples 'redirects to version download' do |user_type, status, add it 'returns a valid response' do subject - expect(request.url).to include 'module-1/system/download' + expect(request.url).to include "#{package.name}/download" expect(response.headers).to include 'Location' - expect(response.headers['Location']).to include 'module-1/system/1.0.1/download' + expect(response.headers['Location']).to include "#{package.name}/1.0.1/download" end end end diff --git a/spec/support/shared_examples/services/alert_management_shared_examples.rb b/spec/support/shared_examples/services/alert_management_shared_examples.rb index 571cb7dc03d..b46ace1824a 100644 --- a/spec/support/shared_examples/services/alert_management_shared_examples.rb +++ b/spec/support/shared_examples/services/alert_management_shared_examples.rb @@ -72,8 +72,8 @@ RSpec.shared_examples 'processes one firing and one resolved prometheus alerts' .and change(Note, :count).by(1) expect(subject).to be_success - expect(subject.payload[:alerts]).to all(be_a_kind_of(AlertManagement::Alert)) - expect(subject.payload[:alerts].size).to eq(1) + expect(subject.payload).to eq({}) + expect(subject.http_status).to eq(:created) end it_behaves_like 'processes incident issues' diff --git a/spec/support/shared_examples/services/base_rpm_service_shared_examples.rb b/spec/support/shared_examples/services/base_rpm_service_shared_examples.rb deleted file mode 100644 index c9520852a5b..00000000000 --- a/spec/support/shared_examples/services/base_rpm_service_shared_examples.rb +++ /dev/null @@ -1,52 +0,0 @@ -# frozen_string_literal: true - -RSpec.shared_examples 'handling rpm xml file' do - include_context 'with rpm package data' - - let(:xml) { nil } - let(:data) { {} } - - context 'when generate empty xml' do - it 'generate expected xml' do - expect(subject).to eq(empty_xml) - end - end - - context 'when updating existing xml' do - let(:xml) { empty_xml } - let(:data) { xml_update_params } - - shared_examples 'changing root tag attribute' do - it "increment previous 'packages' value by 1" do - previous_value = Nokogiri::XML(xml).at(described_class::ROOT_TAG).attributes["packages"].value.to_i - new_value = Nokogiri::XML(subject).at(described_class::ROOT_TAG).attributes["packages"].value.to_i - - expect(previous_value + 1).to eq(new_value) - end - end - - it 'generate valid xml add expected xml node to existing xml' do - # Have one root attribute - result = Nokogiri::XML::Document.parse(subject).remove_namespaces! - expect(result.children.count).to eq(1) - - # Root node has 1 child with generated node - expect(result.xpath("//#{described_class::ROOT_TAG}/package").count).to eq(1) - end - - context 'when empty xml' do - it_behaves_like 'changing root tag attribute' - end - - context 'when xml has children' do - let(:xml) { described_class.new(xml: empty_xml, data: data).execute } - - it 'has children nodes' do - result = Nokogiri::XML::Document.parse(xml).remove_namespaces! - expect(result.children.count).to be > 0 - end - - it_behaves_like 'changing root tag attribute' - end - end -end diff --git a/spec/support/shared_examples/services/issuable/discussions_list_shared_examples.rb b/spec/support/shared_examples/services/issuable/discussions_list_shared_examples.rb new file mode 100644 index 00000000000..c38ca6a3bf0 --- /dev/null +++ b/spec/support/shared_examples/services/issuable/discussions_list_shared_examples.rb @@ -0,0 +1,112 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'listing issuable discussions' do |user_role, internal_discussion_count, total_discussions_count| + before_all do + create_notes(issuable, "some user comment") + end + + context 'when user cannot read issue' do + it "returns no notes" do + expect(discussions_service.execute).to be_empty + end + end + + context 'when user can read issuable' do + before do + group.add_developer(current_user) + end + + context 'with paginated results' do + let(:finder_params_for_issuable) { { per_page: 2 } } + let(:next_page_cursor) { { cursor: discussions_service.paginator.cursor_for_next_page } } + + it "returns next page notes" do + next_page_discussions_service = described_class.new(current_user, issuable, + finder_params_for_issuable.merge(next_page_cursor)) + discussions = next_page_discussions_service.execute + + expect(discussions.count).to eq(2) + expect(discussions.first.notes.map(&:note)).to match_array(["added #{label.to_reference} label"]) + expect(discussions.second.notes.map(&:note)).to match_array(["removed #{label.to_reference} label"]) + end + end + + # confidential notes are currently available only on issues and epics + context 'and cannot read confidential notes' do + before do + group.add_member(current_user, user_role) + end + + it "returns non confidential notes" do + discussions = discussions_service.execute + + non_conf_discussion_count = total_discussions_count - internal_discussion_count + expect(discussions.count).to eq(non_conf_discussion_count) + expect(discussions.count { |disc| disc.notes.any?(&:confidential) }).to eq(0) + expect(discussions.count { |disc| !disc.notes.any?(&:confidential) }).to eq(non_conf_discussion_count) + end + end + + # confidential notes are currently available only on issues and epics + context 'and can read confidential notes' do + it "returns all notes" do + discussions = discussions_service.execute + + expect(discussions.count).to eq(total_discussions_count) + expect(discussions.count { |disc| disc.notes.any?(&:confidential) }).to eq(internal_discussion_count) + non_conf_discussion_count = total_discussions_count - internal_discussion_count + expect(discussions.count { |disc| !disc.notes.any?(&:confidential) }).to eq(non_conf_discussion_count) + end + end + + context 'and system notes only' do + let(:finder_params_for_issuable) { { notes_filter: UserPreference::NOTES_FILTERS[:only_activity] } } + + it "returns system notes" do + discussions = discussions_service.execute + + expect(discussions.count { |disc| disc.notes.any?(&:system) }).to be > 0 + expect(discussions.count { |disc| !disc.notes.any?(&:system) }).to eq(0) + end + end + + context 'and user comments only' do + let(:finder_params_for_issuable) { { notes_filter: UserPreference::NOTES_FILTERS[:only_comments] } } + + it "returns user comments" do + discussions = discussions_service.execute + + expect(discussions.count { |disc| disc.notes.any?(&:system) }).to eq(0) + expect(discussions.count { |disc| !disc.notes.any?(&:system) }).to be > 0 + end + end + end +end + +def create_notes(issuable, note_body) + assoc_name = issuable.to_ability_name + + create(:note, system: true, project: issuable.project, noteable: issuable) + + first_discussion = create(:discussion_note_on_issue, noteable: issuable, project: issuable.project, note: note_body) + create(:note, + discussion_id: first_discussion.discussion_id, noteable: issuable, + project: issuable.project, note: "reply on #{note_body}") + + create(:resource_label_event, user: current_user, "#{assoc_name}": issuable, label: label, action: 'add') + create(:resource_label_event, user: current_user, "#{assoc_name}": issuable, label: label, action: 'remove') + + unless issuable.is_a?(Epic) + create(:resource_milestone_event, "#{assoc_name}": issuable, milestone: milestone, action: 'add') + create(:resource_milestone_event, "#{assoc_name}": issuable, milestone: milestone, action: 'remove') + end + + # confidential notes are currently available only on issues and epics + return unless issuable.is_a?(Issue) || issuable.is_a?(Epic) + + first_internal_discussion = create(:discussion_note_on_issue, :confidential, + noteable: issuable, project: issuable.project, note: "confidential #{note_body}") + create(:note, :confidential, + discussion_id: first_internal_discussion.discussion_id, noteable: issuable, + project: issuable.project, note: "reply on confidential #{note_body}") +end diff --git a/spec/support/shared_examples/services/merge_status_updated_trigger_shared_examples.rb b/spec/support/shared_examples/services/merge_status_updated_trigger_shared_examples.rb new file mode 100644 index 00000000000..97e3b0a44a7 --- /dev/null +++ b/spec/support/shared_examples/services/merge_status_updated_trigger_shared_examples.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'triggers GraphQL subscription mergeRequestMergeStatusUpdated' do + specify do + expect(GraphqlTriggers).to receive(:merge_request_merge_status_updated).with(merge_request) + + action + end +end + +RSpec.shared_examples 'does not trigger GraphQL subscription mergeRequestMergeStatusUpdated' do + specify do + expect(GraphqlTriggers).not_to receive(:merge_request_merge_status_updated) + + action + end +end diff --git a/spec/support/shared_examples/services/users/dismiss_user_callout_service_shared_examples.rb b/spec/support/shared_examples/services/users/dismiss_user_callout_service_shared_examples.rb index 09820593cdb..46a1f4b6598 100644 --- a/spec/support/shared_examples/services/users/dismiss_user_callout_service_shared_examples.rb +++ b/spec/support/shared_examples/services/users/dismiss_user_callout_service_shared_examples.rb @@ -20,7 +20,7 @@ RSpec.shared_examples_for 'dismissing user callout' do |model| old_time = 1.day.ago new_time = Time.current attributes = params.merge(dismissed_at: old_time, user: user) - existing_callout = create("#{model.name.split('::').last.underscore}".to_sym, attributes) + existing_callout = create(model.name.split('::').last.underscore.to_s.to_sym, attributes) expect { execute }.to change { existing_callout.reload.dismissed_at }.from(old_time).to(new_time) end diff --git a/spec/support/shared_examples/services/work_items/widgets/milestone_service_shared_examples.rb b/spec/support/shared_examples/services/work_items/widgets/milestone_service_shared_examples.rb new file mode 100644 index 00000000000..ac17915c15a --- /dev/null +++ b/spec/support/shared_examples/services/work_items/widgets/milestone_service_shared_examples.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +RSpec.shared_examples "setting work item's milestone" do + context "when 'milestone' param does not exist" do + let(:params) { {} } + + it "does not set the work item's milestone" do + expect { execute_callback }.to not_change(work_item, :milestone) + end + end + + context "when 'milestone' is not in the work item's project's hierarchy" do + let(:another_group_milestone) { create(:milestone, group: create(:group)) } + let(:params) { { milestone_id: another_group_milestone.id } } + + it "does not set the work item's milestone" do + expect { execute_callback }.to not_change(work_item, :milestone) + end + end + + context 'when assigning a group milestone' do + let(:params) { { milestone_id: group_milestone.id } } + + it "sets the work item's milestone" do + expect { execute_callback } + .to change(work_item, :milestone) + .from(nil) + .to(group_milestone) + end + end + + context 'when assigning a project milestone' do + let(:params) { { milestone_id: project_milestone.id } } + + it "sets the work item's milestone" do + expect { execute_callback } + .to change(work_item, :milestone) + .from(nil) + .to(project_milestone) + end + end +end diff --git a/spec/support/shared_examples/uploaders/object_storage_shared_examples.rb b/spec/support/shared_examples/uploaders/object_storage_shared_examples.rb index 3c977e62a10..af56f8ffac7 100644 --- a/spec/support/shared_examples/uploaders/object_storage_shared_examples.rb +++ b/spec/support/shared_examples/uploaders/object_storage_shared_examples.rb @@ -26,9 +26,10 @@ RSpec.shared_examples "migrates" do |to_store:, from_store: nil| expect(subject).to be_an(CarrierWave::Uploader::Base) expect(subject).to be_a(ObjectStorage::Concern) - if from == described_class::Store::REMOTE + case from + when described_class::Store::REMOTE expect(subject.file).to be_a(CarrierWave::Storage::Fog::File) - elsif from == described_class::Store::LOCAL + when described_class::Store::LOCAL expect(subject.file).to be_a(CarrierWave::SanitizedFile) else raise 'Unexpected file type' diff --git a/spec/support/shared_examples/workers/batched_background_migration_worker_shared_examples.rb b/spec/support/shared_examples/workers/batched_background_migration_worker_shared_examples.rb index 3ba5f080a01..0be55fd2a3e 100644 --- a/spec/support/shared_examples/workers/batched_background_migration_worker_shared_examples.rb +++ b/spec/support/shared_examples/workers/batched_background_migration_worker_shared_examples.rb @@ -137,8 +137,12 @@ RSpec.shared_examples 'it runs batched background migration jobs' do |tracking_d let(:lease_timeout) { 15.minutes } let(:lease_key) { described_class.name.demodulize.underscore } let(:interval_variance) { described_class::INTERVAL_VARIANCE } + let(:migration_id) { 123 } let(:migration) do - build(:batched_background_migration, :active, interval: job_interval, table_name: table_name) + build( + :batched_background_migration, :active, + id: migration_id, interval: job_interval, table_name: table_name + ) end before do @@ -150,45 +154,6 @@ RSpec.shared_examples 'it runs batched background migration jobs' do |tracking_d 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 } @@ -196,8 +161,8 @@ RSpec.shared_examples 'it runs batched background migration jobs' do |tracking_d 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) + expect_next_instance_of(Database::BatchedBackgroundMigration::ExecutionWorker) do |worker| + expect(worker).to receive(:perform).with(tracking_database, migration_id) end expect(worker).to receive(:run_active_migration).and_call_original @@ -217,10 +182,13 @@ RSpec.shared_examples 'it runs batched background migration jobs' do |tracking_d expect { worker.perform }.to raise_error(RuntimeError, 'I broke') end - it 'receives the correct connection' do + it 'delegetes the execution to ExecutionWorker' do base_model = Gitlab::Database.database_base_models[tracking_database] expect(Gitlab::Database::SharedModel).to receive(:using_connection).with(base_model.connection).and_yield + expect_next_instance_of(Database::BatchedBackgroundMigration::ExecutionWorker) do |worker| + expect(worker).to receive(:perform).with(tracking_database, migration_id) + end worker.perform end @@ -236,10 +204,10 @@ RSpec.shared_examples 'it runs batched background migration jobs' do |tracking_d let(:migration_class) do Class.new(Gitlab::BackgroundMigration::BatchedMigrationJob) do job_arguments :matching_status + operation_name :update_all def perform each_sub_batch( - operation_name: :update_all, batching_scope: -> (relation) { relation.where(status: matching_status) } ) do |sub_batch| sub_batch.update_all(some_column: 0) diff --git a/spec/support/sidekiq_middleware.rb b/spec/support/sidekiq_middleware.rb index cbd6163d46b..73f43487d7c 100644 --- a/spec/support/sidekiq_middleware.rb +++ b/spec/support/sidekiq_middleware.rb @@ -6,6 +6,15 @@ require 'sidekiq/testing' module SidekiqMiddleware def with_sidekiq_server_middleware(&block) Sidekiq::Testing.server_middleware.clear + + if Gem::Version.new(Sidekiq::VERSION) != Gem::Version.new('6.5.7') + raise 'New version of sidekiq detected, please remove this line' + end + + # This line is a workaround for a Sidekiq bug that is already fixed in v7.0.0 + # https://github.com/mperham/sidekiq/commit/1b83a152786ed382f07fff12d2608534f1e3c922 + Sidekiq::Testing.server_middleware.instance_variable_set(:@config, Sidekiq) + Sidekiq::Testing.server_middleware(&block) ensure Sidekiq::Testing.server_middleware.clear diff --git a/spec/support/webmock.rb b/spec/support/webmock.rb index f952f7f0985..b9bd3f82f65 100644 --- a/spec/support/webmock.rb +++ b/spec/support/webmock.rb @@ -15,6 +15,13 @@ def webmock_allowed_hosts end.compact.uniq end +def with_net_connect_allowed + WebMock.allow_net_connect! + yield +ensure + webmock_enable! +end + # This prevents Selenium/WebMock from spawning thousands of connections # while waiting for an element to appear via Capybara's find: # https://github.com/teamcapybara/capybara/issues/2322#issuecomment-619321520 |