diff options
Diffstat (limited to 'spec/support')
87 files changed, 1310 insertions, 427 deletions
diff --git a/spec/support/database/cross-join-allowlist.yml b/spec/support/database/cross-join-allowlist.yml index 19b1ce30d5f..fe51488c706 100644 --- a/spec/support/database/cross-join-allowlist.yml +++ b/spec/support/database/cross-join-allowlist.yml @@ -1,6 +1 @@ -- "./spec/lib/gitlab/background_migration/copy_ci_builds_columns_to_security_scans_spec.rb" -- "./spec/lib/gitlab/background_migration/migrate_pages_metadata_spec.rb" -- "./spec/migrations/20210907211557_finalize_ci_builds_bigint_conversion_spec.rb" -- "./spec/migrations/associate_existing_dast_builds_with_variables_spec.rb" -- "./spec/migrations/disable_job_token_scope_when_unused_spec.rb" -- "./spec/migrations/schedule_copy_ci_builds_columns_to_security_scans2_spec.rb" +[] diff --git a/spec/support/database/gitlab_schemas_validate_connection.rb b/spec/support/database/gitlab_schemas_validate_connection.rb new file mode 100644 index 00000000000..118c6ea5001 --- /dev/null +++ b/spec/support/database/gitlab_schemas_validate_connection.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +RSpec.configure do |config| + def with_gitlab_schemas_validate_connection_prevented + Gitlab::Database::QueryAnalyzers::GitlabSchemasValidateConnection.with_suppressed do + yield + end + end + + config.around(:each, :suppress_gitlab_schemas_validate_connection) do |example| + with_gitlab_schemas_validate_connection_prevented(&example) + end + + config.around(:each, query_analyzers: false) do |example| + with_gitlab_schemas_validate_connection_prevented(&example) + end +end diff --git a/spec/support/database/multiple_databases.rb b/spec/support/database/multiple_databases.rb index 94857b47127..05f26e57e9c 100644 --- a/spec/support/database/multiple_databases.rb +++ b/spec/support/database/multiple_databases.rb @@ -98,6 +98,26 @@ RSpec.configure do |config| example.run end end + + config.around(:each, :migration) do |example| + 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 + end end ActiveRecord::Base.singleton_class.prepend(::Database::ActiveRecordBaseEstablishConnection) # rubocop:disable Database/MultipleDatabases diff --git a/spec/support/finder_collection_allowlist.yml b/spec/support/finder_collection_allowlist.yml index 8f09153afec..1ac8e49fb45 100644 --- a/spec/support/finder_collection_allowlist.yml +++ b/spec/support/finder_collection_allowlist.yml @@ -57,6 +57,8 @@ - Security::ScanExecutionPoliciesFinder - Security::TrainingProviders::BaseUrlFinder - Security::TrainingUrlsFinder +- Security::TrainingProviders::KontraUrlFinder +- Security::TrainingProviders::SecureCodeWarriorUrlFinder - SentryIssueFinder - ServerlessDomainFinder - TagsFinder diff --git a/spec/support/helpers/api_helpers.rb b/spec/support/helpers/api_helpers.rb index fd85071cca3..62bb9576695 100644 --- a/spec/support/helpers/api_helpers.rb +++ b/spec/support/helpers/api_helpers.rb @@ -19,15 +19,17 @@ module ApiHelpers # => "/api/v2/issues?foo=bar&private_token=..." # # Returns the relative path to the requested API resource - def api(path, user = nil, version: API::API.version, personal_access_token: nil, oauth_access_token: nil, job_token: nil) + def api(path, user = nil, version: API::API.version, personal_access_token: nil, oauth_access_token: nil, job_token: nil, access_token: nil) full_path = "/api/#{version}#{path}" if oauth_access_token - query_string = "access_token=#{oauth_access_token.token}" + query_string = "access_token=#{oauth_access_token.plaintext_token}" elsif personal_access_token query_string = "private_token=#{personal_access_token.token}" elsif job_token query_string = "job_token=#{job_token}" + elsif access_token + query_string = "access_token=#{access_token.token}" elsif user personal_access_token = create(:personal_access_token, user: user) query_string = "private_token=#{personal_access_token.token}" @@ -66,6 +68,13 @@ module ApiHelpers expect(json_response.map { |item| item['id'] }).to contain_exactly(*items) end + def expect_paginated_array_response_contain_exactly(*items) + expect(response).to have_gitlab_http_status(:ok) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.map { |item| item['id'] }).to contain_exactly(*items) + end + def stub_last_activity_update allow_any_instance_of(Users::ActivityService).to receive(:execute) end diff --git a/spec/support/helpers/ci/template_helpers.rb b/spec/support/helpers/ci/template_helpers.rb index 598a5a0becc..119f8d001a1 100644 --- a/spec/support/helpers/ci/template_helpers.rb +++ b/spec/support/helpers/ci/template_helpers.rb @@ -5,6 +5,10 @@ module Ci def secure_analyzers_prefix 'registry.gitlab.com/security-products' end + + def template_registry_host + 'registry.gitlab.com' + end end end diff --git a/spec/support/helpers/cycle_analytics_helpers.rb b/spec/support/helpers/cycle_analytics_helpers.rb index 044ec56b1cc..05e9a099a2b 100644 --- a/spec/support/helpers/cycle_analytics_helpers.rb +++ b/spec/support/helpers/cycle_analytics_helpers.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true module CycleAnalyticsHelpers - include GitHelpers - def toggle_value_stream_dropdown page.find('[data-testid="dropdown-value-streams"]').click end @@ -129,10 +127,6 @@ module CycleAnalyticsHelpers repository = project.repository oldrev = repository.commit(branch_name)&.sha || Gitlab::Git::BLANK_SHA - if Timecop.frozen? - mock_gitaly_multi_action_dates(repository, commit_time) - end - commit_shas = Array.new(count) do |index| commit_sha = repository.create_file(user, generate(:branch), "content", message: message, branch_name: branch_name) repository.commit(commit_sha) @@ -241,23 +235,4 @@ module CycleAnalyticsHelpers pipeline: dummy_pipeline(project), protected: false) end - - def mock_gitaly_multi_action_dates(repository, commit_time) - allow(repository.raw).to receive(:multi_action).and_wrap_original do |m, user, kargs| - new_date = commit_time || Time.now - branch_update = m.call(user, **kargs) - - if branch_update.newrev - commit = rugged_repo(repository).rev_parse(branch_update.newrev) - - branch_update.newrev = commit.amend( - update_ref: "#{Gitlab::Git::BRANCH_REF_PREFIX}#{kargs[:branch_name]}", - author: commit.author.merge(time: new_date), - committer: commit.committer.merge(time: new_date) - ) - end - - branch_update - end - end end diff --git a/spec/support/helpers/dns_helpers.rb b/spec/support/helpers/dns_helpers.rb index b941e7c4808..c60c14f10a3 100644 --- a/spec/support/helpers/dns_helpers.rb +++ b/spec/support/helpers/dns_helpers.rb @@ -5,6 +5,7 @@ module DnsHelpers stub_all_dns! stub_invalid_dns! permit_local_dns! + permit_postgresql! end def permit_dns! @@ -25,14 +26,30 @@ module DnsHelpers def permit_local_dns! local_addresses = %r{ \A - ::1? | # IPV6 - (127|10)\.0\.0\.\d{1,3} | # 127.0.0.x or 10.0.0.x local network - (192\.168|172\.16)\.\d{1,3}\.\d{1,3} | # 192.168.x.x or 172.16.x.x local network - 0\.0\.0\.0 | # loopback + ::1? | # IPV6 + (127|10)\.0\.0\.\d{1,3} | # 127.0.0.x or 10.0.0.x local network + 192\.168\.\d{1,3}\.\d{1,3} | # 192.168.x.x local network + 172\.(1[6-9]|2[0-9]|3[0-1])\.\d{1,3}\.\d{1,3} | # 172.16.x.x - 172.31.x.x local network + 0\.0\.0\.0 | # loopback localhost \z }xi allow(Addrinfo).to receive(:getaddrinfo).with(local_addresses, anything, nil, :STREAM).and_call_original allow(Addrinfo).to receive(:getaddrinfo).with(local_addresses, anything, nil, :STREAM, anything, anything, any_args).and_call_original end + + # pg v1.4.0, unlike v1.3.5, uses AddrInfo.getaddrinfo to resolve IPv4 and IPv6 addresses: + # https://github.com/ged/ruby-pg/pull/459 + def permit_postgresql! + db_hosts.each do |host| + next if host.start_with?('/') # Exclude UNIX sockets + + # https://github.com/ged/ruby-pg/blob/252512608a814de16bbad55911f9bbcef0e73cb9/lib/pg/connection.rb#L720 + allow(Addrinfo).to receive(:getaddrinfo).with(host, anything, nil, :STREAM).and_call_original + end + end + + def db_hosts + ActiveRecord::Base.configurations.configs_for(env_name: Rails.env).map(&:host).compact.uniq + end end diff --git a/spec/support/helpers/features/blob_spec_helpers.rb b/spec/support/helpers/features/blob_spec_helpers.rb index 880a7249284..7ccfc9be7e2 100644 --- a/spec/support/helpers/features/blob_spec_helpers.rb +++ b/spec/support/helpers/features/blob_spec_helpers.rb @@ -11,12 +11,4 @@ module BlobSpecHelpers def unset_default_button set_default_button('') end - - def editor_value - evaluate_script('monaco.editor.getModels()[0].getValue()') - end - - def set_editor_value(value) - execute_script("monaco.editor.getModels()[0].setValue('#{value}')") - end end diff --git a/spec/support/helpers/features/invite_members_modal_helper.rb b/spec/support/helpers/features/invite_members_modal_helper.rb index b56ac5b32c6..d02ec06d886 100644 --- a/spec/support/helpers/features/invite_members_modal_helper.rb +++ b/spec/support/helpers/features/invite_members_modal_helper.rb @@ -11,7 +11,7 @@ module Spec page.within invite_modal_selector do select_members(names) choose_options(role, expires_at) - click_button 'Invite' + submit_invites end page.refresh if refresh @@ -42,11 +42,15 @@ module Spec click_button name choose_options(role, expires_at) - click_button 'Invite' + submit_invites page.refresh end + def submit_invites + click_button 'Invite' + end + def choose_options(role, expires_at) unless role == 'Guest' click_button 'Guest' @@ -86,12 +90,47 @@ module Spec "[data-token-id='#{id}']" end + def more_invite_errors_button_selector + "[data-testid='accordion-button']" + end + + def limited_invite_error_selector + "[data-testid='errors-limited-item']" + end + + def expanded_invite_error_selector + "[data-testid='errors-expanded-item']" + end + def remove_token(id) page.within member_token_selector(id) do find('[data-testid="close-icon"]').click end end + def expect_to_have_successful_invite_indicator(page, user) + expect(page).to have_selector("#{member_token_selector(user.id)} .gl-bg-green-100") + expect(page).not_to have_text("#{user.name}: ") + end + + def expect_to_have_invalid_invite_indicator(page, user, message: true) + expect(page).to have_selector("#{member_token_selector(user.id)} .gl-bg-red-100") + expect(page).to have_selector(member_token_error_selector(user.id)) + expect(page).to have_text("#{user.name}: Access level should be greater than or equal to") if message + end + + def expect_to_have_normal_invite_indicator(page, user) + expect(page).to have_selector(member_token_selector(user.id)) + expect(page).not_to have_selector("#{member_token_selector(user.id)} .gl-bg-red-100") + expect(page).not_to have_selector("#{member_token_selector(user.id)} .gl-bg-green-100") + expect(page).not_to have_text("#{user.name}: ") + end + + def expect_to_have_invite_removed(page, user) + expect(page).not_to have_selector(member_token_selector(user.id)) + expect(page).not_to have_text("#{user.name}: Access level should be greater than or equal to") + end + def expect_to_have_group(group) expect(page).to have_selector("[entity-id='#{group.id}']") end diff --git a/spec/support/helpers/features/runner_helpers.rb b/spec/support/helpers/features/runners_helpers.rb index 63fc628358c..63fc628358c 100644 --- a/spec/support/helpers/features/runner_helpers.rb +++ b/spec/support/helpers/features/runners_helpers.rb diff --git a/spec/support/helpers/features/source_editor_spec_helpers.rb b/spec/support/helpers/features/source_editor_spec_helpers.rb index cdc59f9cbe1..f7eb2a52507 100644 --- a/spec/support/helpers/features/source_editor_spec_helpers.rb +++ b/spec/support/helpers/features/source_editor_spec_helpers.rb @@ -12,8 +12,11 @@ module Spec def editor_set_value(value) editor = find('.monaco-editor') uri = editor['data-uri'] + execute_script("localMonaco.getModel('#{uri}').setValue('#{escape_javascript(value)}')") - execute_script("monaco.editor.getModel('#{uri}').setValue('#{escape_javascript(value)}')") + # We only check that the first line is present because when the content is long, + # only a part of the text will be rendered in the DOM due to scrolling + page.has_selector?('.gl-source-editor .view-lines', text: value.lines.first) end end end diff --git a/spec/support/helpers/gitaly_setup.rb b/spec/support/helpers/gitaly_setup.rb index 56993fc27b7..278dc79e1d0 100644 --- a/spec/support/helpers/gitaly_setup.rb +++ b/spec/support/helpers/gitaly_setup.rb @@ -12,6 +12,8 @@ require 'logger' require 'fileutils' require 'bundler' +require_relative '../../../lib/gitlab/utils' + module GitalySetup extend self @@ -139,7 +141,7 @@ module GitalySetup end def start_praefect - if ENV['GITALY_PRAEFECT_WITH_DB'] + if praefect_with_db? LOGGER.debug 'Starting Praefect with database election strategy' start(:praefect, File.join(tmp_tests_gitaly_dir, 'praefect-db.config.toml')) else @@ -290,7 +292,7 @@ module GitalySetup # In CI we need to pre-generate both config files. # For local testing we'll create the correct file on-demand. - if ENV['CI'] || ENV['GITALY_PRAEFECT_WITH_DB'].nil? + if ENV['CI'] || !praefect_with_db? Gitlab::SetupHelper::Praefect.create_configuration( gitaly_dir, { 'praefect' => repos_path }, @@ -298,7 +300,7 @@ module GitalySetup ) end - if ENV['CI'] || ENV['GITALY_PRAEFECT_WITH_DB'] + if ENV['CI'] || praefect_with_db? Gitlab::SetupHelper::Praefect.create_configuration( gitaly_dir, { 'praefect' => repos_path }, @@ -319,7 +321,7 @@ module GitalySetup end def setup_praefect - return unless ENV['GITALY_PRAEFECT_WITH_DB'] + return unless praefect_with_db? migrate_cmd = service_cmd(:praefect, File.join(tmp_tests_gitaly_dir, 'praefect-db.config.toml')) + ['sql-migrate'] system(env, *migrate_cmd, [:out, :err] => 'log/praefect-test.log') @@ -396,4 +398,8 @@ module GitalySetup def praefect_binary File.join(tmp_tests_gitaly_dir, "_build", "bin", "praefect") end + + def praefect_with_db? + Gitlab::Utils.to_boolean(ENV['GITALY_PRAEFECT_WITH_DB'], default: false) + end end diff --git a/spec/support/helpers/global_id_deprecation_helpers.rb b/spec/support/helpers/global_id_deprecation_helpers.rb index 37ba1420fb3..5c6862ca84a 100644 --- a/spec/support/helpers/global_id_deprecation_helpers.rb +++ b/spec/support/helpers/global_id_deprecation_helpers.rb @@ -2,9 +2,11 @@ module GlobalIDDeprecationHelpers def stub_global_id_deprecations(*deprecations) - old_name_map = deprecations.index_by(&:old_model_name) - new_name_map = deprecations.index_by(&:new_model_name) - old_graphql_name_map = deprecations.index_by { |d| Types::GlobalIDType.model_name_to_graphql_name(d.old_model_name) } + old_name_map = deprecations.index_by(&:old_name) + new_name_map = deprecations.index_by(&:new_name) + old_graphql_name_map = deprecations.index_by do |d| + Gitlab::GlobalId::Deprecations.map_graphql_name(d.old_name) + end stub_const('Gitlab::GlobalId::Deprecations::OLD_NAME_MAP', old_name_map) stub_const('Gitlab::GlobalId::Deprecations::NEW_NAME_MAP', new_name_map) diff --git a/spec/support/helpers/graphql_helpers.rb b/spec/support/helpers/graphql_helpers.rb index d0a1941817a..d78c523decd 100644 --- a/spec/support/helpers/graphql_helpers.rb +++ b/spec/support/helpers/graphql_helpers.rb @@ -170,7 +170,7 @@ module GraphqlHelpers # or `prepare` in app/graphql/types/range_input_type.rb, used by Types::TimeframeInputType def args_internal(field, args:, query_ctx:, parent:, extras:, query:) arguments = GraphqlHelpers.deep_transform_args(args, field) - arguments.merge!(extras.reject {|k, v| v == :not_given}) + arguments.merge!(extras.reject { |k, v| v == :not_given }) end # Pros: @@ -185,7 +185,7 @@ module GraphqlHelpers # take internal style args, and force them into client style args def args_internal_prepared(field, args:, query_ctx:, parent:, extras:, query:) arguments = GraphqlHelpers.as_graphql_argument_literals(args) - arguments.merge!(extras.reject {|k, v| v == :not_given}) + arguments.merge!(extras.reject { |k, v| v == :not_given }) # Use public API to properly prepare the args for use by the resolver. # It uses `coerce_arguments` under the covers @@ -307,14 +307,14 @@ module GraphqlHelpers end def graphql_mutation(name, input, fields = nil, &block) - raise ArgumentError, 'Please pass either `fields` parameter or a block to `#graphql_mutation`, but not both.' if fields.present? && block_given? + raise ArgumentError, 'Please pass either `fields` parameter or a block to `#graphql_mutation`, but not both.' if fields.present? && block name = name.graphql_name if name.respond_to?(:graphql_name) mutation_name = GraphqlHelpers.fieldnamerize(name) input_variable_name = "$#{input_variable_name_for_mutation(name)}" mutation_field = GitlabSchema.mutation.fields[mutation_name] - fields = yield if block_given? + fields = yield if block fields ||= all_graphql_fields_for(mutation_field.type.to_type_signature) query = <<~MUTATION diff --git a/spec/support/helpers/javascript_fixtures_helpers.rb b/spec/support/helpers/javascript_fixtures_helpers.rb index 84cd0181533..32e6e8d50bd 100644 --- a/spec/support/helpers/javascript_fixtures_helpers.rb +++ b/spec/support/helpers/javascript_fixtures_helpers.rb @@ -2,6 +2,7 @@ require 'action_dispatch/testing/test_request' require 'fileutils' +require 'graphlyte' require_relative '../../../lib/gitlab/popen' @@ -47,7 +48,8 @@ module JavaScriptFixturesHelpers path = Rails.root / base / query_path queries = Gitlab::Graphql::Queries.find(path) if queries.length == 1 - queries.first.text(mode: Gitlab.ee? ? :ee : :ce ) + query = queries.first.text(mode: Gitlab.ee? ? :ee : :ce ) + inflate_query_with_typenames(query) else raise "Could not find query file at #{path}, please check your query_path" % path end @@ -55,6 +57,23 @@ module JavaScriptFixturesHelpers private + # Private: Parse a GraphQL query and inflate the fields with a __typename + # + # query - the GraqhQL query to parse + def inflate_query_with_typenames(query, doc: Graphlyte.parse(query)) + typename_editor.edit(doc) + + doc.to_s + end + + def typename_editor + typename = Graphlyte::Syntax::Field.new(name: '__typename') + + @editor ||= Graphlyte::Editor.new.on_field do |field| + field.selection << typename unless field.selection.empty? || field.selection.map(&:name).include?('__typename') + end + end + # Private: Store a response object as fixture file # # response - string or response object to store diff --git a/spec/support/helpers/lfs_http_helpers.rb b/spec/support/helpers/lfs_http_helpers.rb index 199d5e70e32..91ed56b4d13 100644 --- a/spec/support/helpers/lfs_http_helpers.rb +++ b/spec/support/helpers/lfs_http_helpers.rb @@ -52,11 +52,9 @@ module LfsHttpHelpers end def request_body(operation, objects) - objects = [objects] unless objects.is_a?(Array) - { 'operation' => operation, - 'objects' => objects + 'objects' => Array.wrap(objects) } end end diff --git a/spec/support/helpers/login_helpers.rb b/spec/support/helpers/login_helpers.rb index c93ef8b0ead..f83f5c7bfde 100644 --- a/spec/support/helpers/login_helpers.rb +++ b/spec/support/helpers/login_helpers.rb @@ -91,12 +91,12 @@ module LoginHelpers # user - User instance to login with # remember - Whether or not to check "Remember me" (default: false) # two_factor_auth - If two-factor authentication is enabled (default: false) - # password - password to attempt to login with + # password - password to attempt to login with (default: user.password) def gitlab_sign_in_with(user, remember: false, two_factor_auth: false, password: nil) visit new_user_session_path fill_in "user_login", with: user.email - fill_in "user_password", with: (password || "12345678") + fill_in "user_password", with: (password || user.password) check 'user_remember_me' if remember find('[data-testid="sign-in-button"]:enabled').click diff --git a/spec/support/helpers/query_recorder.rb b/spec/support/helpers/query_recorder.rb index 01839a74e65..dd124ed9c7f 100644 --- a/spec/support/helpers/query_recorder.rb +++ b/spec/support/helpers/query_recorder.rb @@ -14,7 +14,7 @@ module ActiveRecord @skip_schema_queries = skip_schema_queries @query_recorder_debug = ENV['QUERY_RECORDER_DEBUG'] || query_recorder_debug @log_file = log_file - record(&block) if block_given? + record(&block) if block end def record(&block) diff --git a/spec/support/helpers/rack_attack_spec_helpers.rb b/spec/support/helpers/rack_attack_spec_helpers.rb index 6c06781df03..2502889e17c 100644 --- a/spec/support/helpers/rack_attack_spec_helpers.rb +++ b/spec/support/helpers/rack_attack_spec_helpers.rb @@ -17,8 +17,12 @@ module RackAttackSpecHelpers { Gitlab::Auth::AuthFinders::PRIVATE_TOKEN_HEADER => personal_access_token.token } end + def bearer_headers(token) + { 'AUTHORIZATION' => "Bearer #{token.token}" } + end + def oauth_token_headers(oauth_access_token) - { 'AUTHORIZATION' => "Bearer #{oauth_access_token.token}" } + { 'AUTHORIZATION' => "Bearer #{oauth_access_token.plaintext_token}" } end def basic_auth_headers(user, personal_access_token) diff --git a/spec/support/helpers/redis_commands/recorder.rb b/spec/support/helpers/redis_commands/recorder.rb new file mode 100644 index 00000000000..05a1aa67853 --- /dev/null +++ b/spec/support/helpers/redis_commands/recorder.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +module RedisCommands + class Recorder + def initialize(pattern: nil, &block) + @log = [] + @pattern = pattern + + record(&block) if block + end + + attr_reader :log + + def record(&block) + ActiveSupport::Notifications.subscribed(method(:callback), 'redis.process_commands', &block) + end + + def by_command(command) + @log.select { |record| record.include?(command) } + end + + def count + @count ||= @log.count + end + + private + + def callback(name, start, finish, message_id, values) + commands = values[:commands] + + @log << commands.flatten if @pattern.nil? || commands.to_s.include?(@pattern) + end + end +end diff --git a/spec/support/helpers/runner_releases_helper.rb b/spec/support/helpers/runner_releases_helper.rb new file mode 100644 index 00000000000..ab16a705425 --- /dev/null +++ b/spec/support/helpers/runner_releases_helper.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module RunnerReleasesHelper + def stub_runner_releases(available_runner_releases, gitlab_version: nil) + # We stub the behavior of RunnerReleases so that we don't need to rely on flaky global settings + available_runner_releases = available_runner_releases + .map { |v| ::Gitlab::VersionInfo.parse(v, parse_suffix: true) } + .sort + releases_by_minor = available_runner_releases + .group_by(&:without_patch) + .transform_values(&:max) + + runner_releases_double = instance_double(Gitlab::Ci::RunnerReleases) + allow(::Gitlab::Ci::RunnerUpgradeCheck).to receive(:new).and_wrap_original do |method, *_original_args| + gitlab_version ||= available_runner_releases.max + method.call(gitlab_version, runner_releases_double) + end + + allow(runner_releases_double).to receive(:releases).and_return(available_runner_releases) + allow(runner_releases_double).to receive(:releases_by_minor).and_return(releases_by_minor) + end +end diff --git a/spec/support/helpers/stub_member.rb b/spec/support/helpers/stub_member.rb new file mode 100644 index 00000000000..bcd0b675041 --- /dev/null +++ b/spec/support/helpers/stub_member.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +module StubMember + def self.included(base) + Member.prepend(StubbedMember::Member) + ProjectMember.prepend(StubbedMember::ProjectMember) + end +end diff --git a/spec/support/helpers/stub_method_calls.rb b/spec/support/helpers/stub_method_calls.rb index 45d704958ca..ccbede16563 100644 --- a/spec/support/helpers/stub_method_calls.rb +++ b/spec/support/helpers/stub_method_calls.rb @@ -44,7 +44,7 @@ module StubMethodCalls end def self.stub_method(object, method, &block) - raise ArgumentError, "Block is required" unless block_given? + raise ArgumentError, "Block is required" unless block backup_method(object, method) unless backed_up_method?(object, method) object.define_singleton_method(method, &block) diff --git a/spec/support/helpers/stubbed_member.rb b/spec/support/helpers/stubbed_member.rb new file mode 100644 index 00000000000..27420c9b709 --- /dev/null +++ b/spec/support/helpers/stubbed_member.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +# Extend the ProjectMember & GroupMember class with the ability to +# to run project_authorizations refresh jobs inline. + +# This is needed so that calls like `group.add_member(user, access_level)` or `create(:project_member)` +# in the specs can be run without including `:sidekiq_inline` trait. +module StubbedMember + extend ActiveSupport::Concern + + module Member + private + + def refresh_member_authorized_projects(blocking:) + return super unless blocking + + AuthorizedProjectsWorker.new.perform(user_id) + end + end + + module ProjectMember + private + + def blocking_project_authorizations_refresh + AuthorizedProjectUpdate::ProjectRecalculatePerUserWorker.new.perform(project.id, user.id) + end + end +end diff --git a/spec/support/helpers/type_name_deprecation_helpers.rb b/spec/support/helpers/type_name_deprecation_helpers.rb new file mode 100644 index 00000000000..591737ab532 --- /dev/null +++ b/spec/support/helpers/type_name_deprecation_helpers.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module TypeNameDeprecationHelpers + def stub_type_name_deprecations(*deprecations) + old_name_map = deprecations.index_by(&:old_name) + new_name_map = deprecations.index_by(&:new_name) + old_graphql_name_map = deprecations.index_by do |d| + Gitlab::Graphql::TypeNameDeprecations.map_graphql_name(d.old_name) + end + + stub_const('Gitlab::Graphql::TypeNameDeprecations::OLD_NAME_MAP', old_name_map) + stub_const('Gitlab::Graphql::TypeNameDeprecations::NEW_NAME_MAP', new_name_map) + stub_const('Gitlab::Graphql::TypeNameDeprecations::OLD_GRAPHQL_NAME_MAP', old_graphql_name_map) + end +end diff --git a/spec/support/matchers/event_store.rb b/spec/support/matchers/event_store.rb index 14f6a42d7f4..4ecb924b3ed 100644 --- a/spec/support/matchers/event_store.rb +++ b/spec/support/matchers/event_store.rb @@ -23,8 +23,8 @@ RSpec::Matchers.define :publish_event do |expected_event_class| def match_data?(actual, expected) values_match?(actual.keys, expected.keys) && - actual.keys.each do |key| - values_match?(actual[key], expected[key]) + actual.keys.all? do |key| + values_match?(expected[key], actual[key]) end end diff --git a/spec/support/matchers/markdown_matchers.rb b/spec/support/matchers/markdown_matchers.rb index 1932f78506f..8bec3be2535 100644 --- a/spec/support/matchers/markdown_matchers.rb +++ b/spec/support/matchers/markdown_matchers.rb @@ -189,8 +189,10 @@ module MarkdownMatchers match do |actual| expect(actual).to have_selector('ul.task-list', count: 2) - expect(actual).to have_selector('li.task-list-item', count: 7) + expect(actual).to have_selector('li.task-list-item', count: 9) + expect(actual).to have_selector('li.task-list-item.inapplicable > s', count: 2) expect(actual).to have_selector('input[checked]', count: 3) + expect(actual).to have_selector('input[data-inapplicable]', count: 2) end end diff --git a/spec/support/shared_contexts/bulk_imports_requests_shared_context.rb b/spec/support/shared_contexts/bulk_imports_requests_shared_context.rb index 62d708420c3..5fcb14e075a 100644 --- a/spec/support/shared_contexts/bulk_imports_requests_shared_context.rb +++ b/spec/support/shared_contexts/bulk_imports_requests_shared_context.rb @@ -12,17 +12,17 @@ RSpec.shared_context 'bulk imports requests context' do |url| } end - let(:request_headers) { { 'Authorization' => 'Bearer demo-pat', 'Content-Type' => 'application/json' } } + let(:request_headers) { { 'Content-Type' => 'application/json' } } before do - stub_request(:get, "#{url}/api/v4/version") + stub_request(:get, "#{url}/api/v4/version?page=1&per_page=20&private_token=demo-pat") .with(headers: request_headers) .to_return( status: 200, body: { version: ::BulkImport.min_gl_version_for_project_migration.to_s }.to_json, headers: { 'Content-Type' => 'application/json' }) - stub_request(:get, "https://gitlab.example.com/api/v4/groups?min_access_level=50&page=1&per_page=20&search=test&top_level_only=true") + stub_request(:get, "https://gitlab.example.com/api/v4/groups?min_access_level=50&page=1&per_page=20&private_token=demo-pat&search=test&top_level_only=true") .with(headers: request_headers) .to_return(status: 200, body: [{ @@ -33,10 +33,9 @@ RSpec.shared_context 'bulk imports requests context' do |url| full_name: 'Test', full_path: 'stub-test-group' }].to_json, - headers: page_response_headers - ) + headers: page_response_headers) - stub_request(:get, "%{url}/api/v4/groups?page=1&per_page=20&top_level_only=true&min_access_level=50&search=" % { url: url }) + stub_request(:get, "%{url}/api/v4/groups?min_access_level=50&page=1&per_page=20&private_token=demo-pat&search=&top_level_only=true" % { url: url }) .to_return( body: [{ id: 2595438, @@ -46,7 +45,6 @@ RSpec.shared_context 'bulk imports requests context' do |url| full_name: 'Stub', full_path: 'stub-group' }].to_json, - headers: page_response_headers - ) + headers: page_response_headers) end end diff --git a/spec/support/shared_contexts/features/integrations/integrations_shared_context.rb b/spec/support/shared_contexts/features/integrations/integrations_shared_context.rb index 255c4e6f882..ca2fe8a6c54 100644 --- a/spec/support/shared_contexts/features/integrations/integrations_shared_context.rb +++ b/spec/support/shared_contexts/features/integrations/integrations_shared_context.rb @@ -66,7 +66,7 @@ Integration.available_integration_names.each do |integration| hash.merge!(k => 'foo@bar.com') elsif (integration == 'slack' || integration == 'mattermost') && k == :labels_to_be_notified_behavior hash.merge!(k => "match_any") - elsif integration == 'campfire' && k = :room + elsif integration == 'campfire' && k == :room hash.merge!(k => '1234') else hash.merge!(k => "someword") diff --git a/spec/support/shared_contexts/fixtures/analytics_shared_context.rb b/spec/support/shared_contexts/fixtures/analytics_shared_context.rb index 13d3697a378..8e09cccee3e 100644 --- a/spec/support/shared_contexts/fixtures/analytics_shared_context.rb +++ b/spec/support/shared_contexts/fixtures/analytics_shared_context.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true RSpec.shared_context 'Analytics fixtures shared context' do + include CycleAnalyticsHelpers include JavaScriptFixturesHelpers let_it_be(:group) { create(:group) } diff --git a/spec/support/shared_contexts/lib/gitlab/sidekiq_middleware/server_metrics_shared_context.rb b/spec/support/shared_contexts/lib/gitlab/sidekiq_middleware/server_metrics_shared_context.rb index 449db59e35d..b6c54e902a2 100644 --- a/spec/support/shared_contexts/lib/gitlab/sidekiq_middleware/server_metrics_shared_context.rb +++ b/spec/support/shared_contexts/lib/gitlab/sidekiq_middleware/server_metrics_shared_context.rb @@ -17,6 +17,7 @@ RSpec.shared_context 'server metrics with mocked prometheus' do let(:elasticsearch_seconds_metric) { double('elasticsearch seconds metric') } let(:elasticsearch_requests_total) { double('elasticsearch calls total metric') } let(:load_balancing_metric) { double('load balancing metric') } + let(:sidekiq_mem_total_bytes) { double('sidekiq mem total bytes') } before do allow(Gitlab::Metrics).to receive(:histogram).and_call_original @@ -37,6 +38,7 @@ RSpec.shared_context 'server metrics with mocked prometheus' do allow(Gitlab::Metrics).to receive(:counter).with(:sidekiq_load_balancing_count, anything).and_return(load_balancing_metric) allow(Gitlab::Metrics).to receive(:gauge).with(:sidekiq_running_jobs, anything, {}, :all).and_return(running_jobs_metric) allow(Gitlab::Metrics).to receive(:gauge).with(:sidekiq_concurrency, anything, {}, :all).and_return(concurrency_metric) + allow(Gitlab::Metrics).to receive(:gauge).with(:sidekiq_mem_total_bytes, anything, {}, :all).and_return(sidekiq_mem_total_bytes) allow(concurrency_metric).to receive(:set) end @@ -61,13 +63,16 @@ RSpec.shared_context 'server metrics call' do let(:elasticsearch_calls) { 8 } let(:elasticsearch_duration) { 0.54 } + + let(:mem_total_bytes) { 1000000000 } let(:instrumentation) do { gitaly_duration_s: gitaly_duration, redis_calls: redis_calls, redis_duration_s: redis_duration, elasticsearch_calls: elasticsearch_calls, - elasticsearch_duration_s: elasticsearch_duration + elasticsearch_duration_s: elasticsearch_duration, + mem_total_bytes: mem_total_bytes } end @@ -95,5 +100,6 @@ RSpec.shared_context 'server metrics call' do allow(completion_seconds_metric).to receive(:observe) allow(redis_seconds_metric).to receive(:observe) allow(elasticsearch_seconds_metric).to receive(:observe) + allow(sidekiq_mem_total_bytes).to receive(:set) end end diff --git a/spec/support/shared_contexts/markdown_snapshot_shared_examples.rb b/spec/support/shared_contexts/markdown_snapshot_shared_examples.rb index a90fe9e1723..040b2da9f37 100644 --- a/spec/support/shared_contexts/markdown_snapshot_shared_examples.rb +++ b/spec/support/shared_contexts/markdown_snapshot_shared_examples.rb @@ -9,6 +9,9 @@ RSpec.shared_context 'with API::Markdown Snapshot shared context' do |glfm_speci # rubocop:enable Layout/LineLength include ApiHelpers + let_it_be(:user) { create(:user) } + let_it_be(:api_url) { api('/markdown', user) } + markdown_examples, html_examples = %w[markdown.yml html.yml].map do |file_name| yaml = File.read("#{glfm_specification_dir}/example_snapshots/#{file_name}") YAML.safe_load(yaml, symbolize_names: true, aliases: true) @@ -29,8 +32,6 @@ RSpec.shared_context 'with API::Markdown Snapshot shared context' do |glfm_speci let(:normalizations) { normalizations_by_example_name.dig(name, :html, :static, :snapshot) } it "verifies conversion of GLFM to HTML", :unlimited_max_formatted_output_length do - api_url = api "/markdown" - # noinspection RubyResolve normalized_html = normalize_html(html, normalizations) 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 eec6e92c5fe..893d3702407 100644 --- a/spec/support/shared_contexts/policies/group_policy_shared_context.rb +++ b/spec/support/shared_contexts/policies/group_policy_shared_context.rb @@ -56,6 +56,7 @@ RSpec.shared_context 'GroupPolicy context' do admin_package create_projects create_cluster update_cluster admin_cluster add_cluster + destroy_upload ] 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 789b385c435..1d4731d9b39 100644 --- a/spec/support/shared_contexts/policies/project_policy_shared_context.rb +++ b/spec/support/shared_contexts/policies/project_policy_shared_context.rb @@ -62,6 +62,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 ] end 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 fbd82fbbe31..b18ce14eba6 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 @@ -545,5 +545,62 @@ RSpec.shared_context 'ProjectPolicyTable context' do :private | :non_member | nil | 0 :private | :anonymous | nil | 0 end + + # Based on the permission_table_for_reporter_feature_access table, but for issue + # features where public and internal projects with issues enabled only allow + # access to reporters and above (excluding admins if admin mode is disabled) + # + # project_level, :feature_access_level, :membership, :admin_mode, :expected_count + def permission_table_for_reporter_issue_access + :public | :enabled | :admin | true | 1 + :public | :enabled | :admin | false | 0 + :public | :enabled | :reporter | nil | 1 + :public | :enabled | :guest | nil | 0 + :public | :enabled | :non_member | nil | 0 + :public | :enabled | :anonymous | nil | 0 + + :public | :private | :admin | true | 1 + :public | :private | :admin | false | 0 + :public | :private | :reporter | nil | 1 + :public | :private | :guest | nil | 0 + :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 | 1 + :internal | :enabled | :admin | false | 0 + :internal | :enabled | :reporter | nil | 1 + :internal | :enabled | :guest | nil | 0 + :internal | :enabled | :non_member | nil | 0 + :internal | :enabled | :anonymous | nil | 0 + + :internal | :private | :admin | true | 1 + :internal | :private | :admin | false | 0 + :internal | :private | :reporter | nil | 1 + :internal | :private | :guest | nil | 0 + :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 | 1 + :private | :private | :admin | false | 0 + :private | :private | :reporter | nil | 1 + :private | :private | :guest | nil | 0 + :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 # rubocop:enable Metrics/AbcSize end diff --git a/spec/support/shared_contexts/upload_type_check_shared_context.rb b/spec/support/shared_contexts/upload_type_check_shared_context.rb index 5fce31b4a15..57b8d7472df 100644 --- a/spec/support/shared_contexts/upload_type_check_shared_context.rb +++ b/spec/support/shared_contexts/upload_type_check_shared_context.rb @@ -3,7 +3,7 @@ # Construct an `uploader` variable that is configured to `check_upload_type` # with `mime_types` and `extensions`. # @param uploader [CarrierWave::Uploader::Base] uploader with extension_whitelist method. -RSpec.shared_context 'ignore extension whitelist check' do +RSpec.shared_context 'ignore extension allowlist check' do before do allow(uploader).to receive(:extension_whitelist).and_return(nil) end @@ -16,3 +16,15 @@ RSpec.shared_context 'force content type detection to mime_type' do allow(Gitlab::Utils::MimeType).to receive(:from_io).and_return(mime_type) end end + +def mock_upload(success = true) + allow(UploadService).to receive(:new).with(project, file).and_return(upload_service) + + if success + allow(upload_service).to receive(:execute).and_return(uploader) + allow(uploader).to receive(:upload).and_return(upload) + allow(upload).to receive(:id).and_return(upload_id) + else + allow(upload_service).to receive(:execute).and_return(nil) + end +end diff --git a/spec/support/shared_examples/attention_request_cache_invalidation_examples.rb b/spec/support/shared_examples/attention_request_cache_invalidation_examples.rb deleted file mode 100644 index 7fe696abc69..00000000000 --- a/spec/support/shared_examples/attention_request_cache_invalidation_examples.rb +++ /dev/null @@ -1,15 +0,0 @@ -# frozen_string_literal: true - -RSpec.shared_examples 'invalidates attention request cache' do - it 'invalidates the merge requests requiring attention count' do - cache_mock = double - - users.each do |user| - expect(cache_mock).to receive(:delete).with(['users', user.id, 'attention_requested_open_merge_requests_count']) - end - - allow(Rails).to receive(:cache).and_return(cache_mock) - - service.execute - end -end diff --git a/spec/support/shared_examples/boards/destroy_service_shared_examples.rb b/spec/support/shared_examples/boards/destroy_service_shared_examples.rb index 33bae3da44b..b1cb58a736f 100644 --- a/spec/support/shared_examples/boards/destroy_service_shared_examples.rb +++ b/spec/support/shared_examples/boards/destroy_service_shared_examples.rb @@ -20,10 +20,10 @@ RSpec.shared_examples 'board destroy service' do end context 'when there is only one board' do - it 'does not remove board' do + it 'does remove board' do expect do - expect(service.execute(board)).to be_error - end.not_to change(boards, :count) + service.execute(board) + end.to change(boards, :count).by(-1) end end end diff --git a/spec/support/shared_examples/components/pajamas_shared_examples.rb b/spec/support/shared_examples/components/pajamas_shared_examples.rb index 5c0ad1a1bc9..bcf7df24fd9 100644 --- a/spec/support/shared_examples/components/pajamas_shared_examples.rb +++ b/spec/support/shared_examples/components/pajamas_shared_examples.rb @@ -2,12 +2,18 @@ RSpec.shared_examples 'it renders help text' do it 'renders help text' do - expect(rendered_component).to have_selector('[data-testid="pajamas-component-help-text"]', text: help_text) + expect(page).to have_css('[data-testid="pajamas-component-help-text"]', text: help_text) end end RSpec.shared_examples 'it does not render help text' do it 'does not render help text' do - expect(rendered_component).not_to have_selector('[data-testid="pajamas-component-help-text"]') + expect(page).not_to have_css('[data-testid="pajamas-component-help-text"]') + end +end + +RSpec.shared_examples 'it renders unchecked checkbox with value of `1`' do + it 'renders unchecked checkbox with value of `1`' do + expect(page).to have_unchecked_field(label, with: '1') end end diff --git a/spec/support/shared_examples/controllers/search_cross_project_authorization_shared_examples.rb b/spec/support/shared_examples/controllers/search_cross_project_authorization_shared_examples.rb new file mode 100644 index 00000000000..9421561aea4 --- /dev/null +++ b/spec/support/shared_examples/controllers/search_cross_project_authorization_shared_examples.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +RSpec.shared_examples_for 'when the user cannot read cross project' do |action, params| + before do + allow(Ability).to receive(:allowed?).and_call_original + allow(Ability).to receive(:allowed?).with(user, :read_cross_project, :global).and_return(false) + end + + it 'blocks access without a project_id' do + get action, params: params + + expect(response).to have_gitlab_http_status(:forbidden) + end + + it 'allows access with a project_id' do + get action, params: params.merge(project_id: create(:project, :public).id) + + expect(response).to have_gitlab_http_status(:ok) + end +end diff --git a/spec/support/shared_examples/controllers/search_external_authorization_service_shared_examples.rb b/spec/support/shared_examples/controllers/search_external_authorization_service_shared_examples.rb new file mode 100644 index 00000000000..6b72988b3e6 --- /dev/null +++ b/spec/support/shared_examples/controllers/search_external_authorization_service_shared_examples.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +RSpec.shared_examples_for 'with external authorization service enabled' do |action, params| + include ExternalAuthorizationServiceHelpers + + let(:project) { create(:project, namespace: user.namespace) } + let(:note) { create(:note_on_issue, project: project) } + + before do + enable_external_authorization_service_check + end + + it 'renders a 403 when no project is given' do + get action, params: params + + expect(response).to have_gitlab_http_status(:forbidden) + end + + it 'renders a 200 when a project was set' do + get action, params: params.merge(project_id: project.id) + + expect(response).to have_gitlab_http_status(:ok) + end +end diff --git a/spec/support/shared_examples/controllers/snowplow_event_tracking_examples.rb b/spec/support/shared_examples/controllers/snowplow_event_tracking_examples.rb index 98fc52add51..2e691d1b36f 100644 --- a/spec/support/shared_examples/controllers/snowplow_event_tracking_examples.rb +++ b/spec/support/shared_examples/controllers/snowplow_event_tracking_examples.rb @@ -2,22 +2,26 @@ # # Requires a context containing: # - subject -# - project # - feature_flag_name # - category # - action # - namespace +# Optionaly, the context can contain: +# - project +# - property # - user +# - label +# - **extra -shared_examples 'Snowplow event tracking' do - let(:label) { nil } +shared_examples 'Snowplow event tracking' do |overrides: {}| + let(:extra) { {} } it 'is not emitted if FF is disabled' do stub_feature_flags(feature_flag_name => false) subject - expect_no_snowplow_event + expect_no_snowplow_event(category: category, action: action) end it 'is emitted' do @@ -25,10 +29,11 @@ shared_examples 'Snowplow event tracking' do category: category, action: action, namespace: namespace, - user: user, - project: project, - label: label - }.compact + user: try(:user), + project: try(:project), + label: try(:label), + property: try(:property) + }.merge(overrides).compact.merge(extra) subject diff --git a/spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb b/spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb index 6dca94ecf0a..0792ac14e47 100644 --- a/spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb +++ b/spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb @@ -205,41 +205,13 @@ RSpec.shared_examples 'handle uploads' do allow_any_instance_of(FileUploader).to receive(:image?).and_return(true) end - context "enforce_auth_checks_on_uploads feature flag" do - context "with flag enabled" do - before do - stub_feature_flags(enforce_auth_checks_on_uploads: true) - end + it "responds with the appropriate status code" do + show_upload - it "responds with appropriate status" do - show_upload - - # We're switching here based on the class due to the feature - # flag :enforce_auth_checks_on_uploads switching on project. - # When it is enabled fully, we will apply the code it guards - # to both Projects::UploadsController as well as - # Groups::UploadsController. - # - # https://gitlab.com/gitlab-org/gitlab/-/issues/352291 - # - if model.instance_of?(Group) - expect(response).to have_gitlab_http_status(:ok) - else - expect(response).to have_gitlab_http_status(:redirect) - end - end - end - - context "with flag disabled" do - before do - stub_feature_flags(enforce_auth_checks_on_uploads: false) - end - - it "responds with status 200" do - show_upload - - expect(response).to have_gitlab_http_status(:ok) - end + if model.instance_of?(Group) + expect(response).to have_gitlab_http_status(:ok) + else + expect(response).to have_gitlab_http_status(:redirect) end end end @@ -308,41 +280,13 @@ RSpec.shared_examples 'handle uploads' do allow_any_instance_of(FileUploader).to receive(:image?).and_return(true) end - context "enforce_auth_checks_on_uploads feature flag" do - context "with flag enabled" do - before do - stub_feature_flags(enforce_auth_checks_on_uploads: true) - end - - it "responds with status 404" do - show_upload - - # We're switching here based on the class due to the feature - # flag :enforce_auth_checks_on_uploads switching on - # project. When it is enabled fully, we will apply the - # code it guards to both Projects::UploadsController as - # well as Groups::UploadsController. - # - # https://gitlab.com/gitlab-org/gitlab/-/issues/352291 - # - if model.instance_of?(Group) - expect(response).to have_gitlab_http_status(:ok) - else - expect(response).to have_gitlab_http_status(:not_found) - end - end - end - - context "with flag disabled" do - before do - stub_feature_flags(enforce_auth_checks_on_uploads: false) - end - - it "responds with status 200" do - show_upload + it "responds with the appropriate status code" do + show_upload - expect(response).to have_gitlab_http_status(:ok) - end + if model.instance_of?(Group) + expect(response).to have_gitlab_http_status(:ok) + else + expect(response).to have_gitlab_http_status(:not_found) end end end diff --git a/spec/support/shared_examples/features/access_tokens_shared_examples.rb b/spec/support/shared_examples/features/access_tokens_shared_examples.rb index c162ed36881..0fc45b154d8 100644 --- a/spec/support/shared_examples/features/access_tokens_shared_examples.rb +++ b/spec/support/shared_examples/features/access_tokens_shared_examples.rb @@ -38,7 +38,7 @@ RSpec.shared_examples 'resource access tokens creation' do |resource_type| 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('Maintainer') + expect(active_resource_access_tokens).to have_text('Guest') expect(created_resource_access_token).not_to be_empty 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 0ea82f37db0..3fa7beea97e 100644 --- a/spec/support/shared_examples/features/content_editor_shared_examples.rb +++ b/spec/support/shared_examples/features/content_editor_shared_examples.rb @@ -13,9 +13,8 @@ RSpec.shared_examples 'edits content using the content editor' do expect(page).to have_css('[data-testid="formatting-bubble-menu"]') end - it 'does not show a formatting bubble menu for code' do - find(content_editor_testid).send_keys 'This is a `code`' - find(content_editor_testid).send_keys [:shift, :left] + it 'does not show a formatting bubble menu for code blocks' do + find(content_editor_testid).send_keys '```js ' expect(page).not_to have_css('[data-testid="formatting-bubble-menu"]') end diff --git a/spec/support/shared_examples/features/inviting_members_shared_examples.rb b/spec/support/shared_examples/features/inviting_members_shared_examples.rb index bca0e02fcdd..277ec6a7fa7 100644 --- a/spec/support/shared_examples/features/inviting_members_shared_examples.rb +++ b/spec/support/shared_examples/features/inviting_members_shared_examples.rb @@ -147,9 +147,9 @@ RSpec.shared_examples 'inviting members' do |snowplow_invite_label| invite_member(user2.name, role: role, refresh: false) - expect(page).to have_selector(invite_modal_selector) - expect(page).to have_content "#{user2.name}: Access level should be greater than or equal to Developer " \ - "inherited membership from group #{group.name}" + invite_modal = page.find(invite_modal_selector) + expect(invite_modal).to have_content "#{user2.name}: Access level should be greater than or equal to " \ + "Developer inherited membership from group #{group.name}" page.refresh @@ -166,31 +166,85 @@ RSpec.shared_examples 'inviting members' do |snowplow_invite_label| group.add_maintainer(user3) end - it 'shows the user errors and then removes them from the form', :js do - visit subentity_members_page_path + it 'shows the partial user error and success and then removes them from the form', :js do + user4 = create(:user) + user5 = create(:user) + user6 = create(:user) + user7 = create(:user) + + group.add_maintainer(user6) + group.add_maintainer(user7) - invite_member([user2.name, user3.name], role: role, refresh: false) + visit subentity_members_page_path - expect(page).to have_selector(invite_modal_selector) - expect(page).to have_selector(member_token_error_selector(user2.id)) - expect(page).to have_selector(member_token_error_selector(user3.id)) - expect(page).to have_text("The following 2 members couldn't be invited") - expect(page).to have_text("#{user2.name}: Access level should be greater than or equal to") - expect(page).to have_text("#{user3.name}: Access level should be greater than or equal to") + invite_member([user2.name, user3.name, user4.name, user6.name, user7.name], role: role, refresh: false) + + # we have more than 2 errors, so one will be hidden + invite_modal = page.find(invite_modal_selector) + expect(invite_modal).to have_text("The following 4 members couldn't be invited") + expect(invite_modal).to have_selector(limited_invite_error_selector, count: 2, visible: :visible) + expect(invite_modal).to have_selector(expanded_invite_error_selector, count: 2, visible: :hidden) + # unpredictability of return order means we can't rely on message showing in any order here + # so we will not expect on the message + expect_to_have_invalid_invite_indicator(invite_modal, user2, message: false) + expect_to_have_invalid_invite_indicator(invite_modal, user3, message: false) + expect_to_have_invalid_invite_indicator(invite_modal, user6, message: false) + expect_to_have_invalid_invite_indicator(invite_modal, user7, message: false) + expect_to_have_successful_invite_indicator(invite_modal, user4) + expect(invite_modal).to have_button('Show more (2)') + + # now we want to test the show more errors count logic + remove_token(user7.id) + + # count decreases from 4 to 3 and 2 to 1 + expect(invite_modal).to have_text("The following 3 members couldn't be invited") + expect(invite_modal).to have_button('Show more (1)') + + # we want to show this error now for user6 + invite_modal.find(more_invite_errors_button_selector).click + + # now we should see the error for all users and our collapse button text + expect(invite_modal).to have_selector(limited_invite_error_selector, count: 2, visible: :visible) + expect(invite_modal).to have_selector(expanded_invite_error_selector, count: 1, visible: :visible) + expect_to_have_invalid_invite_indicator(invite_modal, user2, message: true) + expect_to_have_invalid_invite_indicator(invite_modal, user3, message: true) + expect_to_have_invalid_invite_indicator(invite_modal, user6, message: true) + expect(invite_modal).to have_button('Show less') + + # adds new token, but doesn't submit + select_members(user5.name) + + expect_to_have_normal_invite_indicator(invite_modal, user5) remove_token(user2.id) - expect(page).not_to have_selector(member_token_error_selector(user2.id)) - expect(page).to have_selector(member_token_error_selector(user3.id)) - expect(page).to have_text("The following member couldn't be invited") - expect(page).not_to have_text("#{user2.name}: Access level should be greater than or equal to") + expect(invite_modal).to have_text("The following 2 members couldn't be invited") + expect(invite_modal).not_to have_selector(more_invite_errors_button_selector) + expect_to_have_invite_removed(invite_modal, user2) + expect_to_have_invalid_invite_indicator(invite_modal, user3) + expect_to_have_invalid_invite_indicator(invite_modal, user6) + expect_to_have_successful_invite_indicator(invite_modal, user4) + expect_to_have_normal_invite_indicator(invite_modal, user5) + + remove_token(user6.id) + + expect(invite_modal).to have_text("The following member couldn't be invited") + expect_to_have_invite_removed(invite_modal, user6) + expect_to_have_invalid_invite_indicator(invite_modal, user3) + expect_to_have_successful_invite_indicator(invite_modal, user4) + expect_to_have_normal_invite_indicator(invite_modal, user5) remove_token(user3.id) - expect(page).not_to have_selector(member_token_error_selector(user3.id)) - expect(page).not_to have_text("The following member couldn't be invited") - expect(page).not_to have_text("Review the invite errors and try again") - expect(page).not_to have_text("#{user3.name}: Access level should be greater than or equal to") + expect(invite_modal).not_to have_text("The following member couldn't be invited") + expect(invite_modal).not_to have_text("Review the invite errors and try again") + expect_to_have_invite_removed(invite_modal, user3) + expect_to_have_successful_invite_indicator(invite_modal, user4) + expect_to_have_normal_invite_indicator(invite_modal, user5) + + submit_invites + + expect(page).not_to have_selector(invite_modal_selector) page.refresh @@ -203,6 +257,10 @@ RSpec.shared_examples 'inviting members' do |snowplow_invite_label| expect(page).to have_content('Maintainer') expect(page).not_to have_button('Maintainer') end + + page.within find_invited_member_row(user4.name) do + expect(page).to have_button(role) + end end it 'only shows the error for an invalid formatted email and does not display other member errors', :js do @@ -210,12 +268,12 @@ RSpec.shared_examples 'inviting members' do |snowplow_invite_label| invite_member([user2.name, user3.name, 'bad@email'], role: role, refresh: false) - expect(page).to have_selector(invite_modal_selector) - expect(page).to have_text('email contains an invalid email address') - expect(page).not_to have_text("The following 2 members couldn't be invited") - expect(page).not_to have_text("Review the invite errors and try again") - expect(page).not_to have_text("#{user2.name}: Access level should be greater than or equal to") - expect(page).not_to have_text("#{user3.name}: Access level should be greater than or equal to") + invite_modal = page.find(invite_modal_selector) + expect(invite_modal).to have_text('email contains an invalid email address') + expect(invite_modal).not_to have_text("The following 2 members couldn't be invited") + expect(invite_modal).not_to have_text("Review the invite errors and try again") + expect(invite_modal).not_to have_text("#{user2.name}: Access level should be greater than or equal to") + expect(invite_modal).not_to have_text("#{user3.name}: Access level should be greater than or equal to") end end end diff --git a/spec/support/shared_examples/features/multiple_assignees_widget_mr_shared_examples.rb b/spec/support/shared_examples/features/multiple_assignees_widget_mr_shared_examples.rb index bbde448a1a1..ef2683d6424 100644 --- a/spec/support/shared_examples/features/multiple_assignees_widget_mr_shared_examples.rb +++ b/spec/support/shared_examples/features/multiple_assignees_widget_mr_shared_examples.rb @@ -32,7 +32,7 @@ RSpec.shared_examples 'multiple assignees widget merge request' do |action, save end page.within '.dropdown-menu-user' do - click_link user.name + click_button user.name end page.within '.issuable-sidebar' do diff --git a/spec/support/shared_examples/features/sidebar/sidebar_due_date_shared_examples.rb b/spec/support/shared_examples/features/sidebar/sidebar_due_date_shared_examples.rb index 345dfbce423..95c0a76d726 100644 --- a/spec/support/shared_examples/features/sidebar/sidebar_due_date_shared_examples.rb +++ b/spec/support/shared_examples/features/sidebar/sidebar_due_date_shared_examples.rb @@ -16,7 +16,9 @@ RSpec.shared_examples 'date sidebar widget' do page.within('[data-testid="sidebar-due-date"]') do today = Date.today.day - click_button 'Edit' + button = find_button('Edit') + scroll_to(button) + button.click click_button today.to_s diff --git a/spec/support/shared_examples/features/trial_email_validation_shared_example.rb b/spec/support/shared_examples/features/trial_email_validation_shared_example.rb new file mode 100644 index 00000000000..8304a91af86 --- /dev/null +++ b/spec/support/shared_examples/features/trial_email_validation_shared_example.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'user email validation' do + let(:email_hint_message) { 'We recommend a work email address.' } + let(:email_error_message) { 'Please provide a valid email address.' } + + let(:email_warning_message) do + 'This email address does not look right, are you sure you typed it correctly?' + end + + context 'with trial_email_validation flag enabled' do + it 'shows an error message until a correct email is entered' do + visit path + expect(page).to have_content(email_hint_message) + expect(page).not_to have_content(email_error_message) + expect(page).not_to have_content(email_warning_message) + + fill_in 'new_user_email', with: 'foo@' + fill_in 'new_user_first_name', with: '' + + expect(page).not_to have_content(email_hint_message) + expect(page).to have_content(email_error_message) + expect(page).not_to have_content(email_warning_message) + + fill_in 'new_user_email', with: 'foo@bar' + fill_in 'new_user_first_name', with: '' + + expect(page).not_to have_content(email_hint_message) + expect(page).not_to have_content(email_error_message) + expect(page).to have_content(email_warning_message) + + fill_in 'new_user_email', with: 'foo@gitlab.com' + fill_in 'new_user_first_name', with: '' + + expect(page).not_to have_content(email_hint_message) + expect(page).not_to have_content(email_error_message) + expect(page).not_to have_content(email_warning_message) + end + end + + context 'when trial_email_validation flag disabled' do + before do + stub_feature_flags trial_email_validation: false + end + + it 'does not show an error message' do + visit path + expect(page).to have_content(email_hint_message) + expect(page).not_to have_content(email_error_message) + expect(page).not_to have_content(email_warning_message) + + fill_in 'new_user_email', with: 'foo@' + + expect(page).to have_content(email_hint_message) + expect(page).not_to have_content(email_error_message) + expect(page).not_to have_content(email_warning_message) + end + end +end diff --git a/spec/support/shared_examples/features/user_views_tag_shared_examples.rb b/spec/support/shared_examples/features/user_views_tag_shared_examples.rb new file mode 100644 index 00000000000..989de1dbfbb --- /dev/null +++ b/spec/support/shared_examples/features/user_views_tag_shared_examples.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'user views tag' do + context 'when user views with the tag' do + let(:project) { create(:project, :repository) } + let(:user) { create(:user) } + let(:tag_name) { "stable" } + let!(:release) { create(:release, project: project, tag: tag_name, name: "ReleaseName") } + + before do + project.add_developer(user) + project.repository.add_tag(user, tag_name, project.default_branch_or_main) + + sign_in(user) + end + + shared_examples 'shows tag' do + it do + visit tag_page + + expect(page).to have_content tag_name + expect(page).to have_link("ReleaseName", href: project_release_path(project, release)) + end + end + + it_behaves_like 'shows tag' + + context 'when tag name contains a slash' do + let(:tag_name) { "stable/v0.1" } + + it_behaves_like 'shows tag' + 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 c63faace6b2..9d81c0e9a3e 100644 --- a/spec/support/shared_examples/features/variable_list_shared_examples.rb +++ b/spec/support/shared_examples/features/variable_list_shared_examples.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -RSpec.shared_examples 'variable list' do +RSpec.shared_examples 'variable list' do |is_admin| it 'shows a list of variables' do page.within('[data-testid="ci-variable-table"]') do expect(find('.js-ci-variable-row:nth-child(1) td[data-label="Key"]').text).to eq(variable.key) @@ -166,7 +166,7 @@ RSpec.shared_examples 'variable list' do wait_for_requests expect(find('.flash-container')).to be_present - expect(find('[data-testid="alert-danger"]').text).to have_content('Variables key (key) has already been taken') + expect(find('[data-testid="alert-danger"]').text).to have_content('(key) has already been taken') end it 'prevents a variable to be added if no values are provided when a variable is set to masked' do @@ -257,7 +257,11 @@ RSpec.shared_examples 'variable list' do end it 'shows a message regarding the changed default' do - expect(page).to have_content 'Environment variables are configured by your administrator to be protected by default' + if is_admin + expect(page).to have_content 'Environment variables on this GitLab instance are configured to be protected by default' + else + expect(page).to have_content 'Environment variables are configured by your administrator to be protected by default' + end end end diff --git a/spec/support/shared_examples/features/wiki/file_attachments_shared_examples.rb b/spec/support/shared_examples/features/wiki/file_attachments_shared_examples.rb index 0ef1ccdfe57..8d1502bed84 100644 --- a/spec/support/shared_examples/features/wiki/file_attachments_shared_examples.rb +++ b/spec/support/shared_examples/features/wiki/file_attachments_shared_examples.rb @@ -12,8 +12,8 @@ RSpec.shared_examples 'wiki file attachments' do end context 'before uploading' do - it 'shows "Attach a file" button' do - expect(page).to have_button('Attach a file') + it 'shows "Attach a file or image" button' do + expect(page).to have_selector('[data-testid="button-attach-file"]') expect(page).not_to have_selector('.uploading-progress-container', visible: true) end end @@ -26,7 +26,7 @@ RSpec.shared_examples 'wiki file attachments' do click_button 'Cancel' end - expect(page).to have_button('Attach a file') + expect(page).to have_selector('[data-testid="button-attach-file"]') expect(page).not_to have_button('Cancel') expect(page).not_to have_selector('.uploading-progress-container', visible: true) end @@ -41,11 +41,11 @@ RSpec.shared_examples 'wiki file attachments' do end context 'uploading is complete' do - it 'shows "Attach a file" button on uploading complete' do + it 'shows "Attach a file or image" button on uploading complete' do attach_with_dropzone wait_for_requests - expect(page).to have_button('Attach a file') + expect(page).to have_selector('[data-testid="button-attach-file"]') expect(page).not_to have_selector('.uploading-progress-container', visible: true) end diff --git a/spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb index 79c7c1891ac..87067336a36 100644 --- a/spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb +++ b/spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb @@ -140,7 +140,7 @@ RSpec.shared_examples 'User updates wiki page' do context 'when using the content editor' do context 'with feature flag on' do before do - click_button 'Edit rich text' + find('[data-testid="toggle-editing-mode-button"] label', text: 'Rich text').click end it_behaves_like 'edits content using the content editor' diff --git a/spec/support/shared_examples/graphql/mutations/timelogs/create_shared_examples.rb b/spec/support/shared_examples/graphql/mutations/timelogs/create_shared_examples.rb new file mode 100644 index 00000000000..f28348fb945 --- /dev/null +++ b/spec/support/shared_examples/graphql/mutations/timelogs/create_shared_examples.rb @@ -0,0 +1,97 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'issuable supports timelog creation mutation' do + context 'when the user is anonymous' do + before do + post_graphql_mutation(mutation, current_user: current_user) + end + + it_behaves_like 'a mutation that returns a top-level access error' + end + + context 'when the user is a guest member of the namespace' do + let(:current_user) { create(:user) } + + before do + users_container.add_guest(current_user) + + post_graphql_mutation(mutation, current_user: current_user) + end + + it_behaves_like 'a mutation that returns a top-level access error' + end + + context 'when user has permissions to create a timelog' do + let(:current_user) { author } + + before do + users_container.add_reporter(current_user) + end + + context 'with valid data' do + it 'creates the timelog' do + expect do + post_graphql_mutation(mutation, current_user: current_user) + end.to change(Timelog, :count).by(1) + + expect(response).to have_gitlab_http_status(:success) + expect(mutation_response['errors']).to be_empty + expect(mutation_response['timelog']).to include( + 'timeSpent' => 3600, + 'spentAt' => '2022-07-08T00:00:00Z', + 'summary' => 'Test summary' + ) + end + end + + context 'with invalid time_spent' do + let(:time_spent) { '3h e' } + + it 'returns an error' do + expect do + post_graphql_mutation(mutation, current_user: current_user) + end.to change(Timelog, :count).by(0) + + expect(response).to have_gitlab_http_status(:success) + expect(mutation_response['errors']).to match_array(['Time spent can\'t be blank']) + expect(mutation_response['timelog']).to be_nil + end + end + end +end + +RSpec.shared_examples 'issuable does not support timelog creation mutation' do + context 'when the user is anonymous' do + before do + post_graphql_mutation(mutation, current_user: current_user) + end + + it_behaves_like 'a mutation that returns a top-level access error' + end + + context 'when the user is a guest member of the namespace' do + let(:current_user) { create(:user) } + + before do + users_container.add_guest(current_user) + + post_graphql_mutation(mutation, current_user: current_user) + end + + it_behaves_like 'a mutation that returns top-level errors' do + let(:match_errors) { contain_exactly(include('is not a valid ID for')) } + end + end + + context 'when user has permissions to create a timelog' do + let(:current_user) { author } + + before do + users_container.add_reporter(current_user) + end + + it_behaves_like 'a mutation that returns top-level errors' do + let(:match_errors) { contain_exactly(include('is not a valid ID for')) } + end + end +end diff --git a/spec/support/shared_examples/graphql/mutations/work_items/update_weight_widget_shared_examples.rb b/spec/support/shared_examples/graphql/mutations/work_items/update_weight_widget_shared_examples.rb deleted file mode 100644 index 3c32b7e0310..00000000000 --- a/spec/support/shared_examples/graphql/mutations/work_items/update_weight_widget_shared_examples.rb +++ /dev/null @@ -1,34 +0,0 @@ -# frozen_string_literal: true - -RSpec.shared_examples 'update work item weight widget' do - it 'updates the weight widget' do - expect do - post_graphql_mutation(mutation, current_user: current_user) - work_item.reload - end.to change(work_item, :weight).from(nil).to(new_weight) - - expect(response).to have_gitlab_http_status(:success) - expect(mutation_response['workItem']['widgets']).to include( - { - 'weight' => new_weight, - 'type' => 'WEIGHT' - } - ) - end - - context 'when the updated work item is not valid' do - it 'returns validation errors without the work item' do - errors = ActiveModel::Errors.new(work_item).tap { |e| e.add(:weight, 'error message') } - - allow_next_found_instance_of(::WorkItem) do |instance| - allow(instance).to receive(:valid?).and_return(false) - allow(instance).to receive(:errors).and_return(errors) - end - - post_graphql_mutation(mutation, current_user: current_user) - - expect(mutation_response['workItem']).to be_nil - expect(mutation_response['errors']).to match_array(['Weight error message']) - end - 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 2c6118779e6..0aa3bf8944f 100644 --- a/spec/support/shared_examples/graphql/notes_creation_shared_examples.rb +++ b/spec/support/shared_examples/graphql/notes_creation_shared_examples.rb @@ -94,5 +94,6 @@ RSpec.shared_examples 'a Note mutation with confidential notes' do expect(mutation_response).to have_key('note') expect(mutation_response['note']['confidential']).to eq(true) + expect(mutation_response['note']['internal']).to eq(true) end end 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 7fd54408b11..2d7da9f9f00 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 @@ -69,4 +69,21 @@ RSpec.shared_examples 'Gitlab-style deprecations' do 'This feature is in Alpha. It can be changed or removed at any time. Introduced in 1.10.' ) end + + it 'supports :alpha' do + deprecable = subject(alpha: { milestone: '1.10' }) + + expect(deprecable.deprecation_reason).to eq( + 'This feature is in Alpha. It can be changed or removed at any time. Introduced in 1.10.' + ) + end + + it 'does not allow :alpha and :deprecated together' do + expect do + subject(alpha: { milestone: '1.10' }, deprecated: { milestone: '1.10', reason: 'my reason' } ) + end.to raise_error( + ArgumentError, + eq("`alpha` and `deprecated` arguments cannot be passed at the same time") + ) + end end diff --git a/spec/support/shared_examples/helpers/wiki_helpers_shared_examples.rb b/spec/support/shared_examples/helpers/wiki_helpers_shared_examples.rb index c2c27fb65ca..61c8a3f47df 100644 --- a/spec/support/shared_examples/helpers/wiki_helpers_shared_examples.rb +++ b/spec/support/shared_examples/helpers/wiki_helpers_shared_examples.rb @@ -2,7 +2,7 @@ RSpec.shared_examples 'wiki endpoint helpers' do let(:resource_path) { page.wiki.container.class.to_s.pluralize.downcase } - let(:url) { "/api/v4/#{resource_path}/#{page.wiki.container.id}/wikis/#{page.slug}?version=#{page.version.id}"} + let(:url) { "/api/v4/#{resource_path}/#{page.wiki.container.id}/wikis/#{page.slug}?version=#{page.version.id}" } it 'returns the full endpoint url' do expect(helper.wiki_page_render_api_endpoint(page)).to end_with(url) diff --git a/spec/support/shared_examples/lib/gitlab/config/inheritable_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/config/inheritable_shared_examples.rb index 95772b1774a..5eae8777a20 100644 --- a/spec/support/shared_examples/lib/gitlab/config/inheritable_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/config/inheritable_shared_examples.rb @@ -86,7 +86,10 @@ RSpec.shared_examples 'with inheritable CI config' do expect do # we ignore exceptions as `#overwrite_entry` # can raise exception on duplicates - entry.send(:inherit!, deps) rescue described_class::InheritError + + entry.send(:inherit!, deps) + rescue described_class::InheritError + nil end.not_to change { entry[entry_key] } end end diff --git a/spec/support/shared_examples/lib/gitlab/usage_data_counters/issuable_activity_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/usage_data_counters/issuable_activity_shared_examples.rb index 9d280d9404a..481e11bcf0e 100644 --- a/spec/support/shared_examples/lib/gitlab/usage_data_counters/issuable_activity_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/usage_data_counters/issuable_activity_shared_examples.rb @@ -33,7 +33,7 @@ RSpec.shared_examples 'does not track when feature flag is disabled' do |feature end end -RSpec.shared_examples 'a daily tracked issuable snowplow and service ping events' do +RSpec.shared_examples 'a daily tracked issuable snowplow and service ping events for given event params' do before do stub_application_setting(usage_ping_enabled: true) end @@ -44,22 +44,21 @@ RSpec.shared_examples 'a daily tracked issuable snowplow and service ping events specify do aggregate_failures do - expect(track_action(author: user1, project: project)).to be_truthy - expect(track_action(author: user1, project: project)).to be_truthy - expect(track_action(author: user2, project: project)).to be_truthy + expect(track_action({ author: user1 }.merge(track_params))).to be_truthy + expect(track_action({ author: user1 }.merge(track_params))).to be_truthy + expect(track_action({ author: user2 }.merge(track_params))).to be_truthy expect(count_unique).to eq(2) end end it 'does not track edit actions if author is not present' do - expect(track_action(author: nil, project: project)).to be_nil + expect(track_action({ author: nil }.merge(track_params))).to be_nil end it 'emits snowplow event' do - track_action(author: user1, project: project) + track_action({ author: user1 }.merge(track_params)) - expect_snowplow_event(category: 'issues_edit', action: action, user: user1, - namespace: project.namespace, project: project) + expect_snowplow_event(**{ category: category, action: event_action, user: user1 }.merge(event_params)) end context 'with route_hll_to_snowplow_phase2 disabled' do @@ -68,9 +67,33 @@ RSpec.shared_examples 'a daily tracked issuable snowplow and service ping events end it 'does not emit snowplow event' do - track_action(author: user1, project: project) + track_action({ author: user1 }.merge(track_params)) expect_no_snowplow_event end end end + +RSpec.shared_examples 'daily tracked issuable snowplow and service ping events with project' do + it_behaves_like 'a daily tracked issuable snowplow and service ping events for given event params' do + let(:track_params) { { project: project } } + let(:event_params) { track_params.merge(label: event_label, property: event_property, namespace: project.namespace) } + end +end + +RSpec.shared_examples 'a daily tracked issuable snowplow and service ping events with namespace' do + it_behaves_like 'a daily tracked issuable snowplow and service ping events for given event params' do + let(:track_params) { { namespace: namespace } } + let(:event_params) { track_params.merge(label: event_label, property: event_property) } + end +end + +RSpec.shared_examples 'does not track with namespace when feature flag is disabled' do |feature_flag| + context "when feature flag #{feature_flag} is disabled" do + it 'does not track action' do + stub_feature_flags(feature_flag => false) + + expect(track_action(author: user1, namespace: namespace)).to be_nil + end + end +end diff --git a/spec/support/shared_examples/models/chat_integration_shared_examples.rb b/spec/support/shared_examples/models/chat_integration_shared_examples.rb index d189e91effd..fb08784f34f 100644 --- a/spec/support/shared_examples/models/chat_integration_shared_examples.rb +++ b/spec/support/shared_examples/models/chat_integration_shared_examples.rb @@ -3,7 +3,6 @@ RSpec.shared_examples "chat integration" do |integration_name| describe "Associations" do it { is_expected.to belong_to :project } - it { is_expected.to have_one :service_hook } end describe "Validations" do @@ -13,6 +12,7 @@ RSpec.shared_examples "chat integration" do |integration_name| end it { is_expected.to validate_presence_of(:webhook) } + it_behaves_like "issue tracker integration URL attribute", :webhook end diff --git a/spec/support/shared_examples/models/ci/metadata_id_tokens_shared_examples.rb b/spec/support/shared_examples/models/ci/metadata_id_tokens_shared_examples.rb new file mode 100644 index 00000000000..0c71ebe7a4d --- /dev/null +++ b/spec/support/shared_examples/models/ci/metadata_id_tokens_shared_examples.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +RSpec.shared_examples_for 'has ID tokens' do |ci_type| + subject(:ci) { FactoryBot.build(ci_type) } + + describe 'delegations' do + it { is_expected.to delegate_method(:id_tokens).to(:metadata).allow_nil } + end + + describe '#id_tokens?' do + subject { ci.id_tokens? } + + context 'without metadata' do + let(:ci) { FactoryBot.build(ci_type) } + + it { is_expected.to be_falsy } + end + + context 'with metadata' do + let(:ci) { FactoryBot.build(ci_type, metadata: FactoryBot.build(:ci_build_metadata, id_tokens: id_tokens)) } + + context 'when ID tokens exist' do + let(:id_tokens) { { TEST_JOB_JWT: { id_token: { aud: 'developers ' } } } } + + it { is_expected.to be_truthy } + end + + context 'when ID tokens do not exist' do + let(:id_tokens) { {} } + + it { is_expected.to be_falsy } + end + end + end + + describe '#id_tokens=' do + it 'assigns the ID tokens to the CI job' do + id_tokens = [{ 'JOB_ID_TOKEN' => { 'id_token' => { 'aud' => 'https://gitlab.test ' } } }] + ci.id_tokens = id_tokens + + expect(ci.id_tokens).to match_array(id_tokens) + end + end +end diff --git a/spec/support/shared_examples/models/concerns/counter_attribute_shared_examples.rb b/spec/support/shared_examples/models/concerns/counter_attribute_shared_examples.rb index f92ed3d7396..f4d5ab3d5c6 100644 --- a/spec/support/shared_examples/models/concerns/counter_attribute_shared_examples.rb +++ b/spec/support/shared_examples/models/concerns/counter_attribute_shared_examples.rb @@ -2,6 +2,10 @@ require 'spec_helper' RSpec.shared_examples_for CounterAttribute do |counter_attributes| + before do + Gitlab::ApplicationContext.push(feature_category: 'test', caller_id: 'caller') + end + it 'defines a Redis counter_key' do expect(model.counter_key(:counter_name)) .to eq("project:{#{model.project_id}}:counters:CounterAttributeModel:#{model.id}:counter_name") @@ -22,7 +26,21 @@ RSpec.shared_examples_for CounterAttribute do |counter_attributes| where(:increment) { [10, -3] } with_them do - it 'increments the counter in Redis' do + it 'increments the counter in Redis and logs it' do + expect(Gitlab::AppLogger).to receive(:info).with( + hash_including( + message: 'Increment counter attribute', + attribute: attribute, + project_id: model.project_id, + increment: increment, + new_counter_value: 0 + increment, + current_db_value: model.read_attribute(attribute), + 'correlation_id' => an_instance_of(String), + 'meta.feature_category' => 'test', + 'meta.caller_id' => 'caller' + ) + ) + subject Gitlab::Redis::SharedState.with do |redis| @@ -86,7 +104,21 @@ RSpec.shared_examples_for CounterAttribute do |counter_attributes| model.delayed_increment_counter(incremented_attribute, -3) end - it 'updates the record' do + it 'updates the record and logs it' do + expect(Gitlab::AppLogger).to receive(:info).with( + hash_including( + message: 'Flush counter attribute to database', + attribute: incremented_attribute, + project_id: model.project_id, + increment: 7, + previous_db_value: 0, + new_db_value: 7, + 'correlation_id' => an_instance_of(String), + 'meta.feature_category' => 'test', + 'meta.caller_id' => 'caller' + ) + ) + expect { subject }.to change { model.reset.read_attribute(incremented_attribute) }.by(7) end @@ -153,4 +185,32 @@ RSpec.shared_examples_for CounterAttribute do |counter_attributes| end end end + + describe '#clear_counter!' do + let(:attribute) { counter_attributes.first } + + before do + model.increment_counter(attribute, 10) + end + + it 'deletes the counter key for the given attribute and logs it' do + expect(Gitlab::AppLogger).to receive(:info).with( + hash_including( + message: 'Clear counter attribute', + attribute: attribute, + project_id: model.project_id, + 'correlation_id' => an_instance_of(String), + 'meta.feature_category' => 'test', + 'meta.caller_id' => 'caller' + ) + ) + + model.clear_counter!(attribute) + + Gitlab::Redis::SharedState.with do |redis| + key_exists = redis.exists(model.counter_key(attribute)) + expect(key_exists).to be_falsey + 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 d80be5be3b3..7512a9f2855 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 @@ -13,7 +13,6 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |integration_name describe "Associations" do it { is_expected.to belong_to :project } - it { is_expected.to have_one :service_hook } end describe 'Validations' do @@ -23,6 +22,7 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |integration_name end it { is_expected.to validate_presence_of(:webhook) } + it_behaves_like 'issue tracker integration URL attribute', :webhook end 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 ae72cb6ec5d..2f693edeb53 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 @@ -3,6 +3,10 @@ RSpec.shared_examples Integrations::HasWebHook do include AfterNextHelpers + describe 'associations' do + it { is_expected.to have_one(:service_hook).inverse_of(:integration).with_foreign_key(:service_id) } + end + describe 'callbacks' do it 'calls #update_web_hook! when enabled' do expect(integration).to receive(:update_web_hook!) diff --git a/spec/support/shared_examples/models/issuable_link_shared_examples.rb b/spec/support/shared_examples/models/issuable_link_shared_examples.rb index 9892e66b582..42c7be5ddc3 100644 --- a/spec/support/shared_examples/models/issuable_link_shared_examples.rb +++ b/spec/support/shared_examples/models/issuable_link_shared_examples.rb @@ -16,6 +16,7 @@ RSpec.shared_examples 'issuable link' do it { is_expected.to validate_presence_of(:source) } it { is_expected.to validate_presence_of(:target) } + it do is_expected.to validate_uniqueness_of(:source) .scoped_to(:target_id) diff --git a/spec/support/shared_examples/models/member_shared_examples.rb b/spec/support/shared_examples/models/member_shared_examples.rb index aa40a2c7135..287b046cbec 100644 --- a/spec/support/shared_examples/models/member_shared_examples.rb +++ b/spec/support/shared_examples/models/member_shared_examples.rb @@ -63,16 +63,23 @@ RSpec.shared_examples '#valid_level_roles' do |entity_name| let(:entity) { create(entity_name) } # rubocop:disable Rails/SaveBang let(:entity_member) { create("#{entity_name}_member", :developer, source: entity, user: member_user) } let(:presenter) { described_class.new(entity_member, current_user: member_user) } - let(:expected_roles) { { 'Developer' => 30, 'Maintainer' => 40, 'Reporter' => 20 } } - it 'returns all roles when no parent member is present' do - expect(presenter.valid_level_roles).to eq(entity_member.class.access_level_roles) + context 'when no parent member is present' do + let(:all_permissible_roles) { entity_member.class.permissible_access_level_roles(member_user, entity) } + + it 'returns all permissible roles' do + expect(presenter.valid_level_roles).to eq(all_permissible_roles) + end end - it 'returns higher roles when a parent member is present' do - group.add_reporter(member_user) + context 'when parent member is present' do + before do + group.add_reporter(member_user) + end - expect(presenter.valid_level_roles).to eq(expected_roles) + it 'returns higher roles when a parent member is present' do + expect(presenter.valid_level_roles).to eq(expected_roles) + end end end diff --git a/spec/support/shared_examples/models/project_shared_examples.rb b/spec/support/shared_examples/models/project_shared_examples.rb index 475ac1da04b..0b880f00a22 100644 --- a/spec/support/shared_examples/models/project_shared_examples.rb +++ b/spec/support/shared_examples/models/project_shared_examples.rb @@ -25,3 +25,38 @@ RSpec.shared_examples 'returns true if project is inactive' do end end end + +RSpec.shared_examples 'checks parent group feature flag' do + let(:group) { subject_project.group } + let(:root_group) { group.parent } + + subject { subject_project.public_send(feature_flag_method) } + + context 'when feature flag is disabled globally' do + before do + stub_feature_flags(feature_flag => false) + end + + it { is_expected.to be_falsey } + end + + context 'when feature flag is enabled globally' do + it { is_expected.to be_truthy } + end + + context 'when feature flag is enabled for the root group' do + before do + stub_feature_flags(feature_flag => root_group) + end + + it { is_expected.to be_truthy } + end + + context 'when feature flag is enabled for the group' do + before do + stub_feature_flags(feature_flag => group) + end + + it { is_expected.to be_truthy } + end +end diff --git a/spec/support/shared_examples/models/taskable_shared_examples.rb b/spec/support/shared_examples/models/taskable_shared_examples.rb index 34b1d735bcd..3ae240c8da8 100644 --- a/spec/support/shared_examples/models/taskable_shared_examples.rb +++ b/spec/support/shared_examples/models/taskable_shared_examples.rb @@ -18,9 +18,9 @@ RSpec.shared_examples 'a Taskable' do it 'returns the correct task status' do expect(subject.task_status).to match('2 of') - expect(subject.task_status).to match('5 tasks completed') + expect(subject.task_status).to match('5 checklist items completed') expect(subject.task_status_short).to match('2/') - expect(subject.task_status_short).to match('5 tasks') + expect(subject.task_status_short).to match('5 checklist items') end describe '#tasks?' do @@ -53,9 +53,9 @@ RSpec.shared_examples 'a Taskable' do it 'returns the correct task status' do expect(subject.task_status).to match('3 of') - expect(subject.task_status).to match('9 tasks completed') + expect(subject.task_status).to match('9 checklist items completed') expect(subject.task_status_short).to match('3/') - expect(subject.task_status_short).to match('9 tasks') + expect(subject.task_status_short).to match('9 checklist items') end end @@ -68,9 +68,9 @@ RSpec.shared_examples 'a Taskable' do it 'returns the correct task status' do expect(subject.task_status).to match('0 of') - expect(subject.task_status).to match('1 task completed') + expect(subject.task_status).to match('1 checklist item completed') expect(subject.task_status_short).to match('0/') - expect(subject.task_status_short).to match('1 task') + expect(subject.task_status_short).to match('1 checklist item') end end @@ -87,9 +87,9 @@ RSpec.shared_examples 'a Taskable' do it 'returns the correct task status' do expect(subject.task_status).to match('0 of') - expect(subject.task_status).to match('0 tasks completed') + expect(subject.task_status).to match('0 checklist items completed') expect(subject.task_status_short).to match('0/') - expect(subject.task_status_short).to match('0 task') + expect(subject.task_status_short).to match('0 checklist items') end end @@ -102,9 +102,9 @@ RSpec.shared_examples 'a Taskable' do it 'returns the correct task status' do expect(subject.task_status).to match('1 of') - expect(subject.task_status).to match('1 task completed') + expect(subject.task_status).to match('1 checklist item completed') expect(subject.task_status_short).to match('1/') - expect(subject.task_status_short).to match('1 task') + expect(subject.task_status_short).to match('1 checklist item') end end @@ -123,9 +123,9 @@ RSpec.shared_examples 'a Taskable' do it 'returns the correct task status' do expect(subject.task_status).to match('2 of') - expect(subject.task_status).to match('4 tasks completed') + expect(subject.task_status).to match('4 checklist items completed') expect(subject.task_status_short).to match('2/') - expect(subject.task_status_short).to match('4 tasks') + expect(subject.task_status_short).to match('4 checklist items') end end end diff --git a/spec/support/shared_examples/namespaces/traversal_scope_examples.rb b/spec/support/shared_examples/namespaces/traversal_scope_examples.rb index 45da1d382c1..807295f8442 100644 --- a/spec/support/shared_examples/namespaces/traversal_scope_examples.rb +++ b/spec/support/shared_examples/namespaces/traversal_scope_examples.rb @@ -273,14 +273,6 @@ RSpec.shared_examples 'namespace traversal scopes' do include_examples '.self_and_descendants' end - - context 'with linear_scopes_superset feature flag disabled' do - before do - stub_feature_flags(linear_scopes_superset: false) - end - - include_examples '.self_and_descendants' - end end shared_examples '.self_and_descendant_ids' do @@ -324,14 +316,6 @@ RSpec.shared_examples 'namespace traversal scopes' do include_examples '.self_and_descendant_ids' end - - context 'with linear_scopes_superset feature flag disabled' do - before do - stub_feature_flags(linear_scopes_superset: false) - end - - include_examples '.self_and_descendant_ids' - end end shared_examples '.self_and_hierarchy' do diff --git a/spec/support/shared_examples/policies/group_project_namespace_policy_shared_examples.rb b/spec/support/shared_examples/policies/group_project_namespace_policy_shared_examples.rb new file mode 100644 index 00000000000..9a1f0e685be --- /dev/null +++ b/spec/support/shared_examples/policies/group_project_namespace_policy_shared_examples.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'checks timelog categories permissions' do + context 'with no user' do + let_it_be(:current_user) { nil } + + it { is_expected.to be_disallowed(:read_timelog_category) } + end + + context 'with a regular user' do + let_it_be(:current_user) { create(:user) } + + it { is_expected.to be_disallowed(:read_timelog_category) } + end + + context 'with a reporter user' do + let_it_be(:current_user) { create(:user) } + + before do + users_container.add_reporter(current_user) + end + + context 'when timelog_categories is enabled' do + it { is_expected.to be_allowed(:read_timelog_category) } + end + + context 'when timelog_categories is disabled' do + before do + stub_feature_flags(timelog_categories: false) + end + + it { is_expected.to be_disallowed(:read_timelog_category) } + end + end +end 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 a12cb24a513..32562aef8d2 100644 --- a/spec/support/shared_examples/requests/api/discussions_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/discussions_shared_examples.rb @@ -128,10 +128,10 @@ RSpec.shared_examples 'discussions API' do |parent_type, noteable_type, id_name, stub_feature_flags(notes_create_service_tracking: false) end - it 'does not track any events', :snowplow do + it 'does not track Notes::CreateService events', :snowplow do post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/discussions"), params: { body: 'hi!' } - expect_no_snowplow_event + expect_no_snowplow_event(category: 'Notes::CreateService', action: 'execute') end 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 a59235486ec..8479493911b 100644 --- a/spec/support/shared_examples/requests/api/notes_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/notes_shared_examples.rb @@ -376,13 +376,28 @@ RSpec.shared_examples 'noteable API with confidential notes' do |parent_type, no post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", user), params: params end - it "creates a confidential note if confidential is set to true" do - post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", user), params: params.merge(confidential: true) + context 'with internal param' do + it "creates a confidential note if internal is set to true" do + post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", user), params: params.merge(internal: true) - expect(response).to have_gitlab_http_status(:created) - expect(json_response['body']).to eq('hi!') - expect(json_response['confidential']).to be_truthy - expect(json_response['author']['username']).to eq(user.username) + expect(response).to have_gitlab_http_status(:created) + expect(json_response['body']).to eq('hi!') + expect(json_response['confidential']).to be_truthy + expect(json_response['internal']).to be_truthy + expect(json_response['author']['username']).to eq(user.username) + end + end + + context 'with deprecated confidential param' do + it "creates a confidential note if confidential is set to true" do + post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", user), params: params.merge(confidential: true) + + expect(response).to have_gitlab_http_status(:created) + expect(json_response['body']).to eq('hi!') + expect(json_response['confidential']).to be_truthy + expect(json_response['internal']).to be_truthy + expect(json_response['author']['username']).to eq(user.username) + end end end end diff --git a/spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb index 8d6d85732be..b651ffc8996 100644 --- a/spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb @@ -244,7 +244,7 @@ RSpec.shared_examples 'handling get metadata requests' do |scope: :project| let(:headers) do case auth when :oauth - build_token_auth_header(token.token) + build_token_auth_header(token.plaintext_token) when :personal_access_token build_token_auth_header(personal_access_token.token) when :job_token @@ -404,7 +404,7 @@ RSpec.shared_examples 'handling get dist tags requests' do |scope: :project| shared_examples 'handling all conditions' do context 'with oauth token' do - let(:headers) { build_token_auth_header(token.token) } + let(:headers) { build_token_auth_header(token.plaintext_token) } it_behaves_like 'handling different package names, visibilities and user roles' end @@ -514,7 +514,7 @@ RSpec.shared_examples 'handling create dist tag requests' do |scope: :project| shared_examples 'handling all conditions' do context 'with oauth token' do - let(:headers) { build_token_auth_header(token.token) } + let(:headers) { build_token_auth_header(token.plaintext_token) } it_behaves_like 'handling different package names, visibilities and user roles' end @@ -622,7 +622,7 @@ RSpec.shared_examples 'handling delete dist tag requests' do |scope: :project| shared_examples 'handling all conditions' do context 'with oauth token' do - let(:headers) { build_token_auth_header(token.token) } + let(:headers) { build_token_auth_header(token.plaintext_token) } it_behaves_like 'handling different package names, visibilities and user roles' end diff --git a/spec/support/shared_examples/services/alert_management/alert_processing/alert_firing_shared_examples.rb b/spec/support/shared_examples/services/alert_management/alert_processing/alert_firing_shared_examples.rb index ca86cb082a7..6cae7d8e00f 100644 --- a/spec/support/shared_examples/services/alert_management/alert_processing/alert_firing_shared_examples.rb +++ b/spec/support/shared_examples/services/alert_management/alert_processing/alert_firing_shared_examples.rb @@ -23,7 +23,7 @@ RSpec.shared_examples 'creates an alert management alert or errors' do end context 'and fails to save' do - let(:errors) { double(messages: { hosts: ['hosts array is over 255 chars'] })} + let(:errors) { double(messages: { hosts: ['hosts array is over 255 chars'] }, '[]': [] )} before do allow(service).to receive(:alert).and_call_original @@ -35,9 +35,10 @@ RSpec.shared_examples 'creates an alert management alert or errors' do it 'writes a warning to the log' do expect(Gitlab::AppLogger).to receive(:warn).with( - message: "Unable to create AlertManagement::Alert from #{source}", + message: "Unable to create AlertManagement::Alert", project_id: project.id, - alert_errors: { hosts: ['hosts array is over 255 chars'] } + alert_errors: { hosts: ['hosts array is over 255 chars'] }, + alert_source: source ) subject @@ -45,6 +46,46 @@ RSpec.shared_examples 'creates an alert management alert or errors' do end end +RSpec.shared_examples 'handles race condition in alert creation' do + let(:other_alert) { create(:alert_management_alert, project: project) } + + context 'when another alert is saved at the same time' do + before do + allow_next_instance_of(::AlertManagement::Alert) do |alert| + allow(alert).to receive(:save) do + other_alert.update!(fingerprint: alert.fingerprint) + + raise ActiveRecord::RecordNotUnique + end + end + end + + it 'finds the other alert and increments the counter' do + subject + + expect(other_alert.reload.events).to eq(2) + end + end + + context 'when another alert is saved before the validation runes' do + before do + allow_next_instance_of(::AlertManagement::Alert) do |alert| + allow(alert).to receive(:save).and_wrap_original do |method, *args| + other_alert.update!(fingerprint: alert.fingerprint) + + method.call(*args) + end + end + end + + it 'finds the other alert and increments the counter' do + subject + + expect(other_alert.reload.events).to eq(2) + end + end +end + # This shared_example requires the following variables: # - last_alert_attributes, last created alert # - project, project that alert created diff --git a/spec/support/shared_examples/services/alert_management/alert_processing/alert_recovery_shared_examples.rb b/spec/support/shared_examples/services/alert_management/alert_processing/alert_recovery_shared_examples.rb index f8e096297d3..eb9f76d8626 100644 --- a/spec/support/shared_examples/services/alert_management/alert_processing/alert_recovery_shared_examples.rb +++ b/spec/support/shared_examples/services/alert_management/alert_processing/alert_recovery_shared_examples.rb @@ -4,8 +4,6 @@ # - `alert`, the alert to be resolved RSpec.shared_examples 'resolves an existing alert management alert' do it 'sets the end time and status' do - expect(Gitlab::AppLogger).not_to receive(:warn) - expect { subject } .to change { alert.reload.resolved? }.to(true) .and change { alert.ended_at.present? }.to(true) @@ -22,36 +20,6 @@ RSpec.shared_examples 'does not change the alert end time' do end end -# This shared_example requires the following variables: -# - `project`, expected project for an incoming alert -# - `service`, a service which includes AlertManagement::AlertProcessing -# - `alert` (optional), the alert which should fail to resolve. If not -# included, the log is expected to correspond to a new alert -RSpec.shared_examples 'writes a warning to the log for a failed alert status update' do - before do - allow(service).to receive(:alert).and_call_original - allow(service).to receive_message_chain(:alert, :resolve).and_return(false) - end - - specify do - expect(Gitlab::AppLogger).to receive(:warn).with( - message: 'Unable to update AlertManagement::Alert status to resolved', - project_id: project.id, - alert_id: alert ? alert.id : (last_alert_id + 1) - ) - - # Failure to resolve a recovery alert is not a critical failure - expect(subject).to be_success - end - - private - - def last_alert_id - AlertManagement::Alert.connection - .select_value("SELECT nextval('#{AlertManagement::Alert.sequence_name}')") - end -end - RSpec.shared_examples 'processes recovery alert' do context 'seen for the first time' do let(:alert) { AlertManagement::Alert.last } @@ -69,7 +37,6 @@ RSpec.shared_examples 'processes recovery alert' do it_behaves_like 'creates expected system notes for alert', :recovery_alert, :resolve_alert it_behaves_like 'sends alert notification emails if enabled' it_behaves_like 'closes related incident if enabled' - it_behaves_like 'writes a warning to the log for a failed alert status update' it_behaves_like 'does not create an alert management alert' it_behaves_like 'does not process incident issues' @@ -83,7 +50,6 @@ RSpec.shared_examples 'processes recovery alert' do it_behaves_like 'creates expected system notes for alert', :recovery_alert, :resolve_alert it_behaves_like 'sends alert notification emails if enabled' it_behaves_like 'closes related incident if enabled' - it_behaves_like 'writes a warning to the log for a failed alert status update' it_behaves_like 'does not create an alert management alert' it_behaves_like 'does not process incident issues' @@ -97,7 +63,6 @@ RSpec.shared_examples 'processes recovery alert' do it_behaves_like 'creates expected system notes for alert', :recovery_alert, :resolve_alert it_behaves_like 'sends alert notification emails if enabled' it_behaves_like 'closes related incident if enabled' - it_behaves_like 'writes a warning to the log for a failed alert status update' it_behaves_like 'does not create an alert management alert' it_behaves_like 'does not process incident issues' diff --git a/spec/support/shared_examples/services/alert_management/alert_processing/incident_creation_shared_examples.rb b/spec/support/shared_examples/services/alert_management/alert_processing/incident_creation_shared_examples.rb index 98834f01ce2..6becc3dc071 100644 --- a/spec/support/shared_examples/services/alert_management/alert_processing/incident_creation_shared_examples.rb +++ b/spec/support/shared_examples/services/alert_management/alert_processing/incident_creation_shared_examples.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# Expects usage of 'incident settings enabled' context. +# Expects usage of 'incident management settings enabled' context. # # This shared_example includes the following option: # - with_issue: includes a test for when the defined `alert` has an associated issue @@ -8,7 +8,7 @@ # This shared_example requires the following variables: # - `alert`, required if :with_issue is true RSpec.shared_examples 'processes incident issues if enabled' do |with_issue: false| - include_examples 'processes incident issues', with_issue + include_examples 'processes incident issues', with_issue: with_issue context 'with incident setting disabled' do let(:create_issue) { false } diff --git a/spec/support/shared_examples/services/alert_management/alert_processing/incident_resolution_shared_examples.rb b/spec/support/shared_examples/services/alert_management/alert_processing/incident_resolution_shared_examples.rb index 3add5485fca..1973577d742 100644 --- a/spec/support/shared_examples/services/alert_management/alert_processing/incident_resolution_shared_examples.rb +++ b/spec/support/shared_examples/services/alert_management/alert_processing/incident_resolution_shared_examples.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# Expects usage of 'incident settings enabled' context. +# Expects usage of 'incident management settings enabled' context. # # This shared_example requires the following variables: # - `alert`, alert for which related incidents should be closed diff --git a/spec/support/shared_examples/services/alert_management/alert_processing/notifications_shared_examples.rb b/spec/support/shared_examples/services/alert_management/alert_processing/notifications_shared_examples.rb index 5f30b58176b..92e7dee7533 100644 --- a/spec/support/shared_examples/services/alert_management/alert_processing/notifications_shared_examples.rb +++ b/spec/support/shared_examples/services/alert_management/alert_processing/notifications_shared_examples.rb @@ -1,11 +1,11 @@ # frozen_string_literal: true -# Expects usage of 'incident settings enabled' context. +# Expects usage of 'incident management settings enabled' context. # # This shared_example includes the following option: # - count: number of notifications expected to be sent RSpec.shared_examples 'sends alert notification emails if enabled' do |count: 1| - include_examples 'sends alert notification emails', count + include_examples 'sends alert notification emails', count: count context 'with email setting disabled' do let(:send_email) { false } diff --git a/spec/support/shared_examples/services/boards/lists_move_service_shared_examples.rb b/spec/support/shared_examples/services/boards/lists_move_service_shared_examples.rb index bf84b912610..97d0bae3552 100644 --- a/spec/support/shared_examples/services/boards/lists_move_service_shared_examples.rb +++ b/spec/support/shared_examples/services/boards/lists_move_service_shared_examples.rb @@ -1,95 +1,103 @@ # frozen_string_literal: true RSpec.shared_examples 'lists move service' do - let!(:planning) { create(:list, board: board, position: 0) } - let!(:development) { create(:list, board: board, position: 1) } - let!(:review) { create(:list, board: board, position: 2) } - let!(:staging) { create(:list, board: board, position: 3) } - let!(:closed) { create(:closed_list, board: board) } + shared_examples 'correct movement behavior' do + context 'when list type is set to label' do + it 'does not reorder lists when new position is nil' do + service = described_class.new(parent, user, position: nil) - context 'when list type is set to label' do - it 'keeps position of lists when new position is nil' do - service = described_class.new(parent, user, position: nil) + service.execute(planning) - service.execute(planning) + expect(ordered_lists).to eq([planning, development, review, staging]) + end - expect(current_list_positions).to eq [0, 1, 2, 3] - end - - it 'keeps position of lists when new position is equal to old position' do - service = described_class.new(parent, user, position: planning.position) + it 'does not reorder lists when new position is equal to old position' do + service = described_class.new(parent, user, position: planning.position) - service.execute(planning) + service.execute(planning) - expect(current_list_positions).to eq [0, 1, 2, 3] - end + expect(ordered_lists).to eq([planning, development, review, staging]) + end - it 'keeps position of lists when new position is negative' do - service = described_class.new(parent, user, position: -1) + it 'does not reorder lists when new position is negative' do + service = described_class.new(parent, user, position: -1) - service.execute(planning) + service.execute(planning) - expect(current_list_positions).to eq [0, 1, 2, 3] - end + expect(ordered_lists).to eq([planning, development, review, staging]) + end - it 'keeps position of lists when new position is equal to number of labels lists' do - service = described_class.new(parent, user, position: board.lists.label.size) + it 'does not reorder lists when new position is bigger then last position' do + service = described_class.new(parent, user, position: ordered_lists.last.position + 1) - service.execute(planning) + service.execute(planning) - expect(current_list_positions).to eq [0, 1, 2, 3] - end + expect(ordered_lists).to eq([planning, development, review, staging]) + end - it 'keeps position of lists when new position is greater than number of labels lists' do - service = described_class.new(parent, user, position: board.lists.label.size + 1) + it 'moves the list to the first position when new position is equal to first position' do + service = described_class.new(parent, user, position: 0) - service.execute(planning) + service.execute(staging) - expect(current_list_positions).to eq [0, 1, 2, 3] - end + expect(ordered_lists).to eq([staging, planning, development, review]) + end - it 'increments position of intermediate lists when new position is equal to first position' do - service = described_class.new(parent, user, position: 0) + it 'moves the list to the last position when new position is equal to last position' do + service = described_class.new(parent, user, position: board.lists.label.last.position) - service.execute(staging) + service.execute(planning) - expect(current_list_positions).to eq [1, 2, 3, 0] - end + expect(ordered_lists).to eq([development, review, staging, planning]) + end - it 'decrements position of intermediate lists when new position is equal to last position' do - service = described_class.new(parent, user, position: board.lists.label.last.position) + it 'moves the list to the correct position when new position is greater than old position (third list)' do + service = described_class.new(parent, user, position: review.position) - service.execute(planning) + service.execute(planning) - expect(current_list_positions).to eq [3, 0, 1, 2] - end + expect(ordered_lists).to eq([development, review, planning, staging]) + end - it 'decrements position of intermediate lists when new position is greater than old position' do - service = described_class.new(parent, user, position: 2) + it 'moves the list to the correct position when new position is lower than old position (second list)' do + service = described_class.new(parent, user, position: development.position) - service.execute(planning) + service.execute(staging) - expect(current_list_positions).to eq [2, 0, 1, 3] + expect(ordered_lists).to eq([planning, staging, development, review]) + end end - it 'increments position of intermediate lists when new position is lower than old position' do - service = described_class.new(parent, user, position: 1) + it 'keeps position of lists when list type is closed' do + service = described_class.new(parent, user, position: 2) - service.execute(staging) + service.execute(closed) - expect(current_list_positions).to eq [0, 2, 3, 1] + expect(ordered_lists).to eq([planning, development, review, staging]) end end - it 'keeps position of lists when list type is closed' do - service = described_class.new(parent, user, position: 2) + context 'with complete position sequence' do + let!(:planning) { create(:list, board: board, position: 0) } + let!(:development) { create(:list, board: board, position: 1) } + let!(:review) { create(:list, board: board, position: 2) } + let!(:staging) { create(:list, board: board, position: 3) } + let!(:closed) { create(:closed_list, board: board) } + + it_behaves_like 'correct movement behavior' + end - service.execute(closed) + context 'with corrupted position sequence' do + let!(:planning) { create(:list, board: board, position: 0) } + let!(:staging) { create(:list, board: board, position: 6) } + let!(:development) { create(:list, board: board, position: 1) } + let!(:review) { create(:list, board: board, position: 4) } + let!(:closed) { create(:closed_list, board: board) } - expect(current_list_positions).to eq [0, 1, 2, 3] + it_behaves_like 'correct movement behavior' end - def current_list_positions - [planning, development, review, staging].map { |list| list.reload.position } + def ordered_lists + board.lists.where.not(position: nil) end end diff --git a/spec/support/shared_examples/services/issuable_shared_examples.rb b/spec/support/shared_examples/services/issuable_shared_examples.rb index a50a386afe1..142d4ae8531 100644 --- a/spec/support/shared_examples/services/issuable_shared_examples.rb +++ b/spec/support/shared_examples/services/issuable_shared_examples.rb @@ -45,7 +45,7 @@ RSpec.shared_examples 'updating a single task' do end it 'creates system note about task status change' do - note1 = find_note('marked the task **Task 1** as completed') + note1 = find_note('marked the checklist item **Task 1** as completed') expect(note1).not_to be_nil @@ -61,7 +61,7 @@ RSpec.shared_examples 'updating a single task' do end it 'creates system note about task status change' do - note1 = find_note('marked the task **Task 2** as incomplete') + note1 = find_note('marked the checklist item **Task 2** as incomplete') expect(note1).not_to be_nil @@ -92,7 +92,7 @@ RSpec.shared_examples 'updating a single task' do end it 'creates system note about task status change' do - note1 = find_note('marked the task **Task 2** as incomplete') + note1 = find_note('marked the checklist item **Task 2** as incomplete') expect(note1).not_to be_nil diff --git a/spec/support/shared_examples/services/packages_shared_examples.rb b/spec/support/shared_examples/services/packages_shared_examples.rb index 6bc4f171d9c..704a4bbe0b8 100644 --- a/spec/support/shared_examples/services/packages_shared_examples.rb +++ b/spec/support/shared_examples/services/packages_shared_examples.rb @@ -81,6 +81,26 @@ RSpec.shared_examples 'returns packages' do |container_type, user_type| end end +RSpec.shared_examples 'returns package' do |container_type, user_type| + context "for #{user_type}" do + before do + send(container_type)&.send("add_#{user_type}", user) unless user_type == :no_type + end + + it 'returns success response' do + subject + + expect(response).to have_gitlab_http_status(:success) + end + + it 'returns a valid response schema' do + subject + + expect(response).to match_response_schema(single_package_schema) + end + end +end + RSpec.shared_examples 'returns packages with subgroups' do |container_type, user_type| context "with subgroups for #{user_type}" do before do diff --git a/spec/support/shared_examples/services/snowplow_tracking_shared_examples.rb b/spec/support/shared_examples/services/snowplow_tracking_shared_examples.rb new file mode 100644 index 00000000000..0687be6f429 --- /dev/null +++ b/spec/support/shared_examples/services/snowplow_tracking_shared_examples.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +shared_examples 'issue_edit snowplow tracking' do + let(:category) { Gitlab::UsageDataCounters::IssueActivityUniqueCounter::ISSUE_CATEGORY } + let(:action) { Gitlab::UsageDataCounters::IssueActivityUniqueCounter::ISSUE_ACTION } + let(:label) { Gitlab::UsageDataCounters::IssueActivityUniqueCounter::ISSUE_LABEL } + let(:namespace) { project.namespace } + let(:feature_flag_name) { :route_hll_to_snowplow_phase2 } + + it_behaves_like 'Snowplow event tracking' +end diff --git a/spec/support/shared_examples/services/timelogs/create_service_shared_examples.rb b/spec/support/shared_examples/services/timelogs/create_service_shared_examples.rb new file mode 100644 index 00000000000..53c42ec0e00 --- /dev/null +++ b/spec/support/shared_examples/services/timelogs/create_service_shared_examples.rb @@ -0,0 +1,89 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'issuable supports timelog creation service' do + shared_examples 'success_response' do + it 'sucessfully saves the timelog' do + is_expected.to be_success + + timelog = subject.payload[:timelog] + + expect(timelog).to be_persisted + expect(timelog.time_spent).to eq(time_spent) + expect(timelog.spent_at).to eq('Fri, 08 Jul 2022 00:00:00.000000000 UTC +00:00') + expect(timelog.summary).to eq(summary) + expect(timelog.issuable).to eq(issuable) + end + end + + context 'when the user does not have permission' do + let(:user) { create(:user) } + + it 'returns an error' do + is_expected.to be_error + + expect(subject.message).to eq( + "#{issuable.base_class_name} doesn't exist or you don't have permission to add timelog to it.") + expect(subject.http_status).to eq(404) + end + end + + context 'when the user has permissions' do + let(:user) { author } + + before do + users_container.add_reporter(user) + end + + context 'when the timelog save fails' do + before do + allow_next_instance_of(Timelog) do |timelog| + allow(timelog).to receive(:save).and_return(false) + end + end + + it 'returns an error' do + is_expected.to be_error + expect(subject.message).to eq('Failed to save timelog') + end + end + + context 'when the creation completes sucessfully' do + it_behaves_like 'success_response' + end + end +end + +RSpec.shared_examples 'issuable does not support timelog creation service' do + shared_examples 'error_response' do + it 'returns an error' do + is_expected.to be_error + + issuable_type = if issuable.nil? + 'Issuable' + else + issuable.base_class_name + end + + expect(subject.message).to eq( + "#{issuable_type} doesn't exist or you don't have permission to add timelog to it." + ) + expect(subject.http_status).to eq(404) + end + end + + context 'when the user does not have permission' do + let(:user) { create(:user) } + + it_behaves_like 'error_response' + end + + context 'when the user has permissions' do + let(:user) { author } + + before do + users_container.add_reporter(user) + end + + it_behaves_like 'error_response' + end +end diff --git a/spec/support/shared_examples/services/work_items/create_task_shared_examples.rb b/spec/support/shared_examples/services/work_items/create_task_shared_examples.rb new file mode 100644 index 00000000000..7771e7f0e21 --- /dev/null +++ b/spec/support/shared_examples/services/work_items/create_task_shared_examples.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'title with extra spaces' do + context 'when title has extra spaces' do + before do + params[:title] = " Awesome work item " + end + + it 'removes extra leading and trailing whitespaces from title' do + subject + + created_work_item = WorkItem.last + expect(created_work_item.title).to eq('Awesome work item') + end + end +end 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 1da21633504..3ba5f080a01 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 @@ -1,6 +1,6 @@ # frozen_string_literal: true -RSpec.shared_examples 'it runs batched background migration jobs' do |tracking_database| +RSpec.shared_examples 'it runs batched background migration jobs' do |tracking_database, table_name| include ExclusiveLeaseHelpers describe 'defining the job attributes' do @@ -136,8 +136,10 @@ RSpec.shared_examples 'it runs batched background migration jobs' do |tracking_d let(:job_interval) { 5.minutes } let(:lease_timeout) { 15.minutes } let(:lease_key) { described_class.name.demodulize.underscore } - let(:migration) { build(:batched_background_migration, :active, interval: job_interval) } let(:interval_variance) { described_class::INTERVAL_VARIANCE } + let(:migration) do + build(:batched_background_migration, :active, interval: job_interval, table_name: table_name) + end before do allow(Gitlab::Database::BackgroundMigration::BatchedMigration).to receive(:active_migration) @@ -233,7 +235,9 @@ RSpec.shared_examples 'it runs batched background migration jobs' do |tracking_d let(:migration_class) do Class.new(Gitlab::BackgroundMigration::BatchedMigrationJob) do - def perform(matching_status) + job_arguments :matching_status + + def perform each_sub_batch( operation_name: :update_all, batching_scope: -> (relation) { relation.where(status: matching_status) } @@ -249,7 +253,7 @@ RSpec.shared_examples 'it runs batched background migration jobs' do |tracking_d create( :batched_background_migration, :active, - table_name: table_name, + table_name: new_table_name, column_name: :id, max_value: migration_records, batch_size: batch_size, @@ -261,14 +265,14 @@ RSpec.shared_examples 'it runs batched background migration jobs' do |tracking_d end let(:base_model) { Gitlab::Database.database_base_models[tracking_database] } - let(:table_name) { 'example_data' } + let(:new_table_name) { '_test_example_data' } let(:batch_size) { 5 } let(:sub_batch_size) { 2 } let(:number_of_batches) { 10 } let(:migration_records) { batch_size * number_of_batches } let(:connection) { Gitlab::Database.database_base_models[tracking_database].connection } - let(:example_data) { define_batchable_model(table_name, connection: connection) } + let(:example_data) { define_batchable_model(new_table_name, connection: connection) } around do |example| Gitlab::Database::SharedModel.using_connection(connection) do @@ -283,16 +287,16 @@ RSpec.shared_examples 'it runs batched background migration jobs' do |tracking_d # - one record beyond the migration's range # - one record that doesn't match the migration job's batch condition connection.execute(<<~SQL) - CREATE TABLE #{table_name} ( + CREATE TABLE #{new_table_name} ( id integer primary key, some_column integer, status smallint); - INSERT INTO #{table_name} (id, some_column, status) + INSERT INTO #{new_table_name} (id, some_column, status) SELECT generate_series, generate_series, 1 FROM generate_series(1, #{migration_records + 1}); - UPDATE #{table_name} + UPDATE #{new_table_name} SET status = 0 WHERE some_column = #{migration_records - 5}; SQL @@ -362,6 +366,15 @@ RSpec.shared_examples 'it runs batched background migration jobs' do |tracking_d expect { migration_run }.to change { migration.reload.on_hold? }.from(false).to(true) end + + it 'puts migration on hold when the pending WAL count is above the limit' do + sql = Gitlab::Database::BackgroundMigration::HealthStatus::Indicators::WriteAheadLog::PENDING_WAL_COUNT_SQL + limit = Gitlab::Database::BackgroundMigration::HealthStatus::Indicators::WriteAheadLog::LIMIT + + expect(connection).to receive(:execute).with(sql).and_return([{ 'pending_wal_count' => limit + 1 }]) + + expect { migration_run }.to change { migration.reload.on_hold? }.from(false).to(true) + end end end end |