From edaa33dee2ff2f7ea3fac488d41558eb5f86d68c Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Thu, 20 Jan 2022 09:16:11 +0000 Subject: Add latest changes from gitlab-org/gitlab@14-7-stable-ee --- .../cross-database-modification-allowlist.yml | 32 +-- spec/support/db_cleaner.rb | 2 +- spec/support/flaky_tests.rb | 2 +- spec/support/gitlab_stubs/gitlab_ci.yml | 8 +- spec/support/helpers/cycle_analytics_helpers.rb | 2 +- spec/support/helpers/gitaly_setup.rb | 204 +++++++++++++++--- spec/support/helpers/login_helpers.rb | 2 +- spec/support/helpers/stub_gitlab_calls.rb | 7 +- spec/support/helpers/stub_object_storage.rb | 6 + spec/support/helpers/test_env.rb | 132 +++--------- spec/support/helpers/usage_data_helpers.rb | 4 + spec/support/import_export/export_file_helper.rb | 2 +- spec/support/praefect.rb | 4 +- .../shared_contexts/navbar_structure_context.rb | 1 + .../policies/group_policy_shared_context.rb | 39 ++-- .../policies/project_policy_shared_context.rb | 2 +- .../access_tokens_controller_shared_examples.rb | 124 ----------- .../create_notes_rate_limit_shared_examples.rb | 43 +--- .../rate_limited_endpoint_shared_examples.rb | 57 +++++ .../features/access_tokens_shared_examples.rb | 165 +++++++++++++++ .../features/packages_shared_examples.rb | 4 +- .../sidebar/sidebar_labels_shared_examples.rb | 127 +++++++++++ .../features/sidebar_shared_examples.rb | 4 + .../finders/snippet_visibility_shared_examples.rb | 2 +- .../graphql/mutation_shared_examples.rb | 2 +- .../issues/permission_check_shared_examples.rb | 44 +++- .../permission_check_shared_examples.rb | 50 ++++- .../background_migration_job_shared_examples.rb | 10 +- .../multi_store_feature_flags_shared_examples.rb | 43 ---- .../lib/gitlab/unique_ip_check_shared_examples.rb | 16 +- .../loose_foreign_keys/have_loose_foreign_key.rb | 10 +- .../metrics/sampler_shared_examples.rb | 84 +++++++- .../models/application_setting_shared_examples.rb | 4 +- .../escalatable_shared_examples.rb | 33 +++ .../slack_mattermost_notifier_shared_examples.rb | 231 +++++++++++++-------- .../packages/destructible_shared_examples.rb | 18 ++ .../concerns/ttl_expirable_shared_examples.rb | 13 +- .../models/member_shared_examples.rb | 4 +- .../debian/distribution_shared_examples.rb | 21 +- .../update_project_statistics_shared_examples.rb | 6 +- .../namespaces/traversal_scope_examples.rb | 25 +++ .../access_tokens_controller_shared_examples.rb | 138 ++++++++++++ .../api/debian_packages_shared_examples.rb | 1 - .../packages/package_details_shared_examples.rb | 24 +++ .../api/nuget_endpoints_shared_examples.rb | 1 + .../services/alert_management_shared_examples.rb | 6 +- ...tainer_registry_auth_service_shared_examples.rb | 2 + .../services/incident_shared_examples.rb | 16 +- ...ad_with_all_expected_metrics_shared_examples.rb | 2 + ...d_without_restricted_metrics_shared_examples.rb | 2 + .../work_item_base_types_importer.rb | 4 +- .../cleanup_worker_shared_examples.rb | 14 +- spec/support/system_exit_detected.rb | 15 ++ 53 files changed, 1250 insertions(+), 564 deletions(-) delete mode 100644 spec/support/shared_examples/controllers/access_tokens_controller_shared_examples.rb create mode 100644 spec/support/shared_examples/controllers/rate_limited_endpoint_shared_examples.rb create mode 100644 spec/support/shared_examples/features/access_tokens_shared_examples.rb create mode 100644 spec/support/shared_examples/features/sidebar/sidebar_labels_shared_examples.rb delete mode 100644 spec/support/shared_examples/lib/gitlab/redis/multi_store_feature_flags_shared_examples.rb create mode 100644 spec/support/shared_examples/models/concerns/packages/destructible_shared_examples.rb create mode 100644 spec/support/shared_examples/requests/access_tokens_controller_shared_examples.rb create mode 100644 spec/support/system_exit_detected.rb (limited to 'spec/support') diff --git a/spec/support/database/cross-database-modification-allowlist.yml b/spec/support/database/cross-database-modification-allowlist.yml index d6e74349069..fe51488c706 100644 --- a/spec/support/database/cross-database-modification-allowlist.yml +++ b/spec/support/database/cross-database-modification-allowlist.yml @@ -1,31 +1 @@ -- "./ee/spec/mailers/notify_spec.rb" -- "./ee/spec/models/group_member_spec.rb" -- "./ee/spec/replicators/geo/terraform_state_version_replicator_spec.rb" -- "./ee/spec/services/ci/retry_build_service_spec.rb" -- "./spec/controllers/abuse_reports_controller_spec.rb" -- "./spec/controllers/omniauth_callbacks_controller_spec.rb" -- "./spec/controllers/projects/issues_controller_spec.rb" -- "./spec/features/issues/issue_detail_spec.rb" -- "./spec/features/projects/pipelines/pipeline_spec.rb" -- "./spec/features/signed_commits_spec.rb" -- "./spec/helpers/issuables_helper_spec.rb" -- "./spec/lib/gitlab/auth_spec.rb" -- "./spec/lib/gitlab/ci/pipeline/chain/create_spec.rb" -- "./spec/lib/gitlab/email/handler/create_issue_handler_spec.rb" -- "./spec/lib/gitlab/email/handler/create_merge_request_handler_spec.rb" -- "./spec/lib/gitlab/email/handler/create_note_handler_spec.rb" -- "./spec/lib/gitlab/email/handler/create_note_on_issuable_handler_spec.rb" -- "./spec/models/ci/build_trace_chunk_spec.rb" -- "./spec/models/ci/job_artifact_spec.rb" -- "./spec/models/ci/runner_spec.rb" -- "./spec/models/clusters/applications/runner_spec.rb" -- "./spec/models/design_management/version_spec.rb" -- "./spec/models/hooks/system_hook_spec.rb" -- "./spec/models/members/project_member_spec.rb" -- "./spec/models/user_spec.rb" -- "./spec/models/user_status_spec.rb" -- "./spec/requests/api/commits_spec.rb" -- "./spec/services/ci/retry_build_service_spec.rb" -- "./spec/services/projects/overwrite_project_service_spec.rb" -- "./spec/workers/merge_requests/create_pipeline_worker_spec.rb" -- "./spec/workers/repository_cleanup_worker_spec.rb" +[] diff --git a/spec/support/db_cleaner.rb b/spec/support/db_cleaner.rb index 316d645f99f..fb70f82ef87 100644 --- a/spec/support/db_cleaner.rb +++ b/spec/support/db_cleaner.rb @@ -67,7 +67,7 @@ module DbCleaner # Migrate each database individually with_reestablished_active_record_base do all_connection_classes.each do |connection_class| - ActiveRecord::Base.establish_connection(connection_class.connection_db_config) + ActiveRecord::Base.establish_connection(connection_class.connection_db_config) # rubocop: disable Database/EstablishConnection ActiveRecord::Tasks::DatabaseTasks.migrate end diff --git a/spec/support/flaky_tests.rb b/spec/support/flaky_tests.rb index 0c211af695d..5ce55c47aab 100644 --- a/spec/support/flaky_tests.rb +++ b/spec/support/flaky_tests.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true return unless ENV['CI'] -return unless ENV['SKIP_FLAKY_TESTS_AUTOMATICALLY'] == "true" +return if ENV['SKIP_FLAKY_TESTS_AUTOMATICALLY'] == "false" return if ENV['CI_MERGE_REQUEST_LABELS'].to_s.include?('pipeline:run-flaky-tests') require_relative '../../tooling/rspec_flaky/report' diff --git a/spec/support/gitlab_stubs/gitlab_ci.yml b/spec/support/gitlab_stubs/gitlab_ci.yml index f3755e52b2c..52ae36229a6 100644 --- a/spec/support/gitlab_stubs/gitlab_ci.yml +++ b/spec/support/gitlab_stubs/gitlab_ci.yml @@ -9,7 +9,7 @@ before_script: variables: DB_NAME: postgres -types: +stages: - test - deploy - notify @@ -36,7 +36,7 @@ staging: KEY1: value1 KEY2: value2 script: "cap deploy stating" - type: deploy + stage: deploy tags: - ruby - mysql @@ -46,7 +46,7 @@ staging: production: variables: DB_NAME: mysql - type: deploy + stage: deploy script: - cap deploy production - cap notify @@ -58,7 +58,7 @@ production: - /^deploy-.*$/ dockerhub: - type: notify + stage: notify script: "curl http://dockerhub/URL" tags: - ruby diff --git a/spec/support/helpers/cycle_analytics_helpers.rb b/spec/support/helpers/cycle_analytics_helpers.rb index 722d484609c..70b794f7d82 100644 --- a/spec/support/helpers/cycle_analytics_helpers.rb +++ b/spec/support/helpers/cycle_analytics_helpers.rb @@ -59,7 +59,7 @@ module CycleAnalyticsHelpers def save_value_stream(custom_value_stream_name) fill_in 'create-value-stream-name', with: custom_value_stream_name - page.find_button(s_('CreateValueStreamForm|Create Value Stream')).click + page.find_button(s_('CreateValueStreamForm|Create value stream')).click wait_for_requests end diff --git a/spec/support/helpers/gitaly_setup.rb b/spec/support/helpers/gitaly_setup.rb index 923051a2e04..905c439f4d9 100644 --- a/spec/support/helpers/gitaly_setup.rb +++ b/spec/support/helpers/gitaly_setup.rb @@ -9,8 +9,13 @@ require 'securerandom' require 'socket' require 'logger' +require 'bundler' module GitalySetup + extend self + + REPOS_STORAGE = 'default' + LOGGER = begin default_name = ENV['CI'] ? 'DEBUG' : 'WARN' level_name = ENV['GITLAB_TESTING_LOG_LEVEL']&.upcase @@ -52,11 +57,13 @@ module GitalySetup def env { - 'HOME' => expand_path('tmp/tests'), 'GEM_PATH' => Gem.path.join(':'), - 'BUNDLE_APP_CONFIG' => File.join(gemfile_dir, '.bundle'), 'BUNDLE_INSTALL_FLAGS' => nil, + 'BUNDLE_IGNORE_CONFIG' => '1', + 'BUNDLE_PATH' => bundle_path, 'BUNDLE_GEMFILE' => gemfile, + 'BUNDLE_JOBS' => '4', + 'BUNDLE_RETRY' => '3', 'RUBYOPT' => nil, # Git hooks can't run during tests as the internal API is not running. @@ -65,17 +72,20 @@ module GitalySetup } end - # rubocop:disable GitlabSecurity/SystemCommandInjection - def set_bundler_config - system('bundle config set --local jobs 4', chdir: gemfile_dir) - system('bundle config set --local retry 3', chdir: gemfile_dir) + def bundle_path + # Allow the user to override BUNDLE_PATH if they need to + return ENV['GITALY_TEST_BUNDLE_PATH'] if ENV['GITALY_TEST_BUNDLE_PATH'] if ENV['CI'] - bundle_path = expand_path('vendor/gitaly-ruby') - system('bundle', 'config', 'set', '--local', 'path', bundle_path, chdir: gemfile_dir) + expand_path('vendor/gitaly-ruby') + else + explicit_path = Bundler.configured_bundle_path.explicit_path + + return unless explicit_path + + expand_path(explicit_path) end end - # rubocop:enable GitlabSecurity/SystemCommandInjection def config_path(service) case service @@ -88,6 +98,10 @@ module GitalySetup end end + def repos_path(storage = REPOS_STORAGE) + Gitlab.config.repositories.storages[REPOS_STORAGE].legacy_disk_path + end + def service_binary(service) case service when :gitaly, :gitaly2 @@ -97,16 +111,20 @@ module GitalySetup end end + def run_command(cmd, env: {}) + system(env, *cmd, exception: true, chdir: tmp_tests_gitaly_dir) + end + def install_gitaly_gems - system(env, "make #{tmp_tests_gitaly_dir}/.ruby-bundle", chdir: tmp_tests_gitaly_dir) # rubocop:disable GitlabSecurity/SystemCommandInjection + run_command(%W[make #{tmp_tests_gitaly_dir}/.ruby-bundle], env: env) end def build_gitaly - system(env.merge({ 'GIT_VERSION' => nil }), 'make all git', chdir: tmp_tests_gitaly_dir) # rubocop:disable GitlabSecurity/SystemCommandInjection + run_command(%w[make all git], env: env.merge('GIT_VERSION' => nil)) end - def start_gitaly - start(:gitaly) + def start_gitaly(toml = nil) + start(:gitaly, toml) end def start_gitaly2 @@ -117,14 +135,20 @@ module GitalySetup start(:praefect) end - def start(service) + def start(service, toml = nil) + toml ||= config_path(service) args = ["#{tmp_tests_gitaly_bin_dir}/#{service_binary(service)}"] args.push("-config") if service == :praefect - args.push(config_path(service)) + args.push(toml) + + # Ensure user configuration does not affect Git + # Context: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/58776#note_547613780 + env = self.env.merge('HOME' => nil, 'XDG_CONFIG_HOME' => nil) + pid = spawn(env, *args, [:out, :err] => "log/#{service}-test.log") begin - try_connect!(service) + try_connect!(service, toml) rescue StandardError Process.kill('TERM', pid) raise @@ -161,29 +185,37 @@ module GitalySetup abort 'bundle check failed' unless system(env, 'bundle', 'check', out: out, chdir: gemfile_dir) end - def read_socket_path(service) + def connect_proc(toml) # This code needs to work in an environment where we cannot use bundler, # so we cannot easily use the toml-rb gem. This ad-hoc parser should be # good enough. - config_text = IO.read(config_path(service)) + config_text = IO.read(toml) config_text.lines.each do |line| - match_data = line.match(/^\s*socket_path\s*=\s*"([^"]*)"$/) + match_data = line.match(/^\s*(socket_path|listen_addr)\s*=\s*"([^"]*)"$/) - return match_data[1] if match_data + next unless match_data + + case match_data[1] + when 'socket_path' + return -> { UNIXSocket.new(match_data[2]) } + when 'listen_addr' + addr, port = match_data[2].split(':') + return -> { TCPSocket.new(addr, port.to_i) } + end end - raise "failed to find socket_path in #{config_path(service)}" + raise "failed to find socket_path or listen_addr in #{toml}" end - def try_connect!(service) + def try_connect!(service, toml) LOGGER.debug "Trying to connect to #{service}: " timeout = 20 delay = 0.1 - socket = read_socket_path(service) + connect = connect_proc(toml) Integer(timeout / delay).times do - UNIXSocket.new(socket) + connect.call LOGGER.debug " OK\n" return @@ -194,6 +226,128 @@ module GitalySetup LOGGER.warn " FAILED to connect to #{service}\n" - raise "could not connect to #{socket}" + raise "could not connect to #{service}" + end + + def gitaly_socket_path + Gitlab::GitalyClient.address(REPOS_STORAGE).delete_prefix('unix:') + end + + def gitaly_dir + socket_path = gitaly_socket_path + socket_path = File.expand_path(gitaly_socket_path) if expand_path_for_socket? + + File.dirname(socket_path) + end + + # Linux fails with "bind: invalid argument" if a UNIX socket path exceeds 108 characters: + # https://github.com/golang/go/issues/6895. We use absolute paths in CI to ensure + # that changes in the current working directory don't affect GRPC reconnections. + def expand_path_for_socket? + !!ENV['CI'] + end + + def setup_gitaly + unless ENV['CI'] + # In CI Gitaly is built in the setup-test-env job and saved in the + # artifacts. So when tests are started, there's no need to build Gitaly. + build_gitaly + end + + Gitlab::SetupHelper::Gitaly.create_configuration( + gitaly_dir, + { 'default' => repos_path }, + force: true, + options: { + prometheus_listen_addr: 'localhost:9236' + } + ) + Gitlab::SetupHelper::Gitaly.create_configuration( + gitaly_dir, + { 'default' => repos_path }, + force: true, + options: { + internal_socket_dir: File.join(gitaly_dir, "internal_gitaly2"), + gitaly_socket: "gitaly2.socket", + config_filename: "gitaly2.config.toml" + } + ) + Gitlab::SetupHelper::Praefect.create_configuration(gitaly_dir, { 'praefect' => repos_path }, force: true) + end + + def socket_path(service) + File.join(tmp_tests_gitaly_dir, "#{service}.socket") + end + + def praefect_socket_path + "unix:" + socket_path(:praefect) + end + + def stop(pid) + Process.kill('KILL', pid) + rescue Errno::ESRCH + # The process can already be gone if the test run was INTerrupted. + end + + def spawn_gitaly(toml = nil) + check_gitaly_config! + + pids = [] + + if toml + pids << start_gitaly(toml) + else + pids << start_gitaly + pids << start_gitaly2 + pids << start_praefect + end + + Kernel.at_exit do + # In CI, this function is called by scripts/gitaly-test-spawn, triggered + # in a before_script. Gitaly needs to remain running until the container + # is stopped. + next if ENV['CI'] + # In Workhorse tests (locally or in CI), this function is called by + # scripts/gitaly-test-spawn during `make test`. Gitaly needs to remain + # running until `make test` cleans it up. + next if ENV['GITALY_PID_FILE'] + + pids.each { |pid| stop(pid) } + end + rescue StandardError + raise gitaly_failure_message + end + + def gitaly_failure_message + message = "gitaly spawn failed\n\n" + + message += "- The `gitaly` binary does not exist: #{gitaly_binary}\n" unless File.exist?(gitaly_binary) + message += "- The `praefect` binary does not exist: #{praefect_binary}\n" unless File.exist?(praefect_binary) + message += "- The `git` binary does not exist: #{git_binary}\n" unless File.exist?(git_binary) + + message += "\nCheck log/gitaly-test.log for errors.\n" + + unless ci? + message += "\nIf binaries are missing, try running `make -C tmp/tests/gitaly build git.`\n" + message += "\nOtherwise, try running `rm -rf #{tmp_tests_gitaly_dir}`." + end + + message + end + + def git_binary + File.join(tmp_tests_gitaly_dir, "_build", "deps", "git", "install", "bin", "git") + end + + def gitaly_binary + File.join(tmp_tests_gitaly_dir, "_build", "bin", "gitaly") + end + + def praefect_binary + File.join(tmp_tests_gitaly_dir, "_build", "bin", "praefect") + end + + def git_binary_exists? + File.exist?(git_binary) end end diff --git a/spec/support/helpers/login_helpers.rb b/spec/support/helpers/login_helpers.rb index d9157fa7485..4e0e8dd96ee 100644 --- a/spec/support/helpers/login_helpers.rb +++ b/spec/support/helpers/login_helpers.rb @@ -95,7 +95,7 @@ module LoginHelpers visit new_user_session_path fill_in "user_login", with: user.email - fill_in "user_password", with: "12345678" + fill_in "user_password", with: Gitlab::Password.test_default check 'user_remember_me' if remember click_button "Sign in" diff --git a/spec/support/helpers/stub_gitlab_calls.rb b/spec/support/helpers/stub_gitlab_calls.rb index ae031f58bd4..c3459f7bc81 100644 --- a/spec/support/helpers/stub_gitlab_calls.rb +++ b/spec/support/helpers/stub_gitlab_calls.rb @@ -92,12 +92,7 @@ module StubGitlabCalls end def stub_commonmark_sourcepos_disabled - render_options = - if Feature.enabled?(:use_cmark_renderer, default_enabled: :yaml) - Banzai::Filter::MarkdownEngines::CommonMark::RENDER_OPTIONS_C - else - Banzai::Filter::MarkdownEngines::CommonMark::RENDER_OPTIONS_RUBY - end + render_options = Banzai::Filter::MarkdownEngines::CommonMark::RENDER_OPTIONS allow_any_instance_of(Banzai::Filter::MarkdownEngines::CommonMark) .to receive(:render_options) diff --git a/spec/support/helpers/stub_object_storage.rb b/spec/support/helpers/stub_object_storage.rb index 5e86b08aa45..d49a14f7f5b 100644 --- a/spec/support/helpers/stub_object_storage.rb +++ b/spec/support/helpers/stub_object_storage.rb @@ -91,6 +91,12 @@ module StubObjectStorage **params) end + def stub_ci_secure_file_object_storage(**params) + stub_object_storage_uploader(config: Gitlab.config.ci_secure_files.object_store, + uploader: Ci::SecureFileUploader, + **params) + end + def stub_terraform_state_object_storage(**params) stub_object_storage_uploader(config: Gitlab.config.terraform_state.object_store, uploader: Terraform::StateUploader, diff --git a/spec/support/helpers/test_env.rb b/spec/support/helpers/test_env.rb index d36bc4e3cb4..5c3ca92c4d0 100644 --- a/spec/support/helpers/test_env.rb +++ b/spec/support/helpers/test_env.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require 'parallel' +require_relative 'gitaly_setup' module TestEnv extend self @@ -93,7 +94,6 @@ module TestEnv }.freeze TMP_TEST_PATH = Rails.root.join('tmp', 'tests').freeze - REPOS_STORAGE = 'default' SECOND_STORAGE_PATH = Rails.root.join('tmp', 'tests', 'second_storage') SETUP_METHODS = %i[setup_gitaly setup_gitlab_shell setup_workhorse setup_factory_repo setup_forked_repo].freeze @@ -128,7 +128,7 @@ module TestEnv # Can be overriden def post_init - start_gitaly(gitaly_dir) + start_gitaly end # Clean /tmp/tests @@ -142,12 +142,15 @@ module TestEnv end FileUtils.mkdir_p( - Gitlab::GitalyClient::StorageSettings.allow_disk_access { TestEnv.repos_path } + Gitlab::GitalyClient::StorageSettings.allow_disk_access { GitalySetup.repos_path } ) FileUtils.mkdir_p(SECOND_STORAGE_PATH) FileUtils.mkdir_p(backup_path) FileUtils.mkdir_p(pages_path) FileUtils.mkdir_p(artifacts_path) + FileUtils.mkdir_p(lfs_path) + FileUtils.mkdir_p(terraform_state_path) + FileUtils.mkdir_p(packages_path) end def setup_gitlab_shell @@ -156,111 +159,28 @@ module TestEnv def setup_gitaly component_timed_setup('Gitaly', - install_dir: gitaly_dir, + install_dir: GitalySetup.gitaly_dir, version: Gitlab::GitalyClient.expected_server_version, - task: "gitlab:gitaly:test_install", - task_args: [gitaly_dir, repos_path, gitaly_url].compact) do - Gitlab::SetupHelper::Gitaly.create_configuration( - gitaly_dir, - { 'default' => repos_path }, - force: true, - options: { - prometheus_listen_addr: 'localhost:9236' - } - ) - Gitlab::SetupHelper::Gitaly.create_configuration( - gitaly_dir, - { 'default' => repos_path }, - force: true, - options: { - internal_socket_dir: File.join(gitaly_dir, "internal_gitaly2"), - gitaly_socket: "gitaly2.socket", - config_filename: "gitaly2.config.toml" - } - ) - Gitlab::SetupHelper::Praefect.create_configuration(gitaly_dir, { 'praefect' => repos_path }, force: true) - end - end - - def gitaly_socket_path - Gitlab::GitalyClient.address('default').sub(/\Aunix:/, '') - end - - def gitaly_dir - socket_path = gitaly_socket_path - socket_path = File.expand_path(gitaly_socket_path) if expand_path? - - File.dirname(socket_path) - end - - # Linux fails with "bind: invalid argument" if a UNIX socket path exceeds 108 characters: - # https://github.com/golang/go/issues/6895. We use absolute paths in CI to ensure - # that changes in the current working directory don't affect GRPC reconnections. - def expand_path? - !!ENV['CI'] + task: "gitlab:gitaly:clone", + fresh_install: ENV.key?('FORCE_GITALY_INSTALL'), + task_args: [GitalySetup.gitaly_dir, GitalySetup.repos_path, gitaly_url].compact) do + GitalySetup.setup_gitaly + end end - def start_gitaly(gitaly_dir) + def start_gitaly if ci? # Gitaly has been spawned outside this process already return end - spawn_script = Rails.root.join('scripts/gitaly-test-spawn').to_s - Bundler.with_original_env do - unless system(spawn_script) - message = 'gitaly spawn failed' - message += " (try `rm -rf #{gitaly_dir}` ?)" unless ci? - raise message - end - end - - gitaly_pid = Integer(File.read(TMP_TEST_PATH.join('gitaly.pid'))) - gitaly2_pid = Integer(File.read(TMP_TEST_PATH.join('gitaly2.pid'))) - praefect_pid = Integer(File.read(TMP_TEST_PATH.join('praefect.pid'))) - - Kernel.at_exit do - pids = [gitaly_pid, gitaly2_pid, praefect_pid] - pids.each { |pid| stop(pid) } - end - - wait('gitaly') - wait('praefect') - end - - def stop(pid) - Process.kill('KILL', pid) - rescue Errno::ESRCH - # The process can already be gone if the test run was INTerrupted. + GitalySetup.spawn_gitaly end def gitaly_url ENV.fetch('GITALY_REPO_URL', nil) end - def socket_path(service) - TMP_TEST_PATH.join('gitaly', "#{service}.socket").to_s - end - - def praefect_socket_path - "unix:" + socket_path(:praefect) - end - - def wait(service) - sleep_time = 10 - sleep_interval = 0.1 - socket = socket_path(service) - - Integer(sleep_time / sleep_interval).times do - Socket.unix(socket) - return - rescue StandardError - sleep sleep_interval - end - - raise "could not connect to #{service} at #{socket.inspect} after #{sleep_time} seconds" - end - # Feature specs are run through Workhorse def setup_workhorse # Always rebuild the config file @@ -376,8 +296,7 @@ module TestEnv def rm_storage_dir(storage, dir) Gitlab::GitalyClient::StorageSettings.allow_disk_access do - repos_path = Gitlab.config.repositories.storages[storage].legacy_disk_path - target_repo_refs_path = File.join(repos_path, dir) + target_repo_refs_path = File.join(GitalySetup.repos_path(storage), dir) FileUtils.remove_dir(target_repo_refs_path) end rescue Errno::ENOENT @@ -385,8 +304,7 @@ module TestEnv def storage_dir_exists?(storage, dir) Gitlab::GitalyClient::StorageSettings.allow_disk_access do - repos_path = Gitlab.config.repositories.storages[storage].legacy_disk_path - File.exist?(File.join(repos_path, dir)) + File.exist?(File.join(GitalySetup.repos_path(storage), dir)) end end @@ -399,7 +317,7 @@ module TestEnv end def repos_path - @repos_path ||= Gitlab.config.repositories.storages[REPOS_STORAGE].legacy_disk_path + @repos_path ||= GitalySetup.repos_path end def backup_path @@ -414,6 +332,18 @@ module TestEnv Gitlab.config.artifacts.storage_path end + def lfs_path + Gitlab.config.lfs.storage_path + end + + def terraform_state_path + Gitlab.config.terraform_state.storage_path + end + + def packages_path + Gitlab.config.packages.storage_path + end + # When no cached assets exist, manually hit the root path to create them # # Otherwise they'd be created by the first test, often timing out and @@ -512,7 +442,7 @@ module TestEnv end end - def component_timed_setup(component, install_dir:, version:, task:, task_args: []) + def component_timed_setup(component, install_dir:, version:, task:, fresh_install: true, task_args: []) start = Time.now ensure_component_dir_name_is_correct!(component, install_dir) @@ -522,7 +452,7 @@ module TestEnv if component_needs_update?(install_dir, version) # Cleanup the component entirely to ensure we start fresh - FileUtils.rm_rf(install_dir) + FileUtils.rm_rf(install_dir) if fresh_install if ENV['SKIP_RAILS_ENV_IN_RAKE'] # When we run `scripts/setup-test-env`, we take care of loading the necessary dependencies diff --git a/spec/support/helpers/usage_data_helpers.rb b/spec/support/helpers/usage_data_helpers.rb index 5865bafd382..776ea37ffdc 100644 --- a/spec/support/helpers/usage_data_helpers.rb +++ b/spec/support/helpers/usage_data_helpers.rb @@ -183,6 +183,10 @@ module UsageDataHelpers ) end + def stub_database_flavor_check(flavor = nil) + allow(ApplicationRecord.database).to receive(:flavor).and_return(flavor) + end + def clear_memoized_values(values) values.each { |v| described_class.clear_memoization(v) } end diff --git a/spec/support/import_export/export_file_helper.rb b/spec/support/import_export/export_file_helper.rb index f862a9bc1a4..3134e5c32a3 100644 --- a/spec/support/import_export/export_file_helper.rb +++ b/spec/support/import_export/export_file_helper.rb @@ -44,7 +44,7 @@ module ExportFileHelper create(:ci_trigger, project: project) key = create(:deploy_key) key.projects << project - create(:service, project: project) + create(:integration, project: project) create(:project_hook, project: project, token: 'token') create(:protected_branch, project: project) diff --git a/spec/support/praefect.rb b/spec/support/praefect.rb index 3218275c2aa..451b47cc83c 100644 --- a/spec/support/praefect.rb +++ b/spec/support/praefect.rb @@ -1,11 +1,11 @@ # frozen_string_literal: true -require_relative 'helpers/test_env' +require_relative 'helpers/gitaly_setup' RSpec.configure do |config| config.before(:each, :praefect) do allow(Gitlab.config.repositories.storages['default']).to receive(:[]).and_call_original allow(Gitlab.config.repositories.storages['default']).to receive(:[]).with('gitaly_address') - .and_return(TestEnv.praefect_socket_path) + .and_return(GitalySetup.praefect_socket_path) end end diff --git a/spec/support/shared_contexts/navbar_structure_context.rb b/spec/support/shared_contexts/navbar_structure_context.rb index 085f1f13c2c..27967850389 100644 --- a/spec/support/shared_contexts/navbar_structure_context.rb +++ b/spec/support/shared_contexts/navbar_structure_context.rb @@ -142,6 +142,7 @@ RSpec.shared_context 'group navbar structure' do nav_sub_items: [ _('General'), _('Integrations'), + _('Access Tokens'), _('Projects'), _('Repository'), _('CI/CD'), 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 ad6462dc367..0dfd76de79c 100644 --- a/spec/support/shared_contexts/policies/group_policy_shared_context.rb +++ b/spec/support/shared_contexts/policies/group_policy_shared_context.rb @@ -8,7 +8,14 @@ RSpec.shared_context 'GroupPolicy context' do let_it_be(:owner) { create(:user) } let_it_be(:admin) { create(:admin) } let_it_be(:non_group_member) { create(:user) } - let_it_be(:group, refind: true) { create(:group, :private, :owner_subgroup_creation_only) } + let_it_be(:group, refind: true) { create(:group, :private, :owner_subgroup_creation_only, :crm_enabled) } + + let(:public_permissions) do + %i[ + read_group read_counts + read_label read_issue_board_list read_milestone read_issue_board + ] + end let(:guest_permissions) do %i[ @@ -18,8 +25,6 @@ RSpec.shared_context 'GroupPolicy context' do ] end - let(:read_group_permissions) { %i[read_label read_issue_board_list read_milestone read_issue_board] } - let(:reporter_permissions) do %i[ admin_label @@ -28,6 +33,8 @@ RSpec.shared_context 'GroupPolicy context' do read_metrics_dashboard_annotation read_prometheus read_package_settings + read_crm_contact + read_crm_organization ] end @@ -48,22 +55,24 @@ RSpec.shared_context 'GroupPolicy context' do destroy_package create_projects read_cluster create_cluster update_cluster admin_cluster add_cluster - admin_group_runners ] end let(:owner_permissions) do - [ - :owner_access, - :admin_group, - :admin_namespace, - :admin_group_member, - :change_visibility_level, - :set_note_created_at, - :create_subgroup, - :read_statistics, - :update_default_branch_protection - ].compact + %i[ + owner_access + admin_group + admin_namespace + admin_group_member + change_visibility_level + set_note_created_at + create_subgroup + read_statistics + update_default_branch_protection + read_group_runners + admin_group_runners + register_group_runners + ] end let(:admin_permissions) { %i[read_confidential_issues] } 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 8a90f887381..c39252cef13 100644 --- a/spec/support/shared_contexts/policies/project_policy_shared_context.rb +++ b/spec/support/shared_contexts/policies/project_policy_shared_context.rb @@ -50,7 +50,7 @@ RSpec.shared_context 'ProjectPolicy context' do resolve_note update_build update_commit_status update_container_image update_deployment update_environment update_merge_request update_metrics_dashboard_annotation update_pipeline update_release destroy_release - read_resource_group update_resource_group + read_resource_group update_resource_group update_escalation_status ] end diff --git a/spec/support/shared_examples/controllers/access_tokens_controller_shared_examples.rb b/spec/support/shared_examples/controllers/access_tokens_controller_shared_examples.rb deleted file mode 100644 index 017e55309f7..00000000000 --- a/spec/support/shared_examples/controllers/access_tokens_controller_shared_examples.rb +++ /dev/null @@ -1,124 +0,0 @@ -# frozen_string_literal: true - -RSpec.shared_examples 'project access tokens available #index' do - let_it_be(:active_project_access_token) { create(:personal_access_token, user: bot_user) } - let_it_be(:inactive_project_access_token) { create(:personal_access_token, :revoked, user: bot_user) } - - it 'retrieves active project access tokens' do - subject - - expect(assigns(:active_project_access_tokens)).to contain_exactly(active_project_access_token) - end - - it 'retrieves inactive project access tokens' do - subject - - expect(assigns(:inactive_project_access_tokens)).to contain_exactly(inactive_project_access_token) - end - - it 'lists all available scopes' do - subject - - expect(assigns(:scopes)).to eq(Gitlab::Auth.resource_bot_scopes) - end - - it 'retrieves newly created personal access token value' do - token_value = 'random-value' - allow(PersonalAccessToken).to receive(:redis_getdel).with("#{user.id}:#{project.id}").and_return(token_value) - - subject - - expect(assigns(:new_project_access_token)).to eq(token_value) - end -end - -RSpec.shared_examples 'project access tokens available #create' do - def created_token - PersonalAccessToken.order(:created_at).last - end - - it 'returns success message' do - subject - - expect(controller).to set_flash[:notice].to match('Your new project access token has been created.') - end - - it 'creates project access token' do - access_level = access_token_params[:access_level] || Gitlab::Access::MAINTAINER - subject - - expect(created_token.name).to eq(access_token_params[:name]) - expect(created_token.scopes).to eq(access_token_params[:scopes]) - expect(created_token.expires_at).to eq(access_token_params[:expires_at]) - expect(project.project_member(created_token.user).access_level).to eq(access_level) - end - - it 'creates project bot user' do - subject - - expect(created_token.user).to be_project_bot - end - - it 'stores newly created token redis store' do - expect(PersonalAccessToken).to receive(:redis_store!) - - subject - end - - it { expect { subject }.to change { User.count }.by(1) } - it { expect { subject }.to change { PersonalAccessToken.count }.by(1) } - - context 'when unsuccessful' do - before do - allow_next_instance_of(ResourceAccessTokens::CreateService) do |service| - allow(service).to receive(:execute).and_return ServiceResponse.error(message: 'Failed!') - end - end - - it 'does not create the token' do - expect { subject }.not_to change { PersonalAccessToken.count } - end - - it 'does not add the project bot as a member' do - expect { subject }.not_to change { Member.count } - end - - it 'does not create the project bot user' do - expect { subject }.not_to change { User.count } - end - - it 'shows a failure alert' do - subject - - expect(controller).to set_flash[:alert].to match("Failed to create new project access token: Failed!") - end - end -end - -RSpec.shared_examples 'project access tokens available #revoke' do - it 'calls delete user worker' do - expect(DeleteUserWorker).to receive(:perform_async).with(user.id, bot_user.id, skip_authorization: true) - - subject - end - - it 'removes membership of bot user' do - subject - - expect(project.reload.bots).not_to include(bot_user) - end - - it 'converts issuables of the bot user to ghost user' do - issue = create(:issue, author: bot_user) - - subject - - expect(issue.reload.author.ghost?).to be true - end - - it 'deletes project bot user' do - subject - - expect(User.exists?(bot_user.id)).to be_falsy - end -end diff --git a/spec/support/shared_examples/controllers/create_notes_rate_limit_shared_examples.rb b/spec/support/shared_examples/controllers/create_notes_rate_limit_shared_examples.rb index 8affe4ac8f5..08d0be8c7ac 100644 --- a/spec/support/shared_examples/controllers/create_notes_rate_limit_shared_examples.rb +++ b/spec/support/shared_examples/controllers/create_notes_rate_limit_shared_examples.rb @@ -3,44 +3,19 @@ # Requires a context containing: # - user # - params -# - request_full_path -RSpec.shared_examples 'request exceeding rate limit' do - context 'with rate limiter', :freeze_time, :clean_gitlab_redis_rate_limiting do - before do - stub_application_setting(notes_create_limit: 2) - 2.times { post :create, params: params } - end +RSpec.shared_examples 'create notes request exceeding rate limit' do + include_examples 'rate limited endpoint', rate_limit_key: :notes_create - it 'prevents from creating more notes' do - expect { post :create, params: params } - .to change { Note.count }.by(0) + it 'allows user in allow-list to create notes, even if the case is different', :freeze_time, :clean_gitlab_redis_rate_limiting do + allow(Gitlab::ApplicationRateLimiter).to receive(:threshold).with(:notes_create).and_return(1) - expect(response).to have_gitlab_http_status(:too_many_requests) - expect(response.body).to eq(_('This endpoint has been requested too many times. Try again later.')) - end + current_user.update_attribute(:username, current_user.username.titleize) + stub_application_setting(notes_create_limit_allowlist: [current_user.username.downcase]) - it 'logs the event in auth.log' do - attributes = { - message: 'Application_Rate_Limiter_Request', - env: :notes_create_request_limit, - remote_ip: '0.0.0.0', - request_method: 'POST', - path: request_full_path, - user_id: user.id, - username: user.username - } + request + request - expect(Gitlab::AuthLogger).to receive(:error).with(attributes).once - post :create, params: params - end - - it 'allows user in allow-list to create notes, even if the case is different' do - user.update_attribute(:username, user.username.titleize) - stub_application_setting(notes_create_limit_allowlist: ["#{user.username.downcase}"]) - - post :create, params: params - expect(response).to have_gitlab_http_status(:found) - end + expect(response).to have_gitlab_http_status(:found) end end diff --git a/spec/support/shared_examples/controllers/rate_limited_endpoint_shared_examples.rb b/spec/support/shared_examples/controllers/rate_limited_endpoint_shared_examples.rb new file mode 100644 index 00000000000..bb2a4159071 --- /dev/null +++ b/spec/support/shared_examples/controllers/rate_limited_endpoint_shared_examples.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true +# +# Requires a context containing: +# - request (use method definition to avoid memoizing!) +# - current_user +# - error_message # optional + +RSpec.shared_examples 'rate limited endpoint' do |rate_limit_key:| + context 'when rate limiter enabled', :freeze_time, :clean_gitlab_redis_rate_limiting do + let(:expected_logger_attributes) do + { + message: 'Application_Rate_Limiter_Request', + env: :"#{rate_limit_key}_request_limit", + remote_ip: kind_of(String), + request_method: kind_of(String), + path: kind_of(String), + user_id: current_user.id, + username: current_user.username + } + end + + let(:error_message) { _('This endpoint has been requested too many times. Try again later.') } + + before do + allow(Gitlab::ApplicationRateLimiter).to receive(:threshold).with(rate_limit_key).and_return(1) + end + + it 'logs request and declines it when endpoint called more than the threshold' do |example| + expect(Gitlab::AuthLogger).to receive(:error).with(expected_logger_attributes).once + + request + request + + expect(response).to have_gitlab_http_status(:too_many_requests) + + if example.metadata[:type] == :controller + expect(response.body).to eq(error_message) + else # it is API spec + expect(response.body).to eq({ message: { error: error_message } }.to_json) + end + end + end + + context 'when rate limiter is disabled' do + before do + allow(Gitlab::ApplicationRateLimiter).to receive(:threshold).with(rate_limit_key).and_return(0) + end + + it 'does not log request and does not block the request' do + expect(Gitlab::AuthLogger).not_to receive(:error) + + request + + expect(response).not_to have_gitlab_http_status(:too_many_requests) + 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 new file mode 100644 index 00000000000..ae246a87bb6 --- /dev/null +++ b/spec/support/shared_examples/features/access_tokens_shared_examples.rb @@ -0,0 +1,165 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'resource access tokens missing access rights' do + it 'does not show access token page' do + visit resource_settings_access_tokens_path + + expect(page).to have_content("Page Not Found") + end +end + +RSpec.shared_examples 'resource access tokens creation' do |resource_type| + def active_resource_access_tokens + find('.table.active-tokens') + end + + def created_resource_access_token + find('#created-personal-access-token').value + end + + it 'allows creation of an access token', :aggregate_failures do + name = 'My access token' + + visit resource_settings_access_tokens_path + fill_in 'Token name', with: name + + # Set date to 1st of next month + find_field('Expiration date').click + find('.pika-next').click + click_on '1' + + # Scopes + check 'api' + check 'read_api' + + click_on "Create #{resource_type} access token" + + expect(active_resource_access_tokens).to have_text(name) + expect(active_resource_access_tokens).to have_text('in') + expect(active_resource_access_tokens).to have_text('api') + expect(active_resource_access_tokens).to have_text('read_api') + expect(active_resource_access_tokens).to have_text('Maintainer') + expect(created_resource_access_token).not_to be_empty + end +end + +RSpec.shared_examples 'resource access tokens creation disallowed' do |error_message| + before do + group.namespace_settings.update_column(:resource_access_token_creation_allowed, false) + end + + it 'does not show access token creation form' do + visit resource_settings_access_tokens_path + + expect(page).not_to have_selector('#new_resource_access_token') + end + + it 'shows access token creation disabled text' do + visit resource_settings_access_tokens_path + + expect(page).to have_text(error_message) + end + + context 'group settings link' do + context 'when user is not a group owner' do + before do + group.add_developer(user) + end + + it 'does not show group settings link' do + visit resource_settings_access_tokens_path + + expect(page).not_to have_link('group settings', href: edit_group_path(group)) + end + end + + context 'with nested groups' do + let(:parent_group) { create(:group) } + let(:group) { create(:group, parent: parent_group) } + + context 'when user is not a top level group owner' do + before do + group.add_owner(user) + end + + it 'does not show group settings link' do + visit resource_settings_access_tokens_path + + expect(page).not_to have_link('group settings', href: edit_group_path(group)) + end + end + end + + context 'when user is a group owner' do + before do + group.add_owner(user) + end + + it 'shows group settings link' do + visit resource_settings_access_tokens_path + + expect(page).to have_link('group settings', href: edit_group_path(group)) + end + end + end +end + +RSpec.shared_examples 'active resource access tokens' do + def active_resource_access_tokens + find('.table.active-tokens') + end + + it 'shows active access tokens' do + visit resource_settings_access_tokens_path + + expect(active_resource_access_tokens).to have_text(resource_access_token.name) + end + + context 'when User#time_display_relative is false' do + before do + user.update!(time_display_relative: false) + end + + it 'shows absolute times for expires_at' do + visit resource_settings_access_tokens_path + + expect(active_resource_access_tokens).to have_text(PersonalAccessToken.last.expires_at.strftime('%b %-d')) + end + end +end + +RSpec.shared_examples 'inactive resource access tokens' do |no_active_tokens_text| + def no_resource_access_tokens_message + find('.settings-message') + end + + it 'allows revocation of an active token' do + visit resource_settings_access_tokens_path + accept_confirm { click_on 'Revoke' } + + expect(page).to have_selector('.settings-message') + expect(no_resource_access_tokens_message).to have_text(no_active_tokens_text) + end + + it 'removes expired tokens from active section' do + resource_access_token.update!(expires_at: 5.days.ago) + visit resource_settings_access_tokens_path + + expect(page).to have_selector('.settings-message') + expect(no_resource_access_tokens_message).to have_text(no_active_tokens_text) + end + + context 'when resource access token creation is not allowed' do + before do + group.namespace_settings.update_column(:resource_access_token_creation_allowed, false) + end + + it 'allows revocation of an active token' do + visit resource_settings_access_tokens_path + accept_confirm { click_on 'Revoke' } + + expect(page).to have_selector('.settings-message') + expect(no_resource_access_tokens_message).to have_text(no_active_tokens_text) + end + end +end diff --git a/spec/support/shared_examples/features/packages_shared_examples.rb b/spec/support/shared_examples/features/packages_shared_examples.rb index d14b4638ca5..ded30f32314 100644 --- a/spec/support/shared_examples/features/packages_shared_examples.rb +++ b/spec/support/shared_examples/features/packages_shared_examples.rb @@ -19,14 +19,12 @@ RSpec.shared_examples 'packages list' do |check_project_name: false| end RSpec.shared_examples 'package details link' do |property| - let(:package) { packages.first } - it 'navigates to the correct url' do page.within(packages_table_selector) do click_link package.name end - expect(page).to have_current_path(project_package_path(package.project, package)) + expect(page).to have_current_path(package_details_path) expect(page).to have_css('.packages-app h2[data-testid="title"]', text: package.name) diff --git a/spec/support/shared_examples/features/sidebar/sidebar_labels_shared_examples.rb b/spec/support/shared_examples/features/sidebar/sidebar_labels_shared_examples.rb new file mode 100644 index 00000000000..a9dac7a391f --- /dev/null +++ b/spec/support/shared_examples/features/sidebar/sidebar_labels_shared_examples.rb @@ -0,0 +1,127 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'labels sidebar widget' do + context 'editing labels' do + let_it_be(:development) { create(:group_label, group: group, name: 'Development') } + let_it_be(:stretch) { create(:label, project: project, name: 'Stretch') } + let_it_be(:xss_label) { create(:label, project: project, title: '<script>alert("xss");</script>') } + + let(:labels_widget) { find('[data-testid="sidebar-labels"]') } + + before do + page.within(labels_widget) do + click_on 'Edit' + end + + wait_for_all_requests + end + + it 'shows labels list in the dropdown' do + expect(labels_widget.find('.gl-new-dropdown-contents')).to have_selector('li.gl-new-dropdown-item', count: 4) + end + + it 'adds a label' do + within(labels_widget) do + adds_label(stretch) + + page.within('[data-testid="value-wrapper"]') do + expect(page).to have_content(stretch.name) + end + end + end + + it 'removes a label' do + within(labels_widget) do + adds_label(stretch) + page.within('[data-testid="value-wrapper"]') do + expect(page).to have_content(stretch.name) + end + + click_on 'Remove label' + + wait_for_requests + + page.within('[data-testid="value-wrapper"]') do + expect(page).not_to have_content(stretch.name) + end + end + end + + it 'adds first label by pressing enter when search' do + within(labels_widget) do + page.within('[data-testid="value-wrapper"]') do + expect(page).not_to have_content(development.name) + end + + fill_in 'Search', with: 'Devel' + sleep 1 + expect(page.all(:css, '[data-testid="dropdown-content"] .gl-new-dropdown-item').length).to eq(1) + + find_field('Search').native.send_keys(:enter) + click_button 'Close' + wait_for_requests + + page.within('[data-testid="value-wrapper"]') do + expect(page).to have_content(development.name) + end + end + end + + it 'escapes XSS when viewing issuable labels' do + page.within(labels_widget) do + expect(page).to have_content '' + end + end + + it 'shows option to create a label' do + page.within(labels_widget) do + expect(page).to have_content 'Create' + end + end + + context 'creating a label', :js do + before do + page.within(labels_widget) do + page.find('[data-testid="create-label-button"]').click + end + end + + it 'shows dropdown switches to "create label" section' do + page.within(labels_widget) do + expect(page.find('[data-testid="dropdown-header"]')).to have_content 'Create' + end + end + + it 'creates new label' do + page.within(labels_widget) do + fill_in 'Name new label', with: 'wontfix' + page.find('.suggest-colors a', match: :first).click + page.find('button', text: 'Create').click + wait_for_requests + + expect(page).to have_content 'wontfix' + end + end + + it 'shows error message if label title is taken' do + page.within(labels_widget) do + fill_in 'Name new label', with: development.title + page.find('.suggest-colors a', match: :first).click + page.find('button', text: 'Create').click + wait_for_requests + + page.within('.dropdown-input') do + expect(page.find('.gl-alert')).to have_content 'Title' + end + end + end + end + end + + def adds_label(label) + click_button label.name + click_button 'Close' + + wait_for_requests + end +end diff --git a/spec/support/shared_examples/features/sidebar_shared_examples.rb b/spec/support/shared_examples/features/sidebar_shared_examples.rb index 615f568420e..11d216ff4b6 100644 --- a/spec/support/shared_examples/features/sidebar_shared_examples.rb +++ b/spec/support/shared_examples/features/sidebar_shared_examples.rb @@ -50,6 +50,10 @@ RSpec.shared_examples 'issue boards sidebar' do it_behaves_like 'date sidebar widget' end + context 'editing issue labels', :js do + it_behaves_like 'labels sidebar widget' + end + context 'in notifications subscription' do it 'displays notifications toggle', :aggregate_failures do page.within('[data-testid="sidebar-notifications"]') do diff --git a/spec/support/shared_examples/finders/snippet_visibility_shared_examples.rb b/spec/support/shared_examples/finders/snippet_visibility_shared_examples.rb index a2c34cdd4a1..601a53ed913 100644 --- a/spec/support/shared_examples/finders/snippet_visibility_shared_examples.rb +++ b/spec/support/shared_examples/finders/snippet_visibility_shared_examples.rb @@ -233,7 +233,7 @@ RSpec.shared_examples 'snippet visibility' do project.update!(visibility_level: Gitlab::VisibilityLevel.level_value(project_visibility.to_s), snippets_access_level: feature_visibility) if user_type == :external - member = project.project_member(external) + member = project.member(external) if project.private? project.add_developer(external) unless member diff --git a/spec/support/shared_examples/graphql/mutation_shared_examples.rb b/spec/support/shared_examples/graphql/mutation_shared_examples.rb index 51d52cbb901..dc590e23ace 100644 --- a/spec/support/shared_examples/graphql/mutation_shared_examples.rb +++ b/spec/support/shared_examples/graphql/mutation_shared_examples.rb @@ -8,7 +8,7 @@ # There must be a method or let called `mutation` defined that executes # the mutation. RSpec.shared_examples 'a mutation that returns top-level errors' do |errors: []| - let(:match_errors) { eq(errors) } + let(:match_errors) { match_array(errors) } it do post_graphql_mutation(mutation, current_user: current_user) diff --git a/spec/support/shared_examples/graphql/mutations/issues/permission_check_shared_examples.rb b/spec/support/shared_examples/graphql/mutations/issues/permission_check_shared_examples.rb index 34c58f524cd..05fee45427a 100644 --- a/spec/support/shared_examples/graphql/mutations/issues/permission_check_shared_examples.rb +++ b/spec/support/shared_examples/graphql/mutations/issues/permission_check_shared_examples.rb @@ -1,12 +1,34 @@ # frozen_string_literal: true RSpec.shared_examples 'permission level for issue mutation is correctly verified' do |raises_for_all_errors = false| - before do - issue.assignees = [] - issue.author = user + let_it_be(:other_user_author) { create(:user) } + + def issue_attributes(issue) + issue.attributes.except( + # Description and title can be updated by authors and assignees of the issues + 'description', + 'title', + # Those fields are calculated or expected to be modified during the mutations + 'author_id', + 'updated_at', + 'updated_by_id', + 'last_edited_at', + 'last_edited_by_id', + 'lock_version', + # There were spec failures due to nano-second comparisons + # this property isn't changed by any mutation so we don't have to verify it + 'created_at' + ) end - shared_examples_for 'when the user does not have access to the resource' do |raise_for_assigned| + let(:expected) { issue_attributes(issue) } + + shared_examples_for 'when the user does not have access to the resource' do |raise_for_assigned_and_author| + before do + issue.assignees = [] + issue.update!(author: other_user_author) + end + it 'raises an error' do expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) end @@ -17,21 +39,25 @@ RSpec.shared_examples 'permission level for issue mutation is correctly verified end it 'does not modify issue' do - if raises_for_all_errors || raise_for_assigned + if raises_for_all_errors || raise_for_assigned_and_author expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) else - expect(subject[:issue]).to eq issue + expect(issue_attributes(subject[:issue])).to eq expected end end end context 'even if author of the issue' do before do - issue.author = user + issue.update!(author: user) end - it 'raises an error' do - expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) + it 'does not modify issue' do + if raises_for_all_errors || raise_for_assigned_and_author + expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) + else + expect(issue_attributes(subject[:issue])).to eq expected + end end end end diff --git a/spec/support/shared_examples/graphql/mutations/merge_requests/permission_check_shared_examples.rb b/spec/support/shared_examples/graphql/mutations/merge_requests/permission_check_shared_examples.rb index 1ddbad1cea7..b0ac742079a 100644 --- a/spec/support/shared_examples/graphql/mutations/merge_requests/permission_check_shared_examples.rb +++ b/spec/support/shared_examples/graphql/mutations/merge_requests/permission_check_shared_examples.rb @@ -1,13 +1,39 @@ # frozen_string_literal: true RSpec.shared_examples 'permission level for merge request mutation is correctly verified' do - before do - merge_request.assignees = [] - merge_request.reviewers = [] - merge_request.author = nil + let(:other_user_author) { create(:user) } + + def mr_attributes(mr) + mr.attributes.except( + # Authors and assignees can edit title, description, target branch and draft status + 'title', + 'description', + 'target_branch', + 'draft', + # Those fields are calculated or expected to be modified during the mutations + 'author_id', + 'latest_merge_request_diff_id', + 'last_edited_at', + 'last_edited_by_id', + 'lock_version', + 'updated_at', + 'updated_by_id', + 'merge_status', + # There were spec failures due to nano-second comparisons + # this property isn't changed by any mutation so we don't have to verify it + 'created_at' + ) end - shared_examples_for 'when the user does not have access to the resource' do |raise_for_assigned| + let(:expected) { mr_attributes(merge_request) } + + shared_examples_for 'when the user does not have access to the resource' do |raise_for_assigned_and_author| + before do + merge_request.assignees = [] + merge_request.reviewers = [] + merge_request.update!(author: other_user_author) + end + it 'raises an error' do expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) end @@ -18,12 +44,12 @@ RSpec.shared_examples 'permission level for merge request mutation is correctly end it 'does not modify merge request' do - if raise_for_assigned + if raise_for_assigned_and_author expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) else # In some cases we simply do nothing instead of raising # https://gitlab.com/gitlab-org/gitlab/-/issues/196241 - expect(subject[:merge_request]).to eq merge_request + expect(mr_attributes(subject[:merge_request])).to eq expected end end end @@ -40,11 +66,17 @@ RSpec.shared_examples 'permission level for merge request mutation is correctly context 'even if author of the merge request' do before do - merge_request.author = user + merge_request.update!(author: user) end it 'raises an error' do - expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) + if raise_for_assigned_and_author + expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) + else + # In some cases we simply do nothing instead of raising + # https://gitlab.com/gitlab-org/gitlab/-/issues/196241 + expect(mr_attributes(subject[:merge_request])).to eq expected + end end end end diff --git a/spec/support/shared_examples/lib/gitlab/database/background_migration_job_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/database/background_migration_job_shared_examples.rb index 7888ade56eb..213f084be17 100644 --- a/spec/support/shared_examples/lib/gitlab/database/background_migration_job_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/database/background_migration_job_shared_examples.rb @@ -22,19 +22,19 @@ RSpec.shared_examples 'marks background migration job records' do end end -RSpec.shared_examples 'finalized background migration' do +RSpec.shared_examples 'finalized background migration' do |worker_class| it 'processed the scheduled sidekiq queue' do queued = Sidekiq::ScheduledSet .new .select do |scheduled| - scheduled.klass == 'BackgroundMigrationWorker' && + scheduled.klass == worker_class.name && scheduled.args.first == job_class_name end expect(queued.size).to eq(0) end it 'processed the async sidekiq queue' do - queued = Sidekiq::Queue.new('BackgroundMigrationWorker') + queued = Sidekiq::Queue.new(worker_class.name) .select { |scheduled| scheduled.klass == job_class_name } expect(queued.size).to eq(0) end @@ -42,8 +42,8 @@ RSpec.shared_examples 'finalized background migration' do include_examples 'removed tracked jobs', 'pending' end -RSpec.shared_examples 'finalized tracked background migration' do - include_examples 'finalized background migration' +RSpec.shared_examples 'finalized tracked background migration' do |worker_class| + include_examples 'finalized background migration', worker_class include_examples 'removed tracked jobs', 'succeeded' end diff --git a/spec/support/shared_examples/lib/gitlab/redis/multi_store_feature_flags_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/redis/multi_store_feature_flags_shared_examples.rb deleted file mode 100644 index 046c70bf779..00000000000 --- a/spec/support/shared_examples/lib/gitlab/redis/multi_store_feature_flags_shared_examples.rb +++ /dev/null @@ -1,43 +0,0 @@ -# frozen_string_literal: true - -RSpec.shared_examples 'multi store feature flags' do |use_primary_and_secondary_stores, use_primary_store_as_default| - context "with feature flag :#{use_primary_and_secondary_stores} is enabled" do - before do - stub_feature_flags(use_primary_and_secondary_stores => true) - end - - it 'multi store is enabled' do - expect(subject.use_primary_and_secondary_stores?).to be true - end - end - - context "with feature flag :#{use_primary_and_secondary_stores} is disabled" do - before do - stub_feature_flags(use_primary_and_secondary_stores => false) - end - - it 'multi store is disabled' do - expect(subject.use_primary_and_secondary_stores?).to be false - end - end - - context "with feature flag :#{use_primary_store_as_default} is enabled" do - before do - stub_feature_flags(use_primary_store_as_default => true) - end - - it 'primary store is enabled' do - expect(subject.use_primary_store_as_default?).to be true - end - end - - context "with feature flag :#{use_primary_store_as_default} is disabled" do - before do - stub_feature_flags(use_primary_store_as_default => false) - end - - it 'primary store is disabled' do - expect(subject.use_primary_store_as_default?).to be false - end - end -end diff --git a/spec/support/shared_examples/lib/gitlab/unique_ip_check_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/unique_ip_check_shared_examples.rb index e42a927b5ba..c735b98aa23 100644 --- a/spec/support/shared_examples/lib/gitlab/unique_ip_check_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/unique_ip_check_shared_examples.rb @@ -7,13 +7,13 @@ RSpec.shared_examples 'user login operation with unique ip limit' do end it 'allows user authenticating from the same ip' do - expect { operation_from_ip('ip') }.not_to raise_error - expect { operation_from_ip('ip') }.not_to raise_error + expect { operation_from_ip('111.221.4.3') }.not_to raise_error + expect { operation_from_ip('111.221.4.3') }.not_to raise_error end it 'blocks user authenticating from two distinct ips' do - expect { operation_from_ip('ip') }.not_to raise_error - expect { operation_from_ip('ip2') }.to raise_error(Gitlab::Auth::TooManyIps) + expect { operation_from_ip('111.221.4.3') }.not_to raise_error + expect { operation_from_ip('1.2.2.3') }.to raise_error(Gitlab::Auth::TooManyIps) end end end @@ -25,13 +25,13 @@ RSpec.shared_examples 'user login request with unique ip limit' do |success_stat end it 'allows user authenticating from the same ip' do - expect(request_from_ip('ip')).to have_gitlab_http_status(success_status) - expect(request_from_ip('ip')).to have_gitlab_http_status(success_status) + expect(request_from_ip('111.221.4.3')).to have_gitlab_http_status(success_status) + expect(request_from_ip('111.221.4.3')).to have_gitlab_http_status(success_status) end it 'blocks user authenticating from two distinct ips' do - expect(request_from_ip('ip')).to have_gitlab_http_status(success_status) - expect(request_from_ip('ip2')).to have_gitlab_http_status(:forbidden) + expect(request_from_ip('111.221.4.3')).to have_gitlab_http_status(success_status) + expect(request_from_ip('1.2.2.3')).to have_gitlab_http_status(:forbidden) end end end diff --git a/spec/support/shared_examples/loose_foreign_keys/have_loose_foreign_key.rb b/spec/support/shared_examples/loose_foreign_keys/have_loose_foreign_key.rb index 8f3a93de509..42eec74e64f 100644 --- a/spec/support/shared_examples/loose_foreign_keys/have_loose_foreign_key.rb +++ b/spec/support/shared_examples/loose_foreign_keys/have_loose_foreign_key.rb @@ -55,10 +55,16 @@ RSpec.shared_examples 'cleanup by a loose foreign key' do end def find_model - model.class.find_by(id: model.id) + query = model.class + # handle composite primary keys + connection = model.class.connection + connection.primary_keys(model.class.table_name).each do |primary_key| + query = query.where(primary_key => model.public_send(primary_key)) + end + query.first end - it 'deletes the model' do + it 'cleans up (delete or nullify) the model' do parent.delete expect(find_model).to be_present diff --git a/spec/support/shared_examples/metrics/sampler_shared_examples.rb b/spec/support/shared_examples/metrics/sampler_shared_examples.rb index ebf199c3a8d..cec540cd120 100644 --- a/spec/support/shared_examples/metrics/sampler_shared_examples.rb +++ b/spec/support/shared_examples/metrics/sampler_shared_examples.rb @@ -2,26 +2,98 @@ RSpec.shared_examples 'metrics sampler' do |env_prefix| context 'when sampling interval is passed explicitly' do - subject { described_class.new(42) } + subject(:sampler) { described_class.new(interval: 42, logger: double) } - specify { expect(subject.interval).to eq(42) } + specify { expect(sampler.interval).to eq(42) } end context 'when sampling interval is passed through the environment' do - subject { described_class.new } + subject(:sampler) { described_class.new(logger: double) } before do stub_env("#{env_prefix}_INTERVAL_SECONDS", '42') end - specify { expect(subject.interval).to eq(42) } + specify { expect(sampler.interval).to eq(42) } end context 'when no sampling interval is passed anywhere' do - subject { described_class.new } + subject(:sampler) { described_class.new(logger: double) } it 'uses the hardcoded default' do - expect(subject.interval).to eq(described_class::DEFAULT_SAMPLING_INTERVAL_SECONDS) + expect(sampler.interval).to eq(described_class::DEFAULT_SAMPLING_INTERVAL_SECONDS) + end + end + + describe '#start' do + include WaitHelpers + + subject(:sampler) { described_class.new(interval: 0.1) } + + it 'calls the sample method on the sampler thread' do + sampling_threads = [] + expect(sampler).to receive(:sample).at_least(:once) { sampling_threads << Thread.current } + + sampler.start + + wait_for('sampler has sampled', max_wait_time: 3) { sampling_threads.any? } + expect(sampling_threads.first.name).to eq(sampler.thread_name) + + sampler.stop + end + + context 'with warmup set to true' do + subject(:sampler) { described_class.new(interval: 0.1, warmup: true) } + + it 'calls the sample method first on the caller thread' do + sampling_threads = [] + current_thread = Thread.current + # Instead of sampling, we're keeping track of which thread the sampling happened on. + # We want the first sample to be on the spec thread, which would mean a blocking sample + # before the actual sampler thread starts. + expect(sampler).to receive(:sample).at_least(:once) { sampling_threads << Thread.current } + + sampler.start + + wait_for('sampler has sampled', max_wait_time: 3) { sampling_threads.size == 2 } + + expect(sampling_threads.first).to be(current_thread) + expect(sampling_threads.last.name).to eq(sampler.thread_name) + + sampler.stop + end + end + end + + describe '#safe_sample' do + let(:logger) { Logger.new(File::NULL) } + + subject(:sampler) { described_class.new(logger: logger) } + + it 'calls #sample once' do + expect(sampler).to receive(:sample) + + sampler.safe_sample + end + + context 'when sampling fails with error' do + before do + expect(sampler).to receive(:sample).and_raise "something failed" + end + + it 'recovers from errors' do + expect { sampler.safe_sample }.not_to raise_error + end + + context 'with logger' do + let(:logger) { double('logger') } + + it 'logs errors' do + expect(logger).to receive(:warn).with(an_instance_of(String)) + + expect { sampler.safe_sample }.not_to raise_error + end + end end end end diff --git a/spec/support/shared_examples/models/application_setting_shared_examples.rb b/spec/support/shared_examples/models/application_setting_shared_examples.rb index 60a02d85a1e..38f5c7be393 100644 --- a/spec/support/shared_examples/models/application_setting_shared_examples.rb +++ b/spec/support/shared_examples/models/application_setting_shared_examples.rb @@ -94,7 +94,7 @@ RSpec.shared_examples 'application settings examples' do '1:2:3:4:5::7:8', '[1:2:3:4:5::7:8]', '[2001:db8:85a3:8d3:1319:8a2e:370:7348]:443', - 'www.example2.com:8080', + 'www.example.org:8080', 'example.com:8080' ] @@ -114,7 +114,7 @@ RSpec.shared_examples 'application settings examples' do an_object_having_attributes(domain: 'example.com'), an_object_having_attributes(domain: 'subdomain.example.com'), an_object_having_attributes(domain: 'www.example.com'), - an_object_having_attributes(domain: 'www.example2.com', port: 8080), + an_object_having_attributes(domain: 'www.example.org', port: 8080), an_object_having_attributes(domain: 'example.com', port: 8080) ] diff --git a/spec/support/shared_examples/models/concerns/incident_management/escalatable_shared_examples.rb b/spec/support/shared_examples/models/concerns/incident_management/escalatable_shared_examples.rb index 7b33a95bfa1..8ee76efc896 100644 --- a/spec/support/shared_examples/models/concerns/incident_management/escalatable_shared_examples.rb +++ b/spec/support/shared_examples/models/concerns/incident_management/escalatable_shared_examples.rb @@ -95,6 +95,12 @@ RSpec.shared_examples 'a model including Escalatable' do it { is_expected.to eq([ignored_escalatable, resolved_escalatable, acknowledged_escalatable, triggered_escalatable]) } end end + + describe '.open' do + subject { all_escalatables.open } + + it { is_expected.to contain_exactly(acknowledged_escalatable, triggered_escalatable) } + end end describe '.status_value' do @@ -133,6 +139,24 @@ RSpec.shared_examples 'a model including Escalatable' do end end + describe '.open_status?' do + using RSpec::Parameterized::TableSyntax + + where(:status, :is_open_status) do + :triggered | true + :acknowledged | true + :resolved | false + :ignored | false + nil | false + end + + with_them do + it 'returns true when the status is open status' do + expect(described_class.open_status?(status)).to eq(is_open_status) + end + end + end + describe '#trigger' do subject { escalatable.trigger } @@ -237,6 +261,15 @@ RSpec.shared_examples 'a model including Escalatable' do end end + describe '#open?' do + it 'returns true when the status is open status' do + expect(triggered_escalatable.open?).to be true + expect(acknowledged_escalatable.open?).to be true + expect(resolved_escalatable.open?).to be false + expect(ignored_escalatable.open?).to be false + end + end + private def factory_from_class(klass) 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 ad15f82be5e..2a976fb7421 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 @@ -1,9 +1,9 @@ # frozen_string_literal: true -RSpec.shared_examples Integrations::SlackMattermostNotifier do |service_name| +RSpec.shared_examples Integrations::SlackMattermostNotifier do |integration_name| include StubRequests - let(:chat_service) { described_class.new } + let(:chat_integration) { described_class.new } let(:webhook_url) { 'https://example.gitlab.com' } def execute_with_options(options) @@ -17,7 +17,7 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |service_name| end describe 'Validations' do - context 'when service is active' do + context 'when integration is active' do before do subject.active = true end @@ -26,7 +26,7 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |service_name| it_behaves_like 'issue tracker integration URL attribute', :webhook end - context 'when service is inactive' do + context 'when integration is inactive' do before do subject.active = false end @@ -35,9 +35,9 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |service_name| end end - shared_examples "triggered #{service_name} service" do |event_type: nil, branches_to_be_notified: nil| + shared_examples "triggered #{integration_name} integration" do |event_type: nil, branches_to_be_notified: nil| before do - chat_service.branches_to_be_notified = branches_to_be_notified if branches_to_be_notified + chat_integration.branches_to_be_notified = branches_to_be_notified if branches_to_be_notified end let!(:stubbed_resolved_hostname) do @@ -45,14 +45,14 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |service_name| end it "notifies about #{event_type} events" do - chat_service.execute(data) + chat_integration.execute(data) expect(WebMock).to have_requested(:post, stubbed_resolved_hostname) end end - shared_examples "untriggered #{service_name} service" do |event_type: nil, branches_to_be_notified: nil| + shared_examples "untriggered #{integration_name} integration" do |event_type: nil, branches_to_be_notified: nil| before do - chat_service.branches_to_be_notified = branches_to_be_notified if branches_to_be_notified + chat_integration.branches_to_be_notified = branches_to_be_notified if branches_to_be_notified end let!(:stubbed_resolved_hostname) do @@ -60,7 +60,7 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |service_name| end it "notifies about #{event_type} events" do - chat_service.execute(data) + chat_integration.execute(data) expect(WebMock).not_to have_requested(:post, stubbed_resolved_hostname) end end @@ -69,50 +69,50 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |service_name| let_it_be(:project) { create(:project, :repository, :wiki_repo) } let_it_be(:user) { create(:user) } - let(:chat_service) { described_class.new( { project: project, webhook: webhook_url, branches_to_be_notified: 'all' }.merge(chat_service_params)) } - let(:chat_service_params) { {} } + let(:chat_integration) { described_class.new( { project: project, webhook: webhook_url, branches_to_be_notified: 'all' }.merge(chat_integration_params)) } + let(:chat_integration_params) { {} } let(:data) { Gitlab::DataBuilder::Push.build_sample(project, user) } let!(:stubbed_resolved_hostname) do stub_full_request(webhook_url, method: :post).request_pattern.uri_pattern.to_s end - subject(:execute_service) { chat_service.execute(data) } + subject(:execute_integration) { chat_integration.execute(data) } - shared_examples 'calls the service API with the event message' do |event_message| + shared_examples 'calls the integration API with the event message' do |event_message| specify do expect_next_instance_of(::Slack::Messenger) do |messenger| expect(messenger).to receive(:ping).with(event_message, anything).and_call_original end - execute_service + execute_integration expect(WebMock).to have_requested(:post, stubbed_resolved_hostname).once end end context 'with username for slack configured' do - let(:chat_service_params) { { username: 'slack_username' } } + let(:chat_integration_params) { { username: 'slack_username' } } it 'uses the username as an option' do expect(::Slack::Messenger).to execute_with_options(username: 'slack_username') - execute_service + execute_integration end end context 'push events' do let(:data) { Gitlab::DataBuilder::Push.build_sample(project, user) } - it_behaves_like 'calls the service API with the event message', /pushed to branch/ + it_behaves_like 'calls the integration API with the event message', /pushed to branch/ context 'with event channel' do - let(:chat_service_params) { { push_channel: 'random' } } + let(:chat_integration_params) { { push_channel: 'random' } } it 'uses the right channel for push event' do expect(::Slack::Messenger).to execute_with_options(channel: ['random']) - execute_service + execute_integration end end end @@ -123,7 +123,7 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |service_name| let(:ref) { 'refs/tags/v1.1.0' } let(:data) { Git::TagHooksService.new(project, user, change: { oldrev: oldrev, newrev: newrev, ref: ref }).send(:push_data) } - it_behaves_like 'calls the service API with the event message', /pushed new tag/ + it_behaves_like 'calls the integration API with the event message', /pushed new tag/ end context 'issue events' do @@ -131,15 +131,15 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |service_name| let(:data) { issue.to_hook_data(user) } - it_behaves_like 'calls the service API with the event message', /Issue (.*?) opened by/ + it_behaves_like 'calls the integration API with the event message', /Issue (.*?) opened by/ context 'whith event channel' do - let(:chat_service_params) { { issue_channel: 'random' } } + let(:chat_integration_params) { { issue_channel: 'random' } } it 'uses the right channel for issue event' do expect(::Slack::Messenger).to execute_with_options(channel: ['random']) - execute_service + execute_integration end context 'for confidential issues' do @@ -150,16 +150,16 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |service_name| it 'falls back to issue channel' do expect(::Slack::Messenger).to execute_with_options(channel: ['random']) - execute_service + execute_integration end context 'and confidential_issue_channel is defined' do - let(:chat_service_params) { { issue_channel: 'random', confidential_issue_channel: 'confidential' } } + let(:chat_integration_params) { { issue_channel: 'random', confidential_issue_channel: 'confidential' } } it 'uses the confidential issue channel when it is defined' do expect(::Slack::Messenger).to execute_with_options(channel: ['confidential']) - execute_service + execute_integration end end end @@ -171,15 +171,15 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |service_name| let(:data) { merge_request.to_hook_data(user) } - it_behaves_like 'calls the service API with the event message', /opened merge request/ + it_behaves_like 'calls the integration API with the event message', /opened merge request/ context 'with event channel' do - let(:chat_service_params) { { merge_request_channel: 'random' } } + let(:chat_integration_params) { { merge_request_channel: 'random' } } it 'uses the right channel for merge request event' do expect(::Slack::Messenger).to execute_with_options(channel: ['random']) - execute_service + execute_integration end end end @@ -189,15 +189,15 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |service_name| let(:data) { Gitlab::DataBuilder::WikiPage.build(wiki_page, user, 'create') } - it_behaves_like 'calls the service API with the event message', %r{ created (.*?)wikis/(.*?)|wiki page> in} + it_behaves_like 'calls the integration API with the event message', %r{ created (.*?)wikis/(.*?)|wiki page> in} context 'with event channel' do - let(:chat_service_params) { { wiki_page_channel: 'random' } } + let(:chat_integration_params) { { wiki_page_channel: 'random' } } it 'uses the right channel for wiki event' do expect(::Slack::Messenger).to execute_with_options(channel: ['random']) - execute_service + execute_integration end end end @@ -207,7 +207,7 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |service_name| let(:data) { Gitlab::DataBuilder::Deployment.build(deployment, Time.current) } - it_behaves_like 'calls the service API with the event message', /Deploy to (.*?) created/ + it_behaves_like 'calls the integration API with the event message', /Deploy to (.*?) created/ end context 'note event' do @@ -215,15 +215,15 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |service_name| let(:data) { Gitlab::DataBuilder::Note.build(issue_note, user) } - it_behaves_like 'calls the service API with the event message', /commented on issue/ + it_behaves_like 'calls the integration API with the event message', /commented on issue/ context 'with event channel' do - let(:chat_service_params) { { note_channel: 'random' } } + let(:chat_integration_params) { { note_channel: 'random' } } it 'uses the right channel' do expect(::Slack::Messenger).to execute_with_options(channel: ['random']) - execute_service + execute_integration end context 'for confidential notes' do @@ -234,16 +234,16 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |service_name| it 'falls back to note channel' do expect(::Slack::Messenger).to execute_with_options(channel: ['random']) - execute_service + execute_integration end context 'and confidential_note_channel is defined' do - let(:chat_service_params) { { note_channel: 'random', confidential_note_channel: 'confidential' } } + let(:chat_integration_params) { { note_channel: 'random', confidential_note_channel: 'confidential' } } it 'uses confidential channel' do expect(::Slack::Messenger).to execute_with_options(channel: ['confidential']) - execute_service + execute_integration end end end @@ -256,7 +256,7 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |service_name| let(:project) { create(:project, :repository, creator: user) } before do - allow(chat_service).to receive_messages( + allow(chat_integration).to receive_messages( project: project, service_hook: true, webhook: webhook_url @@ -283,23 +283,23 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |service_name| ) end - it_behaves_like "triggered #{service_name} service", event_type: "push" + it_behaves_like "triggered #{integration_name} integration", event_type: "push" end context 'notification enabled only for default branch' do - it_behaves_like "triggered #{service_name} service", event_type: "push", branches_to_be_notified: "default" + it_behaves_like "triggered #{integration_name} integration", event_type: "push", branches_to_be_notified: "default" end context 'notification enabled only for protected branches' do - it_behaves_like "untriggered #{service_name} service", event_type: "push", branches_to_be_notified: "protected" + it_behaves_like "untriggered #{integration_name} integration", event_type: "push", branches_to_be_notified: "protected" end context 'notification enabled only for default and protected branches' do - it_behaves_like "triggered #{service_name} service", event_type: "push", branches_to_be_notified: "default_and_protected" + it_behaves_like "triggered #{integration_name} integration", event_type: "push", branches_to_be_notified: "default_and_protected" end context 'notification enabled for all branches' do - it_behaves_like "triggered #{service_name} service", event_type: "push", branches_to_be_notified: "all" + it_behaves_like "triggered #{integration_name} integration", event_type: "push", branches_to_be_notified: "all" end end @@ -325,23 +325,23 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |service_name| ) end - it_behaves_like "triggered #{service_name} service", event_type: "push" + it_behaves_like "triggered #{integration_name} integration", event_type: "push" end context 'notification enabled only for default branch' do - it_behaves_like "untriggered #{service_name} service", event_type: "push", branches_to_be_notified: "default" + it_behaves_like "untriggered #{integration_name} integration", event_type: "push", branches_to_be_notified: "default" end context 'notification enabled only for protected branches' do - it_behaves_like "triggered #{service_name} service", event_type: "push", branches_to_be_notified: "protected" + it_behaves_like "triggered #{integration_name} integration", event_type: "push", branches_to_be_notified: "protected" end context 'notification enabled only for default and protected branches' do - it_behaves_like "triggered #{service_name} service", event_type: "push", branches_to_be_notified: "default_and_protected" + it_behaves_like "triggered #{integration_name} integration", event_type: "push", branches_to_be_notified: "default_and_protected" end context 'notification enabled for all branches' do - it_behaves_like "triggered #{service_name} service", event_type: "push", branches_to_be_notified: "all" + it_behaves_like "triggered #{integration_name} integration", event_type: "push", branches_to_be_notified: "all" end end @@ -367,23 +367,23 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |service_name| ) end - it_behaves_like "triggered #{service_name} service", event_type: "push" + it_behaves_like "triggered #{integration_name} integration", event_type: "push" end context 'notification enabled only for default branch' do - it_behaves_like "untriggered #{service_name} service", event_type: "push", branches_to_be_notified: "default" + it_behaves_like "untriggered #{integration_name} integration", event_type: "push", branches_to_be_notified: "default" end context 'notification enabled only for protected branches' do - it_behaves_like "triggered #{service_name} service", event_type: "push", branches_to_be_notified: "protected" + it_behaves_like "triggered #{integration_name} integration", event_type: "push", branches_to_be_notified: "protected" end context 'notification enabled only for default and protected branches' do - it_behaves_like "triggered #{service_name} service", event_type: "push", branches_to_be_notified: "default_and_protected" + it_behaves_like "triggered #{integration_name} integration", event_type: "push", branches_to_be_notified: "default_and_protected" end context 'notification enabled for all branches' do - it_behaves_like "triggered #{service_name} service", event_type: "push", branches_to_be_notified: "all" + it_behaves_like "triggered #{integration_name} integration", event_type: "push", branches_to_be_notified: "all" end end @@ -405,23 +405,23 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |service_name| ) end - it_behaves_like "triggered #{service_name} service", event_type: "push" + it_behaves_like "triggered #{integration_name} integration", event_type: "push" end context 'notification enabled only for default branch' do - it_behaves_like "untriggered #{service_name} service", event_type: "push", branches_to_be_notified: "default" + it_behaves_like "untriggered #{integration_name} integration", event_type: "push", branches_to_be_notified: "default" end context 'notification enabled only for protected branches' do - it_behaves_like "untriggered #{service_name} service", event_type: "push", branches_to_be_notified: "protected" + it_behaves_like "untriggered #{integration_name} integration", event_type: "push", branches_to_be_notified: "protected" end context 'notification enabled only for default and protected branches' do - it_behaves_like "untriggered #{service_name} service", event_type: "push", branches_to_be_notified: "default_and_protected" + it_behaves_like "untriggered #{integration_name} integration", event_type: "push", branches_to_be_notified: "default_and_protected" end context 'notification enabled for all branches' do - it_behaves_like "triggered #{service_name} service", event_type: "push", branches_to_be_notified: "all" + it_behaves_like "triggered #{integration_name} integration", event_type: "push", branches_to_be_notified: "all" end end end @@ -431,7 +431,7 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |service_name| let(:project) { create(:project, :repository, creator: user) } before do - allow(chat_service).to receive_messages( + allow(chat_integration).to receive_messages( project: project, service_hook: true, webhook: webhook_url @@ -452,7 +452,7 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |service_name| Gitlab::DataBuilder::Note.build(commit_note, user) end - it_behaves_like "triggered #{service_name} service", event_type: "commit comment" + it_behaves_like "triggered #{integration_name} integration", event_type: "commit comment" end context 'when merge request comment event executed' do @@ -465,7 +465,7 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |service_name| Gitlab::DataBuilder::Note.build(merge_request_note, user) end - it_behaves_like "triggered #{service_name} service", event_type: "merge request comment" + it_behaves_like "triggered #{integration_name} integration", event_type: "merge request comment" end context 'when issue comment event executed' do @@ -478,7 +478,7 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |service_name| Gitlab::DataBuilder::Note.build(issue_note, user) end - it_behaves_like "triggered #{service_name} service", event_type: "issue comment" + it_behaves_like "triggered #{integration_name} integration", event_type: "issue comment" end context 'when snippet comment event executed' do @@ -491,7 +491,7 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |service_name| Gitlab::DataBuilder::Note.build(snippet_note, user) end - it_behaves_like "triggered #{service_name} service", event_type: "snippet comment" + it_behaves_like "triggered #{integration_name} integration", event_type: "snippet comment" end end @@ -505,7 +505,7 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |service_name| end before do - allow(chat_service).to receive_messages( + allow(chat_integration).to receive_messages( project: project, service_hook: true, webhook: webhook_url @@ -519,15 +519,15 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |service_name| let(:data) { Gitlab::DataBuilder::Pipeline.build(pipeline) } context 'with default to notify_only_broken_pipelines' do - it_behaves_like "untriggered #{service_name} service", event_type: "pipeline" + it_behaves_like "untriggered #{integration_name} integration", event_type: "pipeline" end context 'with setting notify_only_broken_pipelines to false' do before do - chat_service.notify_only_broken_pipelines = false + chat_integration.notify_only_broken_pipelines = false end - it_behaves_like "triggered #{service_name} service", event_type: "pipeline" + it_behaves_like "triggered #{integration_name} integration", event_type: "pipeline" end end @@ -542,19 +542,19 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |service_name| let(:data) { Gitlab::DataBuilder::Pipeline.build(pipeline) } context 'notification enabled only for default branch' do - it_behaves_like "triggered #{service_name} service", event_type: "pipeline", branches_to_be_notified: "default" + it_behaves_like "triggered #{integration_name} integration", event_type: "pipeline", branches_to_be_notified: "default" end context 'notification enabled only for protected branches' do - it_behaves_like "untriggered #{service_name} service", event_type: "pipeline", branches_to_be_notified: "protected" + it_behaves_like "untriggered #{integration_name} integration", event_type: "pipeline", branches_to_be_notified: "protected" end context 'notification enabled only for default and protected branches' do - it_behaves_like "triggered #{service_name} service", event_type: "pipeline", branches_to_be_notified: "default_and_protected" + it_behaves_like "triggered #{integration_name} integration", event_type: "pipeline", branches_to_be_notified: "default_and_protected" end context 'notification enabled for all branches' do - it_behaves_like "triggered #{service_name} service", event_type: "pipeline", branches_to_be_notified: "all" + it_behaves_like "triggered #{integration_name} integration", event_type: "pipeline", branches_to_be_notified: "all" end end @@ -572,19 +572,19 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |service_name| let(:data) { Gitlab::DataBuilder::Pipeline.build(pipeline) } context 'notification enabled only for default branch' do - it_behaves_like "untriggered #{service_name} service", event_type: "pipeline", branches_to_be_notified: "default" + it_behaves_like "untriggered #{integration_name} integration", event_type: "pipeline", branches_to_be_notified: "default" end context 'notification enabled only for protected branches' do - it_behaves_like "triggered #{service_name} service", event_type: "pipeline", branches_to_be_notified: "protected" + it_behaves_like "triggered #{integration_name} integration", event_type: "pipeline", branches_to_be_notified: "protected" end context 'notification enabled only for default and protected branches' do - it_behaves_like "triggered #{service_name} service", event_type: "pipeline", branches_to_be_notified: "default_and_protected" + it_behaves_like "triggered #{integration_name} integration", event_type: "pipeline", branches_to_be_notified: "default_and_protected" end context 'notification enabled for all branches' do - it_behaves_like "triggered #{service_name} service", event_type: "pipeline", branches_to_be_notified: "all" + it_behaves_like "triggered #{integration_name} integration", event_type: "pipeline", branches_to_be_notified: "all" end end @@ -602,19 +602,19 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |service_name| let(:data) { Gitlab::DataBuilder::Pipeline.build(pipeline) } context 'notification enabled only for default branch' do - it_behaves_like "untriggered #{service_name} service", event_type: "pipeline", branches_to_be_notified: "default" + it_behaves_like "untriggered #{integration_name} integration", event_type: "pipeline", branches_to_be_notified: "default" end context 'notification enabled only for protected branches' do - it_behaves_like "triggered #{service_name} service", event_type: "pipeline", branches_to_be_notified: "protected" + it_behaves_like "triggered #{integration_name} integration", event_type: "pipeline", branches_to_be_notified: "protected" end context 'notification enabled only for default and protected branches' do - it_behaves_like "triggered #{service_name} service", event_type: "pipeline", branches_to_be_notified: "default_and_protected" + it_behaves_like "triggered #{integration_name} integration", event_type: "pipeline", branches_to_be_notified: "default_and_protected" end context 'notification enabled for all branches' do - it_behaves_like "triggered #{service_name} service", event_type: "pipeline", branches_to_be_notified: "all" + it_behaves_like "triggered #{integration_name} integration", event_type: "pipeline", branches_to_be_notified: "all" end end @@ -628,19 +628,78 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |service_name| let(:data) { Gitlab::DataBuilder::Pipeline.build(pipeline) } context 'notification enabled only for default branch' do - it_behaves_like "untriggered #{service_name} service", event_type: "pipeline", branches_to_be_notified: "default" + it_behaves_like "untriggered #{integration_name} integration", event_type: "pipeline", branches_to_be_notified: "default" end context 'notification enabled only for protected branches' do - it_behaves_like "untriggered #{service_name} service", event_type: "pipeline", branches_to_be_notified: "protected" + it_behaves_like "untriggered #{integration_name} integration", event_type: "pipeline", branches_to_be_notified: "protected" end context 'notification enabled only for default and protected branches' do - it_behaves_like "untriggered #{service_name} service", event_type: "pipeline", branches_to_be_notified: "default_and_protected" + it_behaves_like "untriggered #{integration_name} integration", event_type: "pipeline", branches_to_be_notified: "default_and_protected" end context 'notification enabled for all branches' do - it_behaves_like "triggered #{service_name} service", event_type: "pipeline", branches_to_be_notified: "all" + it_behaves_like "triggered #{integration_name} integration", event_type: "pipeline", branches_to_be_notified: "all" + end + end + end + end + + describe 'Deployment events' do + let_it_be(:user) { create(:user) } + let_it_be_with_reload(:project) { create(:project, :repository, creator: user) } + + let(:deployment) do + create(:deployment, :success, project: project, sha: project.commit.sha, ref: project.default_branch) + end + + let(:data) { Gitlab::DataBuilder::Deployment.build(deployment, Time.now) } + + before do + allow(chat_integration).to receive_messages( + project: project, + service_hook: true, + webhook: webhook_url + ) + + stub_full_request(webhook_url, method: :post) + end + + it_behaves_like "triggered #{integration_name} integration", event_type: "deployment" + + context 'on a protected branch' do + before do + create(:protected_branch, :create_branch_on_repository, project: project, name: 'a-protected-branch') + end + + let(:deployment) do + create(:deployment, :success, project: project, sha: project.commit.sha, ref: 'a-protected-branch') + end + + context 'notification enabled only for default branch' do + it_behaves_like "untriggered #{integration_name} integration", event_type: "pipeline", branches_to_be_notified: "default" + end + + context 'notification enabled only for protected branches' do + it_behaves_like "triggered #{integration_name} integration", event_type: "pipeline", branches_to_be_notified: "protected" + end + + context 'notification enabled only for default and protected branches' do + it_behaves_like "triggered #{integration_name} integration", event_type: "pipeline", branches_to_be_notified: "default_and_protected" + end + + context 'notification enabled for all branches' do + it_behaves_like "triggered #{integration_name} integration", event_type: "pipeline", branches_to_be_notified: "all" + end + + context 'when chat_notification_deployment_protected_branch_filter is disabled' do + before do + stub_feature_flags(chat_notification_deployment_protected_branch_filter: false) + end + + context 'notification enabled only for default branch' do + it_behaves_like "triggered #{integration_name} integration", event_type: "pipeline", branches_to_be_notified: "default" end end end diff --git a/spec/support/shared_examples/models/concerns/packages/destructible_shared_examples.rb b/spec/support/shared_examples/models/concerns/packages/destructible_shared_examples.rb new file mode 100644 index 00000000000..f974b46f881 --- /dev/null +++ b/spec/support/shared_examples/models/concerns/packages/destructible_shared_examples.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'destructible' do |factory:| + let_it_be(:item1) { create(factory, created_at: 1.month.ago, updated_at: 1.day.ago) } + let_it_be(:item2) { create(factory, created_at: 1.year.ago, updated_at: 1.year.ago) } + let_it_be(:item3) { create(factory, :pending_destruction, created_at: 2.years.ago, updated_at: 1.month.ago) } + let_it_be(:item4) { create(factory, :pending_destruction, created_at: 3.years.ago, updated_at: 2.weeks.ago) } + + describe '.next_pending_destruction' do + it 'returns the oldest item pending destruction based on updated_at' do + expect(described_class.next_pending_destruction(order_by: :updated_at)).to eq(item3) + end + + it 'returns the oldest item pending destruction based on created_at' do + expect(described_class.next_pending_destruction(order_by: :created_at)).to eq(item4) + end + end +end diff --git a/spec/support/shared_examples/models/concerns/ttl_expirable_shared_examples.rb b/spec/support/shared_examples/models/concerns/ttl_expirable_shared_examples.rb index 2d08de297a3..174b8609337 100644 --- a/spec/support/shared_examples/models/concerns/ttl_expirable_shared_examples.rb +++ b/spec/support/shared_examples/models/concerns/ttl_expirable_shared_examples.rb @@ -29,7 +29,7 @@ RSpec.shared_examples 'ttl_expirable' do describe '.active' do # rubocop:disable Rails/SaveBang let_it_be(:item1) { create(class_symbol) } - let_it_be(:item2) { create(class_symbol, :expired) } + let_it_be(:item2) { create(class_symbol, :pending_destruction) } let_it_be(:item3) { create(class_symbol, status: :error) } # rubocop:enable Rails/SaveBang @@ -38,17 +38,6 @@ RSpec.shared_examples 'ttl_expirable' do end end - describe '.lock_next_by' do - let_it_be(:item1) { create(class_symbol, created_at: 1.month.ago, updated_at: 1.day.ago) } - let_it_be(:item2) { create(class_symbol, created_at: 1.year.ago, updated_at: 1.year.ago) } - let_it_be(:item3) { create(class_symbol, created_at: 2.years.ago, updated_at: 1.month.ago) } - - it 'returns the first item sorted by the argument' do - expect(described_class.lock_next_by(:updated_at)).to contain_exactly(item2) - expect(described_class.lock_next_by(:created_at)).to contain_exactly(item3) - end - end - describe '#read', :freeze_time do let_it_be(:old_read_at) { 1.day.ago } let_it_be(:item1) { create(class_symbol, read_at: old_read_at) } diff --git a/spec/support/shared_examples/models/member_shared_examples.rb b/spec/support/shared_examples/models/member_shared_examples.rb index d5d137922eb..5b4b8c8fcc1 100644 --- a/spec/support/shared_examples/models/member_shared_examples.rb +++ b/spec/support/shared_examples/models/member_shared_examples.rb @@ -3,7 +3,7 @@ RSpec.shared_examples 'inherited access level as a member of entity' do let(:parent_entity) { create(:group) } let(:user) { create(:user) } - let(:member) { entity.is_a?(Group) ? entity.group_member(user) : entity.project_member(user) } + let(:member) { entity.member(user) } context 'with root parent_entity developer member' do before do @@ -49,7 +49,7 @@ RSpec.shared_examples 'inherited access level as a member of entity' do entity.add_maintainer(non_member_user) - non_member = entity.is_a?(Group) ? entity.group_member(non_member_user) : entity.project_member(non_member_user) + non_member = entity.member(non_member_user) expect { non_member.update!(access_level: Gitlab::Access::GUEST) } .to change { non_member.reload.access_level } diff --git a/spec/support/shared_examples/models/packages/debian/distribution_shared_examples.rb b/spec/support/shared_examples/models/packages/debian/distribution_shared_examples.rb index 750d3dd11e3..3f8c3b8960b 100644 --- a/spec/support/shared_examples/models/packages/debian/distribution_shared_examples.rb +++ b/spec/support/shared_examples/models/packages/debian/distribution_shared_examples.rb @@ -198,7 +198,6 @@ RSpec.shared_examples 'Debian Distribution' do |factory, container, can_freeze| describe 'relationships' do it { is_expected.to have_many(:publications).class_name('Packages::Debian::Publication').inverse_of(:distribution).with_foreign_key(:distribution_id) } it { is_expected.to have_many(:packages).class_name('Packages::Package').through(:publications) } - it { is_expected.to have_many(:package_files).class_name('Packages::PackageFile').through(:packages) } end end else @@ -229,6 +228,26 @@ RSpec.shared_examples 'Debian Distribution' do |factory, container, can_freeze| it 'returns only files from public packages with same codename' do expect(subject.to_a).to contain_exactly(*public_package_with_same_codename.package_files) end + + context 'with pending destruction package files' do + let_it_be(:package_file_pending_destruction) { create(:package_file, :pending_destruction, package: public_package_with_same_codename) } + + it 'does not return them' do + expect(subject.to_a).not_to include(package_file_pending_destruction) + end + + context 'with packages_installable_package_files disabled' do + before do + stub_feature_flags(packages_installable_package_files: false) + end + + it 'returns them' do + subject + + expect(subject.to_a).to include(package_file_pending_destruction) + end + end + end end end end diff --git a/spec/support/shared_examples/models/update_project_statistics_shared_examples.rb b/spec/support/shared_examples/models/update_project_statistics_shared_examples.rb index 2e01de2ea84..06326ffac97 100644 --- a/spec/support/shared_examples/models/update_project_statistics_shared_examples.rb +++ b/spec/support/shared_examples/models/update_project_statistics_shared_examples.rb @@ -115,16 +115,14 @@ RSpec.shared_examples 'UpdateProjectStatistics' do |with_counter_attribute| expect(ProjectStatistics) .not_to receive(:increment_statistic) - project.update!(pending_delete: true) - project.destroy! + expect(Projects::DestroyService.new(project, project.owner).execute).to eq(true) end it 'does not schedule a namespace statistics worker' do expect(Namespaces::ScheduleAggregationWorker) .not_to receive(:perform_async) - project.update!(pending_delete: true) - project.destroy! + expect(Projects::DestroyService.new(project, project.owner).execute).to eq(true) 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 3d52ed30c62..b43b7946e69 100644 --- a/spec/support/shared_examples/namespaces/traversal_scope_examples.rb +++ b/spec/support/shared_examples/namespaces/traversal_scope_examples.rb @@ -124,6 +124,18 @@ RSpec.shared_examples 'namespace traversal scopes' do it { expect(subject[0, 2]).to contain_exactly(group_1, group_2) } it { expect(subject[2, 2]).to contain_exactly(nested_group_1, nested_group_2) } end + + context 'with offset and limit' do + subject { described_class.where(id: [deep_nested_group_1, deep_nested_group_2]).offset(1).limit(1).self_and_ancestors } + + it { is_expected.to contain_exactly(group_2, nested_group_2, deep_nested_group_2) } + end + + context 'with upto' do + subject { described_class.where(id: deep_nested_group_1).self_and_ancestors(upto: nested_group_1.id) } + + it { is_expected.to contain_exactly(deep_nested_group_1) } + end end describe '.self_and_ancestors' do @@ -168,6 +180,19 @@ RSpec.shared_examples 'namespace traversal scopes' do it { is_expected.to contain_exactly(group_1.id, group_2.id) } end + + context 'with offset and limit' do + subject do + described_class + .where(id: [deep_nested_group_1, deep_nested_group_2]) + .limit(1) + .offset(1) + .self_and_ancestor_ids + .pluck(:id) + end + + it { is_expected.to contain_exactly(group_2.id, nested_group_2.id, deep_nested_group_2.id) } + end end describe '.self_and_ancestor_ids' do diff --git a/spec/support/shared_examples/requests/access_tokens_controller_shared_examples.rb b/spec/support/shared_examples/requests/access_tokens_controller_shared_examples.rb new file mode 100644 index 00000000000..6cd871d354c --- /dev/null +++ b/spec/support/shared_examples/requests/access_tokens_controller_shared_examples.rb @@ -0,0 +1,138 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'GET resource access tokens available' do + let_it_be(:active_resource_access_token) { create(:personal_access_token, user: bot_user) } + let_it_be(:inactive_resource_access_token) { create(:personal_access_token, :revoked, user: bot_user) } + + it 'retrieves active resource access tokens' do + subject + + expect(assigns(:active_resource_access_tokens)).to contain_exactly(active_resource_access_token) + end + + it 'retrieves inactive resource access tokens' do + subject + + expect(assigns(:inactive_resource_access_tokens)).to contain_exactly(inactive_resource_access_token) + end + + it 'lists all available scopes' do + subject + + expect(assigns(:scopes)).to eq(Gitlab::Auth.resource_bot_scopes) + end + + it 'retrieves newly created personal access token value' do + token_value = 'random-value' + allow(PersonalAccessToken).to receive(:redis_getdel).with("#{user.id}:#{resource.id}").and_return(token_value) + + subject + + expect(assigns(:new_resource_access_token)).to eq(token_value) + end +end + +RSpec.shared_examples 'POST resource access tokens available' do + def created_token + PersonalAccessToken.order(:created_at).last + end + + it 'returns success message' do + subject + + expect(flash[:notice]).to match('Your new access token has been created.') + end + + it 'creates resource access token' do + access_level = access_token_params[:access_level] || Gitlab::Access::MAINTAINER + subject + + expect(created_token.name).to eq(access_token_params[:name]) + expect(created_token.scopes).to eq(access_token_params[:scopes]) + expect(created_token.expires_at).to eq(access_token_params[:expires_at]) + expect(resource.member(created_token.user).access_level).to eq(access_level) + end + + it 'creates project bot user' do + subject + + expect(created_token.user).to be_project_bot + end + + it 'stores newly created token redis store' do + expect(PersonalAccessToken).to receive(:redis_store!) + + subject + end + + it { expect { subject }.to change { User.count }.by(1) } + it { expect { subject }.to change { PersonalAccessToken.count }.by(1) } + + context 'when unsuccessful' do + before do + allow_next_instance_of(ResourceAccessTokens::CreateService) do |service| + allow(service).to receive(:execute).and_return ServiceResponse.error(message: 'Failed!') + end + end + + it 'does not create the token' do + expect { subject }.not_to change { PersonalAccessToken.count } + end + + it 'does not add the project bot as a member' do + expect { subject }.not_to change { Member.count } + end + + it 'does not create the project bot user' do + expect { subject }.not_to change { User.count } + end + + it 'shows a failure alert' do + subject + + expect(flash[:alert]).to match("Failed to create new access token: Failed!") + end + end +end + +RSpec.shared_examples 'PUT resource access tokens available' do + it 'calls delete user worker' do + expect(DeleteUserWorker).to receive(:perform_async).with(user.id, bot_user.id, skip_authorization: true) + + subject + end + + it 'removes membership of bot user' do + subject + + expect(resource.reload.bots).not_to include(bot_user) + end + + it 'converts issuables of the bot user to ghost user' do + issue = create(:issue, author: bot_user) + + subject + + expect(issue.reload.author.ghost?).to be true + end + + it 'deletes project bot user' do + subject + + expect(User.exists?(bot_user.id)).to be_falsy + end + + context 'when unsuccessful' do + before do + allow_next_instance_of(ResourceAccessTokens::RevokeService) do |service| + allow(service).to receive(:execute).and_return ServiceResponse.error(message: 'Failed!') + end + end + + it 'shows a failure alert' do + subject + + expect(flash[:alert]).to include("Could not revoke access token") + end + end +end diff --git a/spec/support/shared_examples/requests/api/debian_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/debian_packages_shared_examples.rb index 2fd5e6a5f91..9f96cb2a164 100644 --- a/spec/support/shared_examples/requests/api/debian_packages_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/debian_packages_shared_examples.rb @@ -40,7 +40,6 @@ RSpec.shared_examples 'Debian packages upload request' do |status, body = nil| expect(response.body).to match(body) end end - it_behaves_like 'a package tracking event', described_class.name, 'push_package' else it "returns #{status}#{and_body}", :aggregate_failures do subject diff --git a/spec/support/shared_examples/requests/api/graphql/packages/package_details_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/packages/package_details_shared_examples.rb index d576a5874fd..9385706d991 100644 --- a/spec/support/shared_examples/requests/api/graphql/packages/package_details_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/graphql/packages/package_details_shared_examples.rb @@ -38,4 +38,28 @@ RSpec.shared_examples 'a package with files' do 'fileSha256' => first_file.file_sha256 ) end + + context 'with package files pending destruction' do + let_it_be(:package_file_pending_destruction) { create(:package_file, :pending_destruction, package: package) } + + let(:response_package_file_ids) { package_files_response.map { |pf| pf['id'] } } + + it 'does not return them' do + expect(package.reload.package_files).to include(package_file_pending_destruction) + + expect(response_package_file_ids).not_to include(package_file_pending_destruction.to_global_id.to_s) + end + + context 'with packages_installable_package_files disabled' do + before(:context) do + stub_feature_flags(packages_installable_package_files: false) + end + + it 'returns them' do + expect(package.reload.package_files).to include(package_file_pending_destruction) + + expect(response_package_file_ids).to include(package_file_pending_destruction.to_global_id.to_s) + end + end + end end diff --git a/spec/support/shared_examples/requests/api/nuget_endpoints_shared_examples.rb b/spec/support/shared_examples/requests/api/nuget_endpoints_shared_examples.rb index db70bc75c63..290bf58fb6b 100644 --- a/spec/support/shared_examples/requests/api/nuget_endpoints_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/nuget_endpoints_shared_examples.rb @@ -221,6 +221,7 @@ RSpec.shared_examples 'handling nuget search requests' do |anonymous_requests_ex let_it_be(:packages_c) { create_list(:nuget_package, 5, name: 'Dummy.PackageC', project: project) } let_it_be(:package_d) { create(:nuget_package, name: 'Dummy.PackageD', version: '5.0.5-alpha', project: project) } let_it_be(:package_e) { create(:nuget_package, name: 'Foo.BarE', project: project) } + let(:search_term) { 'uMmy' } let(:take) { 26 } let(:skip) { 0 } diff --git a/spec/support/shared_examples/services/alert_management_shared_examples.rb b/spec/support/shared_examples/services/alert_management_shared_examples.rb index 827ae42f970..23aee912d2d 100644 --- a/spec/support/shared_examples/services/alert_management_shared_examples.rb +++ b/spec/support/shared_examples/services/alert_management_shared_examples.rb @@ -64,12 +64,16 @@ RSpec.shared_examples 'processes never-before-seen recovery alert' do end RSpec.shared_examples 'processes one firing and one resolved prometheus alerts' do - it 'creates AlertManagement::Alert' do + it 'creates alerts and returns them in the payload', :aggregate_failures do expect(Gitlab::AppLogger).not_to receive(:warn) expect { subject } .to change(AlertManagement::Alert, :count).by(2) .and change(Note, :count).by(4) + + expect(subject).to be_success + expect(subject.payload[:alerts]).to all(be_a_kind_of(AlertManagement::Alert)) + expect(subject.payload[:alerts].size).to eq(2) end it_behaves_like 'processes incident issues' diff --git a/spec/support/shared_examples/services/container_registry_auth_service_shared_examples.rb b/spec/support/shared_examples/services/container_registry_auth_service_shared_examples.rb index f6e25ee6647..87bf134eeb8 100644 --- a/spec/support/shared_examples/services/container_registry_auth_service_shared_examples.rb +++ b/spec/support/shared_examples/services/container_registry_auth_service_shared_examples.rb @@ -71,6 +71,7 @@ end RSpec.shared_examples 'an accessible' do before do stub_feature_flags(container_registry_migration_phase1: false) + stub_feature_flags(container_registry_cdn_redirect: false) end let(:access) do @@ -163,6 +164,7 @@ RSpec.shared_examples 'a container registry auth service' do before do stub_feature_flags(container_registry_migration_phase1: false) + stub_feature_flags(container_registry_cdn_redirect: false) end describe '#full_access_token' do diff --git a/spec/support/shared_examples/services/incident_shared_examples.rb b/spec/support/shared_examples/services/incident_shared_examples.rb index 0277cce975a..36b0acf5a51 100644 --- a/spec/support/shared_examples/services/incident_shared_examples.rb +++ b/spec/support/shared_examples/services/incident_shared_examples.rb @@ -17,16 +17,6 @@ RSpec.shared_examples 'incident issue' do end end -RSpec.shared_examples 'has incident label' do - let(:label_properties) { attributes_for(:label, :incident) } - - it 'has exactly one incident label' do - expect(issue.labels).to be_one do |label| - label.slice(*label_properties.keys).symbolize_keys == label_properties - end - end -end - # This shared_example requires the following variables: # - issue (required) # @@ -45,6 +35,12 @@ RSpec.shared_examples 'not an incident issue' do expect(issue.work_item_type.base_type).not_to eq('incident') end + it_behaves_like 'does not have incident label' +end + +RSpec.shared_examples 'does not have incident label' do + let(:label_properties) { attributes_for(:label, :incident) } + it 'has not an incident label' do expect(issue.labels).not_to include(have_attributes(label_properties)) end diff --git a/spec/support/shared_examples/services/service_ping/service_ping_payload_with_all_expected_metrics_shared_examples.rb b/spec/support/shared_examples/services/service_ping/service_ping_payload_with_all_expected_metrics_shared_examples.rb index 535e7291b7e..856810a4de1 100644 --- a/spec/support/shared_examples/services/service_ping/service_ping_payload_with_all_expected_metrics_shared_examples.rb +++ b/spec/support/shared_examples/services/service_ping/service_ping_payload_with_all_expected_metrics_shared_examples.rb @@ -2,6 +2,8 @@ RSpec.shared_examples 'service ping payload with all expected metrics' do specify do + allow(ApplicationRecord.database).to receive(:flavor).and_return(nil) + aggregate_failures do expected_metrics.each do |metric| is_expected.to have_usage_metric metric['key_path'] diff --git a/spec/support/shared_examples/services/service_ping/service_ping_payload_without_restricted_metrics_shared_examples.rb b/spec/support/shared_examples/services/service_ping/service_ping_payload_without_restricted_metrics_shared_examples.rb index 9f18174cbc7..e05239a9a36 100644 --- a/spec/support/shared_examples/services/service_ping/service_ping_payload_without_restricted_metrics_shared_examples.rb +++ b/spec/support/shared_examples/services/service_ping/service_ping_payload_without_restricted_metrics_shared_examples.rb @@ -2,6 +2,8 @@ RSpec.shared_examples 'service ping payload without restricted metrics' do specify do + allow(ApplicationRecord.database).to receive(:flavor).and_return(nil) + aggregate_failures do restricted_metrics.each do |metric| is_expected.not_to have_usage_metric metric['key_path'] diff --git a/spec/support/shared_examples/work_item_base_types_importer.rb b/spec/support/shared_examples/work_item_base_types_importer.rb index 7d652be8d05..68e37171ea2 100644 --- a/spec/support/shared_examples/work_item_base_types_importer.rb +++ b/spec/support/shared_examples/work_item_base_types_importer.rb @@ -3,8 +3,8 @@ RSpec.shared_examples 'work item base types importer' do it 'creates all base work item types' do # Fixtures need to run on a pristine DB, but the test suite preloads the base types before(:suite) - WorkItem::Type.delete_all + WorkItems::Type.delete_all - expect { subject }.to change(WorkItem::Type, :count).from(0).to(WorkItem::Type::BASE_TYPES.count) + expect { subject }.to change(WorkItems::Type, :count).from(0).to(WorkItems::Type::BASE_TYPES.count) end end diff --git a/spec/support/shared_examples/workers/concerns/dependency_proxy/cleanup_worker_shared_examples.rb b/spec/support/shared_examples/workers/concerns/dependency_proxy/cleanup_worker_shared_examples.rb index c9014ad549c..26444437826 100644 --- a/spec/support/shared_examples/workers/concerns/dependency_proxy/cleanup_worker_shared_examples.rb +++ b/spec/support/shared_examples/workers/concerns/dependency_proxy/cleanup_worker_shared_examples.rb @@ -13,12 +13,12 @@ RSpec.shared_examples 'dependency_proxy_cleanup_worker' do end context 'with work to do' do - let_it_be(:artifact1) { create(factory_type, :expired, group: group) } - let_it_be(:artifact2) { create(factory_type, :expired, group: group, updated_at: 6.months.ago, created_at: 2.years.ago) } - let_it_be_with_reload(:artifact3) { create(factory_type, :expired, group: group, updated_at: 1.year.ago, created_at: 1.year.ago) } + let_it_be(:artifact1) { create(factory_type, :pending_destruction, group: group) } + let_it_be(:artifact2) { create(factory_type, :pending_destruction, group: group, updated_at: 6.months.ago, created_at: 2.years.ago) } + let_it_be_with_reload(:artifact3) { create(factory_type, :pending_destruction, group: group, updated_at: 1.year.ago, created_at: 1.year.ago) } let_it_be(:artifact4) { create(factory_type, group: group, updated_at: 2.years.ago, created_at: 2.years.ago) } - it 'deletes the oldest expired artifact based on updated_at', :aggregate_failures do + it 'deletes the oldest artifact pending destruction based on updated_at', :aggregate_failures do expect(worker).to receive(:log_extra_metadata_on_done).with("#{factory_type}_id".to_sym, artifact3.id) expect(worker).to receive(:log_extra_metadata_on_done).with(:group_id, group.id) @@ -40,10 +40,8 @@ RSpec.shared_examples 'dependency_proxy_cleanup_worker' do end describe '#remaining_work_count' do - let_it_be(:expired_artifacts) do - (1..3).map do |_| - create(factory_type, :expired, group: group) - end + before(:context) do + create_list(factory_type, 3, :pending_destruction, group: group) end subject { worker.remaining_work_count } diff --git a/spec/support/system_exit_detected.rb b/spec/support/system_exit_detected.rb new file mode 100644 index 00000000000..86c6af3ba8c --- /dev/null +++ b/spec/support/system_exit_detected.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +SystemExitDetected = Class.new(RuntimeError) + +RSpec.configure do |config| + config.around do |example| + example.run + rescue SystemExit + # In any cases, we cannot raise SystemExit in the tests, + # because it'll skip any following tests from running. + # Convert it to something that won't skip everything. + # See https://gitlab.com/gitlab-org/gitlab/-/issues/350060 + raise SystemExitDetected, "SystemExit should be rescued in the tests!" + end +end -- cgit v1.2.3