diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-07-20 09:55:51 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-07-20 09:55:51 +0000 |
commit | e8d2c2579383897a1dd7f9debd359abe8ae8373d (patch) | |
tree | c42be41678c2586d49a75cabce89322082698334 /spec/support | |
parent | fc845b37ec3a90aaa719975f607740c22ba6a113 (diff) | |
download | gitlab-ce-e8d2c2579383897a1dd7f9debd359abe8ae8373d.tar.gz |
Add latest changes from gitlab-org/gitlab@14-1-stable-eev14.1.0-rc42
Diffstat (limited to 'spec/support')
84 files changed, 1780 insertions, 526 deletions
diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb index e48a7b322ac..6f96d552da6 100644 --- a/spec/support/capybara.rb +++ b/spec/support/capybara.rb @@ -60,8 +60,8 @@ Capybara.register_driver :chrome do |app| # Chrome won't work properly in a Docker container in sandbox mode options.add_argument("no-sandbox") - # Run headless by default unless CHROME_HEADLESS specified - options.add_argument("headless") unless ENV['CHROME_HEADLESS'] =~ /^(false|no|0)$/i + # Run headless by default unless WEBDRIVER_HEADLESS specified + options.add_argument("headless") unless ENV['WEBDRIVER_HEADLESS'] =~ /^(false|no|0)$/i || ENV['CHROME_HEADLESS'] =~ /^(false|no|0)$/i # Disable /dev/shm use in CI. See https://gitlab.com/gitlab-org/gitlab/issues/4252 options.add_argument("disable-dev-shm-usage") if ENV['CI'] || ENV['CI_SERVER'] @@ -197,7 +197,7 @@ RSpec.configure do |config| raise JSConsoleError, message end rescue Selenium::WebDriver::Error::WebDriverError => error - if error.message =~ /unknown command: session\/[0-9a-zA-Z]+(?:\/se)?\/log/ + if error.message =~ %r{unknown command: session/[0-9a-zA-Z]+(?:/se)?/log} message = "Unable to access Chrome javascript console logs. You may be using an outdated version of ChromeDriver." raise JSConsoleError, message else diff --git a/spec/support/gitlab_experiment.rb b/spec/support/gitlab_experiment.rb index b84adf82d29..3d099dc689c 100644 --- a/spec/support/gitlab_experiment.rb +++ b/spec/support/gitlab_experiment.rb @@ -4,16 +4,6 @@ require 'gitlab/experiment/rspec' require_relative 'stub_snowplow' -# This is a temporary fix until we have a larger discussion around the -# challenges raised in https://gitlab.com/gitlab-org/gitlab/-/issues/300104 -require Rails.root.join('app', 'experiments', 'application_experiment') -class ApplicationExperiment # rubocop:disable Gitlab/NamespacedClass - def initialize(...) - super(...) - Feature.persist_used!(feature_flag_name) - end -end - RSpec.configure do |config| config.include StubSnowplow, :experiment diff --git a/spec/support/helpers/ci/template_helpers.rb b/spec/support/helpers/ci/template_helpers.rb new file mode 100644 index 00000000000..7bab58a574e --- /dev/null +++ b/spec/support/helpers/ci/template_helpers.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Ci + module TemplateHelpers + def secure_analyzers_prefix + 'registry.gitlab.com/gitlab-org/security-products/analyzers' + end + end +end + +Ci::TemplateHelpers.prepend_mod diff --git a/spec/support/helpers/cycle_analytics_helpers.rb b/spec/support/helpers/cycle_analytics_helpers.rb index 4515b96c79e..e48c8125d84 100644 --- a/spec/support/helpers/cycle_analytics_helpers.rb +++ b/spec/support/helpers/cycle_analytics_helpers.rb @@ -12,9 +12,7 @@ module CycleAnalyticsHelpers page.all('.gl-path-button').collect(&:text).map {|name_with_median| name_with_median.split("\n")[0] } end - def add_custom_stage_to_form - page.find_button(s_('CreateValueStreamForm|Add another stage')).click - + def fill_in_custom_stage_fields index = page.all('[data-testid="value-stream-stage-fields"]').length last_stage = page.all('[data-testid="value-stream-stage-fields"]').last @@ -25,6 +23,12 @@ module CycleAnalyticsHelpers end end + def add_custom_stage_to_form + page.find_button(s_('CreateValueStreamForm|Add another stage')).click + + fill_in_custom_stage_fields + end + def save_value_stream(custom_value_stream_name) fill_in 'create-value-stream-name', with: custom_value_stream_name diff --git a/spec/support/helpers/database/table_schema_helpers.rb b/spec/support/helpers/database/table_schema_helpers.rb index 48d33442110..472eaa45b4b 100644 --- a/spec/support/helpers/database/table_schema_helpers.rb +++ b/spec/support/helpers/database/table_schema_helpers.rb @@ -43,6 +43,14 @@ module Database expect(index_exists_by_name(name, schema: schema)).to be_nil end + def expect_foreign_key_to_exist(table_name, name, schema: nil) + expect(foreign_key_exists_by_name(table_name, name, schema: schema)).to eq(true) + end + + def expect_foreign_key_not_to_exist(table_name, name, schema: nil) + expect(foreign_key_exists_by_name(table_name, name, schema: schema)).to be_nil + end + def expect_check_constraint(table_name, name, definition, schema: nil) expect(check_constraint_definition(table_name, name, schema: schema)).to eq("CHECK ((#{definition}))") end @@ -133,6 +141,18 @@ module Database SQL end + def foreign_key_exists_by_name(table_name, foreign_key_name, schema: nil) + table_name = schema ? "#{schema}.#{table_name}" : table_name + + connection.select_value(<<~SQL) + SELECT true + FROM pg_catalog.pg_constraint + WHERE pg_constraint.conrelid = '#{table_name}'::regclass + AND pg_constraint.contype = 'f' + AND pg_constraint.conname = '#{foreign_key_name}' + SQL + end + def check_constraint_definition(table_name, constraint_name, schema: nil) table_name = schema ? "#{schema}.#{table_name}" : table_name diff --git a/spec/support/helpers/feature_flag_helpers.rb b/spec/support/helpers/feature_flag_helpers.rb index af7a674f3bc..51ba9039b70 100644 --- a/spec/support/helpers/feature_flag_helpers.rb +++ b/spec/support/helpers/feature_flag_helpers.rb @@ -14,6 +14,12 @@ module FeatureFlagHelpers strategies: strategies) end + def create_strategy(feature_flag, name = 'default', parameters = {}) + create(:operations_strategy, + feature_flag: feature_flag, + name: name) + end + def within_feature_flag_row(index) within ".gl-responsive-table-row:nth-child(#{index + 1})" do yield diff --git a/spec/support/helpers/features/admin_users_helpers.rb b/spec/support/helpers/features/admin_users_helpers.rb new file mode 100644 index 00000000000..99b19eedcff --- /dev/null +++ b/spec/support/helpers/features/admin_users_helpers.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Spec + module Support + module Helpers + module Features + module AdminUsersHelpers + def click_user_dropdown_toggle(user_id) + page.within("[data-testid='user-actions-#{user_id}']") do + find("[data-testid='dropdown-toggle']").click + end + end + + def click_action_in_user_dropdown(user_id, action) + click_user_dropdown_toggle(user_id) + + within find("[data-testid='user-actions-#{user_id}']") do + find('li button', exact_text: action).click + end + end + end + end + end + end +end diff --git a/spec/support/helpers/features/invite_members_modal_helper.rb b/spec/support/helpers/features/invite_members_modal_helper.rb index 1127c817656..7b8cd6963c0 100644 --- a/spec/support/helpers/features/invite_members_modal_helper.rb +++ b/spec/support/helpers/features/invite_members_modal_helper.rb @@ -9,7 +9,7 @@ module Spec click_on 'Invite members' page.within '#invite-members-modal' do - fill_in 'Select members or type email addresses', with: name + find('[data-testid="members-token-select-input"]').set(name) wait_for_requests click_button name diff --git a/spec/support/helpers/features/snippet_helpers.rb b/spec/support/helpers/features/snippet_helpers.rb index c26849a9680..dc718b1b212 100644 --- a/spec/support/helpers/features/snippet_helpers.rb +++ b/spec/support/helpers/features/snippet_helpers.rb @@ -1,14 +1,17 @@ # frozen_string_literal: true -# These helpers help you interact within the Editor Lite (single-file editor, snippets, etc.). +# These helpers help you interact within the Source Editor (single-file editor, snippets, etc.). # + +require Rails.root.join("spec/support/helpers/features/source_editor_spec_helpers.rb") + module Spec module Support module Helpers module Features module SnippetSpecHelpers include ActionView::Helpers::JavaScriptHelper - include Spec::Support::Helpers::Features::EditorLiteSpecHelpers + include Spec::Support::Helpers::Features::SourceEditorSpecHelpers def snippet_description_locator 'snippet-description' @@ -31,7 +34,7 @@ module Spec end def snippet_get_first_blob_value - page.find('.gl-editor-lite', match: :first) + page.find('.gl-source-editor', match: :first) end def snippet_description_value @@ -53,7 +56,7 @@ module Spec end def snippet_fill_in_content(value) - page.within('.gl-editor-lite') do + page.within('.gl-source-editor') do el = find('.inputarea') el.send_keys value end diff --git a/spec/support/helpers/features/editor_lite_spec_helpers.rb b/spec/support/helpers/features/source_editor_spec_helpers.rb index 0a67e753379..57057b47fbb 100644 --- a/spec/support/helpers/features/editor_lite_spec_helpers.rb +++ b/spec/support/helpers/features/source_editor_spec_helpers.rb @@ -1,12 +1,12 @@ # frozen_string_literal: true -# These helpers help you interact within the Editor Lite (single-file editor, snippets, etc.). +# These helpers help you interact within the Source Editor (single-file editor, snippets, etc.). # module Spec module Support module Helpers module Features - module EditorLiteSpecHelpers + module SourceEditorSpecHelpers include ActionView::Helpers::JavaScriptHelper def editor_set_value(value) diff --git a/spec/support/helpers/features/top_nav_spec_helpers.rb b/spec/support/helpers/features/top_nav_spec_helpers.rb index ab664ce4283..87ed897ec74 100644 --- a/spec/support/helpers/features/top_nav_spec_helpers.rb +++ b/spec/support/helpers/features/top_nav_spec_helpers.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# These helpers help you interact within the Editor Lite (single-file editor, snippets, etc.). +# These helpers help you interact within the Source Editor (single-file editor, snippets, etc.). # module Spec module Support diff --git a/spec/support/helpers/grafana_api_helpers.rb b/spec/support/helpers/grafana_api_helpers.rb index e47b1a808f2..7a7b6fec5b4 100644 --- a/spec/support/helpers/grafana_api_helpers.rb +++ b/spec/support/helpers/grafana_api_helpers.rb @@ -31,7 +31,7 @@ module GrafanaApiHelpers end def stub_all_grafana_proxy_requests(base_url) - stub_request(:any, /#{base_url}\/api\/datasources\/proxy/) + stub_request(:any, %r{#{base_url}/api/datasources/proxy}) .to_return( status: 200, body: fixture_file('grafana/proxy_response.json'), diff --git a/spec/support/helpers/javascript_fixtures_helpers.rb b/spec/support/helpers/javascript_fixtures_helpers.rb index 8fd8a548011..4c90b907d2d 100644 --- a/spec/support/helpers/javascript_fixtures_helpers.rb +++ b/spec/support/helpers/javascript_fixtures_helpers.rb @@ -43,12 +43,14 @@ module JavaScriptFixturesHelpers # Public: Reads a GraphQL query from the filesystem as a string # # query_path - file path to the GraphQL query, relative to `app/assets/javascripts` - # fragment_paths - an optional array of file paths to any fragments the query uses, - # also relative to `app/assets/javascripts` - def get_graphql_query_as_string(query_path, fragment_paths = []) - [query_path, *fragment_paths].map do |path| - File.read(File.join(Rails.root, '/app/assets/javascripts', path)) - end.join("\n") + def get_graphql_query_as_string(query_path) + path = Rails.root / 'app/assets/javascripts' / query_path + queries = Gitlab::Graphql::Queries.find(path) + if queries.length == 1 + queries.first.text(mode: Gitlab.ee? ? :ee : :ce ) + else + raise "Could not find query file at #{path}, please check your query_path" % path + end end private diff --git a/spec/support/helpers/jira_service_helper.rb b/spec/support/helpers/jira_service_helper.rb index ce908d53f88..3cfd0de06e8 100644 --- a/spec/support/helpers/jira_service_helper.rb +++ b/spec/support/helpers/jira_service_helper.rb @@ -4,7 +4,7 @@ module JiraServiceHelper JIRA_URL = "http://jira.example.net" JIRA_API = JIRA_URL + "/rest/api/2" - def jira_service_settings + def jira_integration_settings url = JIRA_URL username = 'jira-user' password = 'my-secret-password' @@ -77,7 +77,7 @@ module JiraServiceHelper JIRA_API + "/issue/#{issue_id}" end - def stub_jira_service_test + def stub_jira_integration_test WebMock.stub_request(:get, /serverInfo/).to_return(body: { url: 'http://url' }.to_json) end diff --git a/spec/support/helpers/live_debugger.rb b/spec/support/helpers/live_debugger.rb index cdb068760f4..f4199d518a3 100644 --- a/spec/support/helpers/live_debugger.rb +++ b/spec/support/helpers/live_debugger.rb @@ -7,8 +7,8 @@ module LiveDebugger puts puts "Current example is paused for live debugging." - if ENV['CHROME_HEADLESS'] =~ /^(false|no|0)$/i - puts "Switch to the Chrome window that was automatically opened to run the test in order to view current page" + if is_headless_disabled? + puts "Switch to the browser window that was automatically opened to run the test in order to view current page" else puts "Opening #{current_url} in your default browser..." end @@ -16,10 +16,16 @@ module LiveDebugger puts "The current user credentials are: #{@current_user.username} / #{@current_user.password}" if @current_user puts "Press any key to resume the execution of the example!!" - `open #{current_url}` if ENV['CHROME_HEADLESS'] !~ /^(false|no|0)$/i + `open #{current_url}` if is_headless_disabled? loop until $stdin.getch puts "Back to the example!" end + + def is_headless_disabled? + ActiveSupport::Deprecation.warn("CHROME_HEADLESS is deprecated. Use WEBDRIVER_HEADLESS instead.") if ENV.key?('CHROME_HEADLESS') + + ENV['WEBDRIVER_HEADLESS'] =~ /^(false|no|0)$/i || ENV['CHROME_HEADLESS'] =~ /^(false|no|0)$/i + end end diff --git a/spec/support/helpers/merge_request_diff_helpers.rb b/spec/support/helpers/merge_request_diff_helpers.rb index 49beecc6d4b..30afde7efed 100644 --- a/spec/support/helpers/merge_request_diff_helpers.rb +++ b/spec/support/helpers/merge_request_diff_helpers.rb @@ -3,8 +3,8 @@ module MergeRequestDiffHelpers def click_diff_line(line_holder, diff_side = nil) line = get_line_components(line_holder, diff_side) - line[:content].hover - line[:num].find('.js-add-diff-note-button', visible: false).send_keys(:return) + line_holder.hover + line[:num].find('.js-add-diff-note-button').click end def get_line_components(line_holder, diff_side = nil) diff --git a/spec/support/helpers/require_migration.rb b/spec/support/helpers/require_migration.rb index 8de71d3073f..de3a8a81ab5 100644 --- a/spec/support/helpers/require_migration.rb +++ b/spec/support/helpers/require_migration.rb @@ -15,7 +15,7 @@ class RequireMigration end MIGRATION_FOLDERS = %w[db/migrate db/post_migrate].freeze - SPEC_FILE_PATTERN = /.+\/(?<file_name>.+)_spec\.rb/.freeze + SPEC_FILE_PATTERN = %r{.+/(?<file_name>.+)_spec\.rb}.freeze class << self def require_migration!(file_name) @@ -29,7 +29,7 @@ class RequireMigration migration_folders.flat_map do |path| migration_path = Rails.root.join(path).to_s - Find.find(migration_path).grep(/\d+_#{file_name}\.rb/) + Find.find(migration_path).select { |m| File.basename(m).match? /\A\d+_#{file_name}\.rb\z/ } end end diff --git a/spec/support/helpers/services_helper.rb b/spec/support/helpers/services_helper.rb deleted file mode 100644 index bf007815551..00000000000 --- a/spec/support/helpers/services_helper.rb +++ /dev/null @@ -1,11 +0,0 @@ -# frozen_string_literal: true - -require_relative './after_next_helpers' - -module ServicesHelper - include AfterNextHelpers - - def expect_execution_of(service_class, *args) - expect_next(service_class, *args).to receive(:execute) - end -end diff --git a/spec/support/helpers/stub_experiments.rb b/spec/support/helpers/stub_experiments.rb index 408d16a7c08..8995b8f5f7b 100644 --- a/spec/support/helpers/stub_experiments.rb +++ b/spec/support/helpers/stub_experiments.rb @@ -11,7 +11,6 @@ module StubExperiments allow(Gitlab::Experimentation).to receive(:active?).and_call_original experiments.each do |experiment_key, enabled| - Feature.persist_used!("#{experiment_key}#{feature_flag_suffix}") allow(Gitlab::Experimentation).to receive(:active?).with(experiment_key) { enabled } end end @@ -26,7 +25,6 @@ module StubExperiments allow(Gitlab::Experimentation).to receive(:in_experiment_group?).and_call_original experiments.each do |experiment_key, enabled| - Feature.persist_used!("#{experiment_key}#{feature_flag_suffix}") allow(Gitlab::Experimentation).to receive(:in_experiment_group?).with(experiment_key, anything) { enabled } end end diff --git a/spec/support/helpers/stub_spam_services.rb b/spec/support/helpers/stub_spam_services.rb new file mode 100644 index 00000000000..841e8366845 --- /dev/null +++ b/spec/support/helpers/stub_spam_services.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module StubSpamServices + def stub_spam_services + allow(::Spam::SpamParams).to receive(:new_from_request) do + ::Spam::SpamParams.new( + captcha_response: double(:captcha_response), + spam_log_id: double(:spam_log_id), + ip_address: double(:ip_address), + user_agent: double(:user_agent), + referer: double(:referer) + ) + end + + allow_next_instance_of(::Spam::SpamActionService) do |service| + allow(service).to receive(:execute) + end + + allow_next_instance_of(::UserAgentDetailService) do |service| + allow(service).to receive(:create) + end + end +end diff --git a/spec/support/helpers/stubbed_feature.rb b/spec/support/helpers/stubbed_feature.rb index 67ceb7d9b35..4113a28182b 100644 --- a/spec/support/helpers/stubbed_feature.rb +++ b/spec/support/helpers/stubbed_feature.rb @@ -4,14 +4,6 @@ module StubbedFeature extend ActiveSupport::Concern - prepended do - cattr_reader(:persist_used) do - # persist feature flags in CI - # nil: indicates that we do not want to persist used feature flags - Gitlab::Utils.to_boolean(ENV['CI']) ? {} : nil - end - end - class_methods do # Turn stubbed feature flags on or off. def stub=(stub) @@ -41,8 +33,6 @@ module StubbedFeature feature_flag = super return feature_flag unless stub? - persist_used!(args.first) - # If feature flag is not persisted we mark the feature flag as enabled # We do `m.call` as we want to validate the execution of method arguments # and a feature flag state if it is not persisted @@ -52,17 +42,5 @@ module StubbedFeature feature_flag end - - # This method creates a temporary file in `tmp/feature_flags` - # if feature flag was touched during execution - def persist_used!(name) - return unless persist_used - return if persist_used[name] - - persist_used[name] = true - FileUtils.touch( - Rails.root.join('tmp', 'feature_flags', name.to_s + ".used") - ) - end end end diff --git a/spec/support/helpers/test_env.rb b/spec/support/helpers/test_env.rb index 40a3dbfbf25..8814d260fb3 100644 --- a/spec/support/helpers/test_env.rb +++ b/spec/support/helpers/test_env.rb @@ -1,7 +1,8 @@ # frozen_string_literal: true +require 'parallel' + module TestEnv - extend ActiveSupport::Concern extend self ComponentFailedToInstallError = Class.new(StandardError) @@ -94,50 +95,40 @@ module TestEnv 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 + + # Can be overriden + def setup_methods + SETUP_METHODS + end # Test environment # # See gitlab.yml.example test section for paths # - def init(opts = {}) + def init unless Rails.env.test? puts "\nTestEnv.init can only be run if `RAILS_ENV` is set to 'test' not '#{Rails.env}'!\n" exit 1 end + start = Time.now # Disable mailer for spinach tests - disable_mailer if opts[:mailer] == false - clean_test_path - setup_gitlab_shell - - setup_gitaly - - # Feature specs are run through Workhorse - setup_workhorse - - # Create repository for FactoryBot.create(:project) - setup_factory_repo - - # Create repository for FactoryBot.create(:forked_project_with_submodules) - setup_forked_repo - end - - included do |config| - config.append_before do - set_current_example_group + # Install components in parallel as most of the setup is I/O. + Parallel.each(setup_methods) do |method| + public_send(method) end - end - def disable_mailer - allow_any_instance_of(NotificationService).to receive(:mailer) - .and_return(double.as_null_object) + post_init + + puts "\nTest environment set up in #{Time.now - start} seconds" end - def enable_mailer - allow_any_instance_of(NotificationService).to receive(:mailer) - .and_call_original + # Can be overriden + def post_init + start_gitaly(gitaly_dir) end # Clean /tmp/tests @@ -164,12 +155,11 @@ module TestEnv end def setup_gitaly - install_gitaly_args = [gitaly_dir, repos_path, gitaly_url].compact.join(',') - component_timed_setup('Gitaly', install_dir: gitaly_dir, version: Gitlab::GitalyClient.expected_server_version, - task: "gitlab:gitaly:install[#{install_gitaly_args}]") do + task: "gitlab:gitaly:install", + task_args: [gitaly_dir, repos_path, gitaly_url].compact) do Gitlab::SetupHelper::Gitaly.create_configuration( gitaly_dir, { 'default' => repos_path }, @@ -190,8 +180,6 @@ module TestEnv ) Gitlab::SetupHelper::Praefect.create_configuration(gitaly_dir, { 'praefect' => repos_path }, force: true) end - - start_gitaly(gitaly_dir) end def gitaly_socket_path @@ -273,19 +261,18 @@ module TestEnv raise "could not connect to #{service} at #{socket.inspect} after #{sleep_time} seconds" end + # Feature specs are run through Workhorse def setup_workhorse start = Time.now return if skip_compile_workhorse? - puts "\n==> Setting up GitLab Workhorse..." - FileUtils.rm_rf(workhorse_dir) Gitlab::SetupHelper::Workhorse.compile_into(workhorse_dir) Gitlab::SetupHelper::Workhorse.create_configuration(workhorse_dir, nil) File.write(workhorse_tree_file, workhorse_tree) if workhorse_source_clean? - puts " GitLab Workhorse set up in #{Time.now - start} seconds...\n" + puts "==> GitLab Workhorse set up in #{Time.now - start} seconds...\n" end def skip_compile_workhorse? @@ -349,10 +336,12 @@ module TestEnv ENV.fetch('GITLAB_WORKHORSE_URL', nil) end + # Create repository for FactoryBot.create(:project) def setup_factory_repo setup_repo(factory_repo_path, factory_repo_path_bare, factory_repo_name, BRANCH_SHA) end + # Create repository for FactoryBot.create(:forked_project_with_submodules) # This repo has a submodule commit that is not present in the main test # repository. def setup_forked_repo @@ -363,20 +352,18 @@ module TestEnv clone_url = "https://gitlab.com/gitlab-org/#{repo_name}.git" unless File.directory?(repo_path) - puts "\n==> Setting up #{repo_name} repository in #{repo_path}..." start = Time.now system(*%W(#{Gitlab.config.git.bin_path} clone --quiet -- #{clone_url} #{repo_path})) - puts " #{repo_path} set up in #{Time.now - start} seconds...\n" + puts "==> #{repo_path} set up in #{Time.now - start} seconds...\n" end set_repo_refs(repo_path, refs) unless File.directory?(repo_path_bare) - puts "\n==> Setting up #{repo_name} bare repository in #{repo_path_bare}..." start = Time.now # We must copy bare repositories because we will push to them. system(git_env, *%W(#{Gitlab.config.git.bin_path} clone --quiet --bare -- #{repo_path} #{repo_path_bare})) - puts " #{repo_path_bare} set up in #{Time.now - start} seconds...\n" + puts "==> #{repo_path_bare} set up in #{Time.now - start} seconds...\n" end end @@ -468,10 +455,6 @@ module TestEnv private - def set_current_example_group - Thread.current[:current_example_group] = ::RSpec.current_example.metadata[:example_group] - end - # These are directories that should be preserved at cleanup time def test_dirs @test_dirs ||= %w[ @@ -526,7 +509,7 @@ module TestEnv end end - def component_timed_setup(component, install_dir:, version:, task:) + def component_timed_setup(component, install_dir:, version:, task:, task_args: []) start = Time.now ensure_component_dir_name_is_correct!(component, install_dir) @@ -535,17 +518,22 @@ module TestEnv return if File.exist?(install_dir) && ci? if component_needs_update?(install_dir, version) - puts "\n==> Setting up #{component}..." # Cleanup the component entirely to ensure we start fresh FileUtils.rm_rf(install_dir) - unless system('rake', task) - raise ComponentFailedToInstallError + if ENV['SKIP_RAILS_ENV_IN_RAKE'] + # When we run `scripts/setup-test-env`, we take care of loading the necessary dependencies + # so we can run the rake task programmatically. + Rake::Task[task].invoke(*task_args) + else + # In other cases, we run the task via `rake` so that the environment + # and dependencies are automatically loaded. + raise ComponentFailedToInstallError unless system('rake', "#{task}[#{task_args.join(',')}]") end yield if block_given? - puts " #{component} set up in #{Time.now - start} seconds...\n" + puts "==> #{component} set up in #{Time.now - start} seconds...\n" end rescue ComponentFailedToInstallError puts "\n#{component} failed to install, cleaning up #{install_dir}!\n" diff --git a/spec/support/matchers/be_executed.rb b/spec/support/matchers/be_executed.rb new file mode 100644 index 00000000000..fea86386755 --- /dev/null +++ b/spec/support/matchers/be_executed.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +# named as `get_executed` to avoid clashing +# with `be_executed === have_attributes(executed: true)` +RSpec::Matchers.define :get_executed do |args = []| + include AfterNextHelpers + + match do |service_class| + expect_next(service_class, *args).to receive(:execute) + end +end diff --git a/spec/support/matchers/have_issuable_counts.rb b/spec/support/matchers/have_issuable_counts.rb index 049cfc022fb..586ba0651dc 100644 --- a/spec/support/matchers/have_issuable_counts.rb +++ b/spec/support/matchers/have_issuable_counts.rb @@ -6,7 +6,7 @@ RSpec::Matchers.define :have_issuable_counts do |opts| end match do |actual| - actual.within '.issues-state-filters' do + actual.within '.top-area' do expected_counts.each do |expected_count| expect(actual).to have_content(expected_count) end diff --git a/spec/support/matchers/usage_metric_matchers.rb b/spec/support/matchers/usage_metric_matchers.rb new file mode 100644 index 00000000000..83433334e8b --- /dev/null +++ b/spec/support/matchers/usage_metric_matchers.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +RSpec::Matchers.define :have_usage_metric do |key_path| + match do |payload| + payload = payload.deep_stringify_keys + + key_path.split('.').each do |part| + break false unless payload&.has_key?(part) + + payload = payload[part] + end + end + + failure_message do + "Payload does not contain metric with key path: '#{key_path}'" + end + + failure_message_when_negated do + "Payload contains restricted metric with key path: '#{key_path}'" + end +end diff --git a/spec/support/omniauth_strategy.rb b/spec/support/omniauth_strategy.rb index 23907b8e450..5d5ee7dc1db 100644 --- a/spec/support/omniauth_strategy.rb +++ b/spec/support/omniauth_strategy.rb @@ -6,12 +6,6 @@ module StrategyHelpers include Shoulda::Matchers::ActionController include OmniAuth::Test::StrategyTestCase - def post(*args) - super.tap do - @response = ActionDispatch::TestResponse.from_response(last_response) - end - end - def auth_hash last_request.env['omniauth.auth'] end @@ -21,7 +15,9 @@ module StrategyHelpers original_on_failure = OmniAuth.config.on_failure OmniAuth.config.test_mode = false - OmniAuth.config.on_failure = OmniAuth::FailureEndpoint + OmniAuth.config.on_failure = proc do |env| + OmniAuth::FailureEndpoint.new(env).redirect_to_failure + end yield ensure @@ -33,7 +29,7 @@ end RSpec.configure do |config| config.include StrategyHelpers, type: :strategy - config.around(:all, type: :strategy) do |example| + config.around(type: :strategy) do |example| StrategyHelpers.without_test_mode do example.run end diff --git a/spec/support/redis/redis_helpers.rb b/spec/support/redis/redis_helpers.rb index b8118bf94cc..3511d906203 100644 --- a/spec/support/redis/redis_helpers.rb +++ b/spec/support/redis/redis_helpers.rb @@ -5,21 +5,21 @@ module RedisHelpers # Usage: performance enhancement def redis_cache_cleanup! - Gitlab::Redis::Cache.with(&:flushall) + Gitlab::Redis::Cache.with(&:flushdb) end # Usage: SideKiq, Mailroom, CI Runner, Workhorse, push services def redis_queues_cleanup! - Gitlab::Redis::Queues.with(&:flushall) + Gitlab::Redis::Queues.with(&:flushdb) end # Usage: session state, rate limiting def redis_shared_state_cleanup! - Gitlab::Redis::SharedState.with(&:flushall) + Gitlab::Redis::SharedState.with(&:flushdb) end # Usage: CI trace chunks def redis_trace_chunks_cleanup! - Gitlab::Redis::TraceChunks.with(&:flushall) + Gitlab::Redis::TraceChunks.with(&:flushdb) end end diff --git a/spec/support/services/issuable_import_csv_service_shared_examples.rb b/spec/support/services/issuable_import_csv_service_shared_examples.rb index f68750bec32..07118198969 100644 --- a/spec/support/services/issuable_import_csv_service_shared_examples.rb +++ b/spec/support/services/issuable_import_csv_service_shared_examples.rb @@ -67,10 +67,7 @@ RSpec.shared_examples 'issuable import csv service' do |issuable_type| it 'correctly sets the issuable attributes' do expect { subject }.to change { issuables.count }.by 4 - expect(issuables.reload.last).to have_attributes( - title: 'Test Title', - description: 'Test Description' - ) + expect(issuables.reload).to include(have_attributes({ title: 'Test Title', description: 'Test Description' })) end it_behaves_like 'importer with email notification' @@ -89,10 +86,7 @@ RSpec.shared_examples 'issuable import csv service' do |issuable_type| it 'correctly sets the issuable attributes' do expect { subject }.to change { issuables.count }.by 3 - expect(issuables.reload.last).to have_attributes( - title: 'Title with quote"', - description: 'Description' - ) + expect(issuables.reload).to include(have_attributes(title: 'Title with quote"', description: 'Description')) end it_behaves_like 'importer with email notification' @@ -111,10 +105,7 @@ RSpec.shared_examples 'issuable import csv service' do |issuable_type| it 'correctly sets the issuable attributes' do expect { subject }.to change { issuables.count }.by 2 - expect(issuables.reload.last).to have_attributes( - title: 'Hello', - description: 'World' - ) + expect(issuables.reload).to include(have_attributes(title: 'Hello', description: 'World')) end it_behaves_like 'importer with email notification' @@ -133,10 +124,7 @@ RSpec.shared_examples 'issuable import csv service' do |issuable_type| it 'correctly sets the issuable attributes' do expect { subject }.to change { issuables.count }.by 3 - expect(issuables.reload.last).to have_attributes( - title: 'Hello', - description: 'World' - ) + expect(issuables.reload).to include(have_attributes(title: 'Hello', description: 'World')) end it_behaves_like 'importer with email notification' diff --git a/spec/support/shared_contexts/features/integrations/integrations_shared_context.rb b/spec/support/shared_contexts/features/integrations/integrations_shared_context.rb index e532b42fd1c..3d2b0433b21 100644 --- a/spec/support/shared_contexts/features/integrations/integrations_shared_context.rb +++ b/spec/support/shared_contexts/features/integrations/integrations_shared_context.rb @@ -1,36 +1,38 @@ # frozen_string_literal: true -Integration.available_services_names.each do |service| - RSpec.shared_context service do - include JiraServiceHelper if service == 'jira' +Integration.available_integration_names.each do |integration| + RSpec.shared_context integration do + include JiraServiceHelper if integration == 'jira' - let(:dashed_service) { service.dasherize } - let(:service_method) { Project.integration_association_name(service) } - let(:service_klass) { Integration.integration_name_to_model(service) } - let(:service_instance) { service_klass.new } - let(:service_fields) { service_instance.fields } - let(:service_attrs_list) { service_fields.inject([]) {|arr, hash| arr << hash[:name].to_sym } } - let(:service_attrs) do - service_attrs_list.inject({}) do |hash, k| + let(:dashed_integration) { integration.dasherize } + let(:integration_method) { Project.integration_association_name(integration) } + let(:integration_klass) { Integration.integration_name_to_model(integration) } + let(:integration_instance) { integration_klass.new } + let(:integration_fields) { integration_instance.fields } + let(:integration_attrs_list) { integration_fields.inject([]) {|arr, hash| arr << hash[:name].to_sym } } + let(:integration_attrs) do + integration_attrs_list.inject({}) do |hash, k| if k =~ /^(token*|.*_token|.*_key)/ hash.merge!(k => 'secrettoken') - elsif service == 'confluence' && k == :confluence_url + elsif integration == 'confluence' && k == :confluence_url hash.merge!(k => 'https://example.atlassian.net/wiki') - elsif service == 'datadog' && k == :datadog_site + elsif integration == 'datadog' && k == :datadog_site hash.merge!(k => 'datadoghq.com') + elsif integration == 'packagist' && k == :server + hash.merge!(k => 'https://packagist.example.com') elsif k =~ /^(.*_url|url|webhook)/ hash.merge!(k => "http://example.com") - elsif service_klass.method_defined?("#{k}?") + elsif integration_klass.method_defined?("#{k}?") hash.merge!(k => true) - elsif service == 'irker' && k == :recipients + elsif integration == 'irker' && k == :recipients hash.merge!(k => 'irc://irc.network.net:666/#channel') - elsif service == 'irker' && k == :server_port + elsif integration == 'irker' && k == :server_port hash.merge!(k => 1234) - elsif service == 'jira' && k == :jira_issue_transition_id + elsif integration == 'jira' && k == :jira_issue_transition_id hash.merge!(k => '1,2,3') - elsif service == 'emails_on_push' && k == :recipients + elsif integration == 'emails_on_push' && k == :recipients hash.merge!(k => 'foo@bar.com') - elsif service == 'slack' || service == 'mattermost' && k == :labels_to_be_notified_behavior + elsif integration == 'slack' || integration == 'mattermost' && k == :labels_to_be_notified_behavior hash.merge!(k => "match_any") else hash.merge!(k => "someword") @@ -45,28 +47,28 @@ Integration.available_services_names.each do |service| end before do - enable_license_for_service(service) - stub_jira_service_test if service == 'jira' + enable_license_for_integration(integration) + stub_jira_integration_test if integration == 'jira' end - def initialize_service(service, attrs = {}) - service_item = project.find_or_initialize_service(service) - service_item.attributes = attrs - service_item.properties = service_attrs - service_item.save! - service_item + def initialize_integration(integration, attrs = {}) + record = project.find_or_initialize_integration(integration) + record.attributes = attrs + record.properties = integration_attrs + record.save! + record end private - def enable_license_for_service(service) + def enable_license_for_integration(integration) return unless respond_to?(:stub_licensed_features) - licensed_feature = licensed_features[service] + licensed_feature = licensed_features[integration] return unless licensed_feature stub_licensed_features(licensed_feature => true) - project.clear_memoization(:disabled_services) + project.clear_memoization(:disabled_integrations) end end end diff --git a/spec/support/shared_contexts/navbar_structure_context.rb b/spec/support/shared_contexts/navbar_structure_context.rb index c00b7203af6..b7eb03de8f0 100644 --- a/spec/support/shared_contexts/navbar_structure_context.rb +++ b/spec/support/shared_contexts/navbar_structure_context.rb @@ -1,19 +1,6 @@ # frozen_string_literal: true RSpec.shared_context 'project navbar structure' do - let(:analytics_nav_item) do - { - nav_item: _('Analytics'), - nav_sub_items: [ - _('CI/CD'), - (_('Code Review') if Gitlab.ee?), - (_('Merge Request') if Gitlab.ee?), - _('Repository'), - _('Value Stream') - ] - } - end - let(:security_and_compliance_nav_item) do { nav_item: _('Security & Compliance'), @@ -24,64 +11,20 @@ RSpec.shared_context 'project navbar structure' do } end - let(:monitor_nav_item) do - { - nav_item: _('Operations'), - nav_sub_items: monitor_menu_items - } - end - - let(:monitor_menu_items) do - [ - _('Metrics'), - _('Logs'), - _('Tracing'), - _('Error Tracking'), - _('Alerts'), - _('Incidents'), - _('Serverless'), - _('Terraform'), - _('Kubernetes'), - _('Environments'), - _('Feature Flags'), - _('Product Analytics') - ] - end - - let(:project_information_nav_item) do - { - nav_item: _('Project overview'), - nav_sub_items: [ - _('Details'), - _('Activity'), - _('Releases') - ] - } - end - - let(:settings_menu_items) do - [ - _('General'), - _('Integrations'), - _('Webhooks'), - _('Access Tokens'), - _('Repository'), - _('CI/CD'), - _('Operations') - ] - end - - let(:project_context_nav_item) do - { - nav_item: "#{project.name[0, 1].upcase} #{project.name}", - nav_sub_items: [] - } - end - let(:structure) do [ - project_context_nav_item, - project_information_nav_item, + { + nav_item: "#{project.name[0, 1].upcase} #{project.name}", + nav_sub_items: [] + }, + { + nav_item: _('Project information'), + nav_sub_items: [ + _('Activity'), + _('Labels'), + _('Members') + ] + }, { nav_item: _('Repository'), nav_sub_items: [ @@ -120,8 +63,44 @@ RSpec.shared_context 'project navbar structure' do ] }, security_and_compliance_nav_item, - monitor_nav_item, - analytics_nav_item, + { + nav_item: _('Deployments'), + nav_sub_items: [ + _('Feature Flags'), + _('Environments'), + _('Releases') + ] + }, + { + nav_item: _('Monitor'), + nav_sub_items: [ + _('Metrics'), + _('Logs'), + _('Tracing'), + _('Error Tracking'), + _('Alerts'), + _('Incidents'), + _('Product Analytics') + ] + }, + { + nav_item: _('Infrastructure'), + nav_sub_items: [ + _('Kubernetes clusters'), + _('Serverless platform'), + _('Terraform') + ] + }, + { + nav_item: _('Analytics'), + nav_sub_items: [ + _('CI/CD'), + (_('Code review') if Gitlab.ee?), + (_('Merge request') if Gitlab.ee?), + _('Repository'), + _('Value stream') + ] + }, { nav_item: _('Wiki'), nav_sub_items: [] @@ -132,7 +111,15 @@ RSpec.shared_context 'project navbar structure' do }, { nav_item: _('Settings'), - nav_sub_items: settings_menu_items + nav_sub_items: [ + _('General'), + _('Integrations'), + _('Webhooks'), + _('Access Tokens'), + _('Repository'), + _('CI/CD'), + _('Monitor') + ] } ].compact end @@ -189,17 +176,6 @@ RSpec.shared_context 'group navbar structure' do } end - let(:group_information_nav_item) do - { - nav_item: _('Group information'), - nav_sub_items: [ - _('Activity'), - _('Labels'), - _('Members') - ] - } - end - let(:issues_nav_items) do [ _('List'), @@ -208,17 +184,20 @@ RSpec.shared_context 'group navbar structure' do ] end - let(:group_context_nav_item) do - { - nav_item: "#{group.name[0, 1].upcase} #{group.name}", - nav_sub_items: [] - } - end - let(:structure) do [ - group_context_nav_item, - group_information_nav_item, + { + nav_item: "#{group.name[0, 1].upcase} #{group.name}", + nav_sub_items: [] + }, + { + nav_item: _('Group information'), + nav_sub_items: [ + _('Activity'), + _('Labels'), + _('Members') + ] + }, { nav_item: _('Issues'), nav_sub_items: issues_nav_items @@ -227,7 +206,7 @@ RSpec.shared_context 'group navbar structure' do nav_item: _('Merge requests'), nav_sub_items: [] }, - security_and_compliance_nav_item, + (security_and_compliance_nav_item if Gitlab.ee?), (push_rules_nav_item if Gitlab.ee?), { nav_item: _('Kubernetes'), 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 d638ffcf8fa..de1b46c65ad 100644 --- a/spec/support/shared_contexts/policies/project_policy_shared_context.rb +++ b/spec/support/shared_contexts/policies/project_policy_shared_context.rb @@ -48,7 +48,7 @@ RSpec.shared_context 'ProjectPolicy context' do destroy_container_image push_code read_pod_logs read_terraform_state 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 + update_metrics_dashboard_annotation update_pipeline update_release destroy_release ] end @@ -57,7 +57,7 @@ RSpec.shared_context 'ProjectPolicy context' do add_cluster admin_build admin_commit_status admin_container_image admin_deployment admin_environment admin_note admin_pipeline admin_project admin_project_member admin_snippet admin_terraform_state - admin_wiki create_deploy_token destroy_deploy_token destroy_release + admin_wiki create_deploy_token destroy_deploy_token push_to_delete_protected_branch read_deploy_token update_snippet ] end diff --git a/spec/support/shared_contexts/requests/api/graphql/jira_import/jira_projects_context.rb b/spec/support/shared_contexts/requests/api/graphql/jira_import/jira_projects_context.rb index de40b926a1c..6d34675e8e5 100644 --- a/spec/support/shared_contexts/requests/api/graphql/jira_import/jira_projects_context.rb +++ b/spec/support/shared_contexts/requests/api/graphql/jira_import/jira_projects_context.rb @@ -4,8 +4,8 @@ RSpec.shared_context 'Jira projects request context' do let(:url) { 'https://jira.example.com' } let(:username) { 'jira-username' } let(:password) { 'jira-password' } - let!(:jira_service) do - create(:jira_service, + let!(:jira_integration) do + create(:jira_integration, project: project, url: url, username: username, diff --git a/spec/support/shared_contexts/services/service_ping/stubbed_service_ping_metrics_definitions_shared_context.rb b/spec/support/shared_contexts/services/service_ping/stubbed_service_ping_metrics_definitions_shared_context.rb new file mode 100644 index 00000000000..ea72398010c --- /dev/null +++ b/spec/support/shared_contexts/services/service_ping/stubbed_service_ping_metrics_definitions_shared_context.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +RSpec.shared_context 'stubbed service ping metrics definitions' do + include UsageDataHelpers + + let(:metrics_definitions) { standard_metrics + subscription_metrics + operational_metrics + optional_metrics } + let(:standard_metrics) do + [ + metric_attributes('uuid', "Standard") + ] + end + + let(:operational_metrics) do + [ + metric_attributes('counts.merge_requests', "Operational"), + metric_attributes('counts.todos', "Operational") + ] + end + + let(:optional_metrics) do + [ + metric_attributes('counts.boards', "Optional"), + metric_attributes('gitaly.filesystems', '').except('data_category') + ] + end + + before do + stub_usage_data_connections + stub_object_store_settings + + allow(Gitlab::Usage::MetricDefinition).to( + receive(:definitions) + .and_return(metrics_definitions.to_h { |definition| [definition['key_path'], Gitlab::Usage::MetricDefinition.new('', definition.symbolize_keys)] }) + ) + end + + def metric_attributes(key_path, category) + { + 'key_path' => key_path, + 'data_category' => category + } + end +end diff --git a/spec/support/shared_contexts/unique_ip_check_shared_context.rb b/spec/support/shared_contexts/unique_ip_check_shared_context.rb index f6bedb6cada..8d199df1c10 100644 --- a/spec/support/shared_contexts/unique_ip_check_shared_context.rb +++ b/spec/support/shared_contexts/unique_ip_check_shared_context.rb @@ -5,9 +5,9 @@ RSpec.shared_context 'unique ips sign in limit' do let(:request_context) { Gitlab::RequestContext.instance } before do - Gitlab::Redis::Cache.with(&:flushall) - Gitlab::Redis::Queues.with(&:flushall) - Gitlab::Redis::SharedState.with(&:flushall) + redis_cache_cleanup! + redis_queues_cleanup! + redis_shared_state_cleanup! end before do diff --git a/spec/support/shared_examples/ci/edit_job_token_scope_shared_examples.rb b/spec/support/shared_examples/ci/edit_job_token_scope_shared_examples.rb new file mode 100644 index 00000000000..05b2b5f5de1 --- /dev/null +++ b/spec/support/shared_examples/ci/edit_job_token_scope_shared_examples.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'editable job token scope' do + shared_examples 'returns error' do |error| + it 'returns an error response', :aggregate_failures do + expect(result).to be_error + expect(result.message).to eq(error) + end + end + + context 'when job token scope is disabled for the given project' do + before do + allow(project).to receive(:ci_job_token_scope_enabled?).and_return(false) + end + + it_behaves_like 'returns error', 'Job token scope is disabled for this project' + end + + context 'when user does not have permissions to edit the job token scope' do + it_behaves_like 'returns error', 'Insufficient permissions to modify the job token scope' + end + + context 'when user has permissions to edit the job token scope' do + before do + project.add_maintainer(current_user) + end + + context 'when target project is not provided' do + let(:target_project) { nil } + + it_behaves_like 'returns error', Ci::JobTokenScope::EditScopeValidations::TARGET_PROJECT_UNAUTHORIZED_OR_UNFOUND + end + + context 'when target project is provided' do + context 'when user does not have permissions to read the target project' do + it_behaves_like 'returns error', Ci::JobTokenScope::EditScopeValidations::TARGET_PROJECT_UNAUTHORIZED_OR_UNFOUND + end + end + end +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 index 70a684c12bf..017e55309f7 100644 --- a/spec/support/shared_examples/controllers/access_tokens_controller_shared_examples.rb +++ b/spec/support/shared_examples/controllers/access_tokens_controller_shared_examples.rb @@ -44,11 +44,13 @@ RSpec.shared_examples 'project access tokens available #create' do 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 diff --git a/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb b/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb index 9af35c189d0..e8f7e62d0d7 100644 --- a/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb +++ b/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb @@ -1,10 +1,11 @@ # frozen_string_literal: true RSpec.shared_examples 'wiki controller actions' do + let_it_be(:user) { create(:user) } + let_it_be(:other_user) { create(:user) } + let(:container) { raise NotImplementedError } let(:routing_params) { raise NotImplementedError } - - let_it_be(:user) { create(:user) } let(:wiki) { Wiki.for_container(container, user) } let(:wiki_title) { 'page title test' } @@ -458,6 +459,7 @@ RSpec.shared_examples 'wiki controller actions' do describe 'DELETE #destroy' do let(:id_param) { wiki_title } + let(:delete_user) { user } subject(:request) do delete(:destroy, @@ -466,13 +468,21 @@ RSpec.shared_examples 'wiki controller actions' do )) end + before do + sign_in(delete_user) + end + context 'when page exists' do - it 'deletes the page' do - expect do - request - end.to change { wiki.list_pages.size }.by(-1) + shared_examples 'deletes the page' do + specify do + expect do + request + end.to change { wiki.list_pages.size }.by(-1) + end end + it_behaves_like 'deletes the page' + context 'but page cannot be deleted' do before do allow_next_instance_of(WikiPage) do |page| @@ -489,6 +499,28 @@ RSpec.shared_examples 'wiki controller actions' do expect(assigns(:error)).to eq('Could not delete wiki page') end end + + context 'when user is a developer' do + let(:delete_user) { other_user } + + before do + container.add_developer(other_user) + end + + it_behaves_like 'deletes the page' + end + + context 'when user is a reporter' do + let(:delete_user) { other_user } + + before do + container.add_reporter(other_user) + end + + it 'returns 404' do + is_expected.to have_gitlab_http_status(:not_found) + end + end end context 'when page does not exist' do diff --git a/spec/support/shared_examples/features/cascading_settings_shared_examples.rb b/spec/support/shared_examples/features/cascading_settings_shared_examples.rb index 29ef3da9a85..395f4fc54e0 100644 --- a/spec/support/shared_examples/features/cascading_settings_shared_examples.rb +++ b/spec/support/shared_examples/features/cascading_settings_shared_examples.rb @@ -13,10 +13,22 @@ RSpec.shared_examples 'a cascading setting' do click_save_button end - it 'disables setting in subgroups' do - visit subgroup_path + shared_examples 'subgroup settings are disabled' do + it 'disables setting in subgroups' do + visit subgroup_path + + expect(find("#{setting_field_selector}[disabled]")).to be_checked + end + end + + include_examples 'subgroup settings are disabled' + + context 'when use_traversal_ids_for_ancestors is disabled' do + before do + stub_feature_flags(use_traversal_ids_for_ancestors: false) + end - expect(find("#{setting_field_selector}[disabled]")).to be_checked + include_examples 'subgroup settings are disabled' end it 'does not show enforcement checkbox in subgroups' do diff --git a/spec/support/shared_examples/features/packages_shared_examples.rb b/spec/support/shared_examples/features/packages_shared_examples.rb index 4d2e13aa5bc..9e88db2e1c0 100644 --- a/spec/support/shared_examples/features/packages_shared_examples.rb +++ b/spec/support/shared_examples/features/packages_shared_examples.rb @@ -32,11 +32,9 @@ RSpec.shared_examples 'package details link' do |property| expect(page).to have_current_path(project_package_path(package.project, package)) - page.within('[data-qa-selector="package_title"]') do - expect(page).to have_content(package.name) - end + expect(page).to have_css('.packages-app h1[data-testid="title"]', text: package.name) - page.within('[data-qa-selector="package_information_content"]') do + page.within(%Q([name="#{package.name}"])) do expect(page).to have_content('Installation') expect(page).to have_content('Registry setup') end diff --git a/spec/support/shared_examples/features/search/search_timeouts_shared_examples.rb b/spec/support/shared_examples/features/search/search_timeouts_shared_examples.rb new file mode 100644 index 00000000000..bb5460e2a6f --- /dev/null +++ b/spec/support/shared_examples/features/search/search_timeouts_shared_examples.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'search timeouts' do |scope| + context 'when search times out' do + before do + allow_next_instance_of(SearchService) do |service| + allow(service).to receive(:search_objects).and_raise(ActiveRecord::QueryCanceled) + end + + visit(search_path(search: 'test', scope: scope)) + end + + it 'renders timeout information' do + expect(page).to have_content('Your search timed out') + end + + it 'sets tab count to 0' do + expect(page.find('.search-filter .active')).to have_text('0') + end + 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 c9508818f74..5bfe929e957 100644 --- a/spec/support/shared_examples/features/sidebar_shared_examples.rb +++ b/spec/support/shared_examples/features/sidebar_shared_examples.rb @@ -175,12 +175,4 @@ RSpec.shared_examples 'issue boards sidebar' do end end end - - def refresh_and_click_first_card - page.refresh - - wait_for_requests - - first_card.click - end end diff --git a/spec/support/shared_examples/features/wiki/user_creates_wiki_page_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_creates_wiki_page_shared_examples.rb index f2576931642..dfc9a45bd0d 100644 --- a/spec/support/shared_examples/features/wiki/user_creates_wiki_page_shared_examples.rb +++ b/spec/support/shared_examples/features/wiki/user_creates_wiki_page_shared_examples.rb @@ -20,17 +20,6 @@ RSpec.shared_examples 'User creates wiki page' do click_link "Create your first page" end - it "shows validation error message if the form is force submitted", :js do - page.within(".wiki-form") do - fill_in(:wiki_content, with: "") - - page.execute_script("document.querySelector('.wiki-form').submit()") - page.accept_alert # manually force form submit - end - - expect(page).to have_content("The form contains the following error:").and have_content("Content can't be blank") - end - it "disables the submit button", :js do page.within(".wiki-form") do fill_in(:wiki_content, with: "") diff --git a/spec/support/shared_examples/features/wiki/user_deletes_wiki_page_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_deletes_wiki_page_shared_examples.rb index ee0261771f9..55c89977a99 100644 --- a/spec/support/shared_examples/features/wiki/user_deletes_wiki_page_shared_examples.rb +++ b/spec/support/shared_examples/features/wiki/user_deletes_wiki_page_shared_examples.rb @@ -7,18 +7,34 @@ RSpec.shared_examples 'User deletes wiki page' do include WikiHelpers + let_it_be(:developer) { create(:user) } + let(:wiki_page) { create(:wiki_page, wiki: wiki) } before do + wiki.container.add_developer(developer) + sign_in(user) visit wiki_page_path(wiki, wiki_page) end - it 'deletes a page', :js do - click_on('Edit') - click_on('Delete') - find('[data-testid="confirm_deletion_button"]').click + shared_examples 'deletes a wiki page' do + specify 'deletes a page', :js do + click_on('Edit') + click_on('Delete') + find('[data-testid="confirm_deletion_button"]').click + + expect(page).to have_content('Wiki page was successfully deleted.') + end + end + + context 'when user is the owner or maintainer' do + it_behaves_like 'deletes a wiki page' + end + + context 'when user is a developer' do + let(:user) { developer } - expect(page).to have_content('Wiki page was successfully deleted.') + it_behaves_like 'deletes a wiki page' end end diff --git a/spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb index db2a96d9649..9587da0233e 100644 --- a/spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb +++ b/spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb @@ -90,19 +90,6 @@ RSpec.shared_examples 'User updates wiki page' do expect(page).to have_field('wiki[message]', with: 'Update Wiki title') end - it 'shows a validation error message if the form is force submitted', :js do - fill_in(:wiki_content, with: '') - - page.execute_script("document.querySelector('.wiki-form').submit()") - page.accept_alert # manually force form submit - - expect(page).to have_selector('.wiki-form') - expect(page).to have_content('Edit Page') - expect(page).to have_content('The form contains the following error:') - expect(page).to have_content("Content can't be blank") - expect(find('textarea#wiki_content').value).to eq('') - end - it "disables the submit button", :js do page.within(".wiki-form") do fill_in(:wiki_content, with: "") diff --git a/spec/support/shared_examples/graphql/design_fields_shared_examples.rb b/spec/support/shared_examples/graphql/design_fields_shared_examples.rb index 9c2eb3e5a5c..efbcfaf0e91 100644 --- a/spec/support/shared_examples/graphql/design_fields_shared_examples.rb +++ b/spec/support/shared_examples/graphql/design_fields_shared_examples.rb @@ -27,6 +27,7 @@ RSpec.shared_examples 'a GraphQL type with design fields' do describe '#image' do let_it_be(:current_user) { create(:user) } + let(:schema) { GitlabSchema } let(:query) { GraphQL::Query.new(schema) } let(:context) { query.context } diff --git a/spec/support/shared_examples/graphql/mutations/can_mutate_spammable_examples.rb b/spec/support/shared_examples/graphql/mutations/can_mutate_spammable_examples.rb index 5e15c91cd41..011a2157f24 100644 --- a/spec/support/shared_examples/graphql/mutations/can_mutate_spammable_examples.rb +++ b/spec/support/shared_examples/graphql/mutations/can_mutate_spammable_examples.rb @@ -3,17 +3,13 @@ require 'spec_helper' RSpec.shared_examples 'a mutation which can mutate a spammable' do - describe "#additional_spam_params" do - it 'passes additional spam params to the service' do + describe "#spam_params" do + it 'passes spam params to the service constructor' do args = [ project: anything, current_user: anything, - params: hash_including( - api: true, - request: instance_of(ActionDispatch::Request), - captcha_response: captcha_response, - spam_log_id: spam_log_id - ) + params: anything, + spam_params: instance_of(::Spam::SpamParams) ] expect(service).to receive(:new).with(*args).and_call_original diff --git a/spec/support/shared_examples/graphql/spam_protection_shared_examples.rb b/spec/support/shared_examples/graphql/spam_protection_shared_examples.rb index 8fb89a4f80e..c0b71a494d0 100644 --- a/spec/support/shared_examples/graphql/spam_protection_shared_examples.rb +++ b/spec/support/shared_examples/graphql/spam_protection_shared_examples.rb @@ -57,7 +57,7 @@ RSpec.shared_examples 'has spam protection' do context 'and no CAPTCHA is required' do let(:render_captcha) { false } - it 'does not return a to-level error' do + it 'does not return a top-level error' do send_request expect(graphql_errors).to be_blank diff --git a/spec/support/shared_examples/lib/cache_helpers_shared_examples.rb b/spec/support/shared_examples/lib/cache_helpers_shared_examples.rb new file mode 100644 index 00000000000..845fa78a827 --- /dev/null +++ b/spec/support/shared_examples/lib/cache_helpers_shared_examples.rb @@ -0,0 +1,101 @@ +# frozen_string_literal: true + +RSpec.shared_examples_for 'object cache helper' do + it { is_expected.to be_a(Gitlab::Json::PrecompiledJson) } + + it "uses the presenter" do + expect(presenter).to receive(:represent).with(presentable, project: project) + + subject + end + + it "is valid JSON" do + parsed = Gitlab::Json.parse(subject.to_s) + + expect(parsed).to be_a(Hash) + expect(parsed["id"]).to eq(presentable.id) + end + + it "fetches from the cache" do + expect(instance.cache).to receive(:fetch).with("#{presenter.class.name}:#{presentable.cache_key}:#{user.cache_key}", expires_in: described_class::DEFAULT_EXPIRY).once + + subject + end + + context "when a cache context is supplied" do + before do + kwargs[:cache_context] = -> (item) { item.project.cache_key } + end + + it "uses the context to augment the cache key" do + expect(instance.cache).to receive(:fetch).with("#{presenter.class.name}:#{presentable.cache_key}:#{project.cache_key}", expires_in: described_class::DEFAULT_EXPIRY).once + + subject + end + end + + context "when expires_in is supplied" do + it "sets the expiry when accessing the cache" do + kwargs[:expires_in] = 7.days + + expect(instance.cache).to receive(:fetch).with("#{presenter.class.name}:#{presentable.cache_key}:#{user.cache_key}", expires_in: 7.days).once + + subject + end + end +end + +RSpec.shared_examples_for 'collection cache helper' do + it { is_expected.to be_an(Gitlab::Json::PrecompiledJson) } + + it "uses the presenter" do + presentable.each do |item| + expect(presenter).to receive(:represent).with(item, project: project) + end + + subject + end + + it "is valid JSON" do + parsed = Gitlab::Json.parse(subject.to_s) + + expect(parsed).to be_an(Array) + + presentable.each_with_index do |item, i| + expect(parsed[i]["id"]).to eq(item.id) + end + end + + it "fetches from the cache" do + keys = presentable.map { |item| "#{presenter.class.name}:#{item.cache_key}:#{user.cache_key}" } + + expect(instance.cache).to receive(:fetch_multi).with(*keys, expires_in: described_class::DEFAULT_EXPIRY).once.and_call_original + + subject + end + + context "when a cache context is supplied" do + before do + kwargs[:cache_context] = -> (item) { item.project.cache_key } + end + + it "uses the context to augment the cache key" do + keys = presentable.map { |item| "#{presenter.class.name}:#{item.cache_key}:#{project.cache_key}" } + + expect(instance.cache).to receive(:fetch_multi).with(*keys, expires_in: described_class::DEFAULT_EXPIRY).once.and_call_original + + subject + end + end + + context "expires_in is supplied" do + it "sets the expiry when accessing the cache" do + keys = presentable.map { |item| "#{presenter.class.name}:#{item.cache_key}:#{user.cache_key}" } + kwargs[:expires_in] = 7.days + + expect(instance.cache).to receive(:fetch_multi).with(*keys, expires_in: 7.days).once.and_call_original + + subject + end + end +end diff --git a/spec/support/shared_examples/lib/gitlab/cycle_analytics/event_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/cycle_analytics/event_shared_examples.rb index 7d341d79bae..6e12b5a0e85 100644 --- a/spec/support/shared_examples/lib/gitlab/cycle_analytics/event_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/cycle_analytics/event_shared_examples.rb @@ -3,6 +3,7 @@ RSpec.shared_examples_for 'value stream analytics event' do let(:params) { {} } let(:instance) { described_class.new(params) } + let(:expected_hash_code) { Digest::SHA256.hexdigest(instance.class.identifier.to_s) } it { expect(described_class.name).to be_a_kind_of(String) } it { expect(described_class.identifier).to be_a_kind_of(Symbol) } @@ -19,4 +20,16 @@ RSpec.shared_examples_for 'value stream analytics event' do expect(output_query).to be_a_kind_of(ActiveRecord::Relation) end end + + describe '#hash_code' do + it 'returns a hash that uniquely identifies an event' do + expect(instance.hash_code).to eq(expected_hash_code) + end + + it 'does not differ when the same object is built with the same params' do + another_instance_with_same_params = described_class.new(params) + + expect(another_instance_with_same_params.hash_code).to eq(instance.hash_code) + end + end end diff --git a/spec/support/shared_examples/lib/gitlab/import_export/relation_factory_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/import_export/relation_factory_shared_examples.rb index 33061f17bde..3c5c65f0690 100644 --- a/spec/support/shared_examples/lib/gitlab/import_export/relation_factory_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/import_export/relation_factory_shared_examples.rb @@ -12,7 +12,7 @@ RSpec.shared_examples 'Notes user references' do 'id' => 111, 'access_level' => 30, 'source_id' => 1, - 'source_type' => importable.class.name == 'Project' ? 'Project' : 'Namespace', + 'source_type' => importable.instance_of?(Project) ? 'Project' : 'Namespace', 'user_id' => 3, 'notification_level' => 3, 'created_at' => '2016-11-18T09:29:42.634Z', diff --git a/spec/support/shared_examples/lib/gitlab/kubernetes/network_policy_common_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/kubernetes/network_policy_common_shared_examples.rb index f018ece0d46..2633a89eeee 100644 --- a/spec/support/shared_examples/lib/gitlab/kubernetes/network_policy_common_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/kubernetes/network_policy_common_shared_examples.rb @@ -19,7 +19,8 @@ RSpec.shared_examples 'network policy common specs' do creation_timestamp: nil, manifest: YAML.dump(policy.resource.deep_stringify_keys), is_autodevops: false, - is_enabled: true + is_enabled: true, + environment_ids: [] } end diff --git a/spec/support/shared_examples/lib/gitlab/search_results_sorted_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/search_results_sorted_shared_examples.rb index eafb49cef71..e4f09dfa0b0 100644 --- a/spec/support/shared_examples/lib/gitlab/search_results_sorted_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/search_results_sorted_shared_examples.rb @@ -33,3 +33,21 @@ RSpec.shared_examples 'search results sorted' do end end end + +RSpec.shared_examples 'search results sorted by popularity' do + context 'sort: popularity_desc' do + let(:sort) { 'popularity_desc' } + + it 'sorts results by upvotes' do + expect(results_popular.objects(scope).map(&:id)).to eq([popular_result.id, less_popular_result.id, non_popular_result.id]) + end + end + + context 'sort: popularity_asc' do + let(:sort) { 'popularity_asc' } + + it 'sorts results by created_at' do + expect(results_popular.objects(scope).map(&:id)).to eq([non_popular_result.id, less_popular_result.id, popular_result.id]) + end + end +end diff --git a/spec/support/shared_examples/metrics/active_record_subscriber_shared_examples.rb b/spec/support/shared_examples/metrics/active_record_subscriber_shared_examples.rb index 1b110ab02b5..a84658780b9 100644 --- a/spec/support/shared_examples/metrics/active_record_subscriber_shared_examples.rb +++ b/spec/support/shared_examples/metrics/active_record_subscriber_shared_examples.rb @@ -5,9 +5,10 @@ RSpec.shared_examples 'store ActiveRecord info in RequestStore' do |db_role| 2.times do Gitlab::WithRequestStore.with_request_store do subscriber.sql(event) + connection = event.payload[:connection] if db_role == :primary - expect(described_class.db_counter_payload).to eq( + expected = { db_count: record_query ? 1 : 0, db_write_count: record_write_query ? 1 : 0, db_cached_count: record_cached_query ? 1 : 0, @@ -18,10 +19,13 @@ RSpec.shared_examples 'store ActiveRecord info in RequestStore' do |db_role| db_replica_count: 0, db_replica_duration_s: 0.0, db_primary_wal_count: record_wal_query ? 1 : 0, + db_primary_wal_cached_count: record_wal_query && record_cached_query ? 1 : 0, + db_replica_wal_cached_count: 0, db_replica_wal_count: 0 - ) + } + expected[:"db_primary_#{::Gitlab::Database.dbname(connection)}_duration_s"] = 0.002 if record_query elsif db_role == :replica - expect(described_class.db_counter_payload).to eq( + expected = { db_count: record_query ? 1 : 0, db_write_count: record_write_query ? 1 : 0, db_cached_count: record_cached_query ? 1 : 0, @@ -32,15 +36,35 @@ RSpec.shared_examples 'store ActiveRecord info in RequestStore' do |db_role| db_replica_count: record_query ? 1 : 0, db_replica_duration_s: record_query ? 0.002 : 0, db_replica_wal_count: record_wal_query ? 1 : 0, + db_replica_wal_cached_count: record_wal_query && record_cached_query ? 1 : 0, + db_primary_wal_cached_count: 0, db_primary_wal_count: 0 - ) + } + expected[:"db_replica_#{::Gitlab::Database.dbname(connection)}_duration_s"] = 0.002 if record_query else - expect(described_class.db_counter_payload).to eq( + expected = { db_count: record_query ? 1 : 0, db_write_count: record_write_query ? 1 : 0, db_cached_count: record_cached_query ? 1 : 0 - ) + } end + + expect(described_class.db_counter_payload).to eq(expected) + end + end + end + + context 'when multiple_database_metrics is disabled' do + before do + stub_feature_flags(multiple_database_metrics: false) + end + + it 'does not include per database metrics' do + Gitlab::WithRequestStore.with_request_store do + subscriber.sql(event) + connection = event.payload[:connection] + + expect(described_class.db_counter_payload).not_to include(:"db_replica_#{::Gitlab::Database.dbname(connection)}_duration_s") end end end @@ -71,7 +95,10 @@ RSpec.shared_examples 'record ActiveRecord metrics in a metrics transaction' do end if record_wal_query - expect(transaction).to receive(:increment).with("gitlab_transaction_db_#{db_role}_wal_count_total".to_sym, 1) if db_role + if db_role + expect(transaction).to receive(:increment).with("gitlab_transaction_db_#{db_role}_wal_count_total".to_sym, 1) + expect(transaction).to receive(:increment).with("gitlab_transaction_db_#{db_role}_wal_cached_count_total".to_sym, 1) if record_cached_query + end else expect(transaction).not_to receive(:increment).with("gitlab_transaction_db_#{db_role}_wal_count_total".to_sym, 1) if db_role end diff --git a/spec/support/shared_examples/models/atomic_internal_id_shared_examples.rb b/spec/support/shared_examples/models/atomic_internal_id_shared_examples.rb index 42f82987989..03f565e0aac 100644 --- a/spec/support/shared_examples/models/atomic_internal_id_shared_examples.rb +++ b/spec/support/shared_examples/models/atomic_internal_id_shared_examples.rb @@ -165,9 +165,9 @@ RSpec.shared_examples 'AtomicInternalId' do |validate_presence: true| 3.times { supply.next_value } end - current_value = described_class.public_send(method_name, scope_value, &:current_value) - - expect(current_value).to eq(iid + 3) + described_class.public_send(method_name, scope_value) do |supply| + expect(supply.next_value).to eq(iid + 4) + end end end diff --git a/spec/support/shared_examples/models/chat_integration_shared_examples.rb b/spec/support/shared_examples/models/chat_integration_shared_examples.rb index 9f3be3e2e06..72659dd5f3b 100644 --- a/spec/support/shared_examples/models/chat_integration_shared_examples.rb +++ b/spec/support/shared_examples/models/chat_integration_shared_examples.rb @@ -13,7 +13,7 @@ RSpec.shared_examples "chat integration" do |integration_name| end it { is_expected.to validate_presence_of(:webhook) } - it_behaves_like "issue tracker service URL attribute", :webhook + it_behaves_like "issue tracker integration URL attribute", :webhook end context "when integration is inactive" do @@ -163,7 +163,7 @@ RSpec.shared_examples "chat integration" do |integration_name| context "with issue events" do let(:opts) { { title: "Awesome issue", description: "please fix" } } let(:sample_data) do - service = Issues::CreateService.new(project: project, current_user: user, params: opts) + service = Issues::CreateService.new(project: project, current_user: user, params: opts, spam_params: nil) issue = service.execute service.hook_data(issue, "open") end diff --git a/spec/support/shared_examples/models/concerns/integrations/slack_mattermost_notifier_shared_examples.rb b/spec/support/shared_examples/models/concerns/integrations/slack_mattermost_notifier_shared_examples.rb index 66448aca2c5..2d4c0b60f2b 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 @@ -8,7 +8,7 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |service_name| def execute_with_options(options) receive(:new).with(webhook_url, options.merge(http_client: Integrations::SlackMattermostNotifier::HTTPClient)) - .and_return(double(:slack_service).as_null_object) + .and_return(double(:slack_integration).as_null_object) end describe "Associations" do @@ -23,7 +23,7 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |service_name| end it { is_expected.to validate_presence_of(:webhook) } - it_behaves_like 'issue tracker service URL attribute', :webhook + it_behaves_like 'issue tracker integration URL attribute', :webhook end context 'when service is inactive' do diff --git a/spec/support/shared_examples/models/cycle_analytics_stage_shared_examples.rb b/spec/support/shared_examples/models/cycle_analytics_stage_shared_examples.rb index d23f95b2e9e..cf38a583944 100644 --- a/spec/support/shared_examples/models/cycle_analytics_stage_shared_examples.rb +++ b/spec/support/shared_examples/models/cycle_analytics_stage_shared_examples.rb @@ -122,6 +122,22 @@ RSpec.shared_examples 'value stream analytics stage' do expect(stage.parent_id).to eq(parent.id) end end + + describe '#hash_code' do + it 'does not differ when the same object is built with the same params' do + stage_1 = build(factory) + stage_2 = build(factory) + + expect(stage_1.events_hash_code).to eq(stage_2.events_hash_code) + end + + it 'differs when the stage events are different' do + stage_1 = build(factory, start_event_identifier: :merge_request_created, end_event_identifier: :merge_request_merged) + stage_2 = build(factory, start_event_identifier: :issue_created, end_event_identifier: :issue_first_mentioned_in_commit) + + expect(stage_1.events_hash_code).not_to eq(stage_2.events_hash_code) + end + end end RSpec.shared_examples 'value stream analytics label based stage' do diff --git a/spec/support/shared_examples/models/integrations/base_slash_commands_shared_examples.rb b/spec/support/shared_examples/models/integrations/base_slash_commands_shared_examples.rb index 128999d02fa..e35ac9c0d0d 100644 --- a/spec/support/shared_examples/models/integrations/base_slash_commands_shared_examples.rb +++ b/spec/support/shared_examples/models/integrations/base_slash_commands_shared_examples.rb @@ -66,14 +66,14 @@ RSpec.shared_examples Integrations::BaseSlashCommands do } end - let(:service) do - project.create_mattermost_slash_commands_service( + let(:integration) do + project.create_mattermost_slash_commands_integration( properties: { token: 'token' } ) end it 'generates the url' do - response = service.trigger(params) + response = integration.trigger(params) expect(response[:text]).to start_with(':wave: Hi there!') end diff --git a/spec/support/shared_examples/models/integrations/has_web_hook_shared_examples.rb b/spec/support/shared_examples/models/integrations/has_web_hook_shared_examples.rb new file mode 100644 index 00000000000..1fa340a0cf4 --- /dev/null +++ b/spec/support/shared_examples/models/integrations/has_web_hook_shared_examples.rb @@ -0,0 +1,97 @@ +# frozen_string_literal: true + +RSpec.shared_examples Integrations::HasWebHook do + include AfterNextHelpers + + describe 'callbacks' do + it 'calls #update_web_hook! when enabled' do + expect(integration).to receive(:update_web_hook!) + + integration.active = true + integration.save! + end + + it 'does not call #update_web_hook! when disabled' do + expect(integration).not_to receive(:update_web_hook!) + + integration.active = false + integration.save! + end + + it 'does not call #update_web_hook! when validation fails' do + expect(integration).not_to receive(:update_web_hook!) + + integration.active = true + integration.project = nil + expect(integration.save).to be(false) + end + end + + describe '#hook_url' do + it 'returns a string' do + expect(integration.hook_url).to be_a(String) + end + end + + describe '#hook_ssl_verification' do + it 'returns a boolean' do + expect(integration.hook_ssl_verification).to be_in([true, false]) + end + end + + describe '#update_web_hook!' do + def call + integration.update_web_hook! + end + + it 'creates or updates a service hook' do + expect { call }.to change(ServiceHook, :count).by(1) + expect(integration.service_hook.url).to eq(hook_url) + + integration.service_hook.update!(url: 'http://other.com') + + expect { call }.to change { integration.service_hook.reload.url }.from('http://other.com').to(hook_url) + end + + it 'raises an error if the service hook could not be saved' do + call + integration.service_hook.integration = nil + + expect { call }.to raise_error(ActiveRecord::RecordInvalid) + end + + it 'does not attempt to save the service hook if there are no changes' do + call + + expect(integration.service_hook).not_to receive(:save!) + + call + end + end + + describe '#execute_web_hook!' do + let(:args) { ['foo', [1, 2, 3]] } + + def call + integration.execute_web_hook!(*args) + end + + it 'creates the webhook if necessary and executes it' do + expect_next(ServiceHook).to receive(:execute).with(*args) + expect { call }.to change(ServiceHook, :count).by(1) + + expect(integration.service_hook).to receive(:execute).with(*args) + expect { call }.not_to change(ServiceHook, :count) + end + + it 'raises an error if the service hook could not be saved' do + expect_next(ServiceHook).to receive(:execute).with(*args) + + call + integration.service_hook.integration = nil + + expect(integration.service_hook).not_to receive(:execute) + expect { call }.to raise_error(ActiveRecord::RecordInvalid) + end + end +end diff --git a/spec/support/shared_examples/models/issue_tracker_service_shared_examples.rb b/spec/support/shared_examples/models/issue_tracker_service_shared_examples.rb index b275d594792..6d519e561ee 100644 --- a/spec/support/shared_examples/models/issue_tracker_service_shared_examples.rb +++ b/spec/support/shared_examples/models/issue_tracker_service_shared_examples.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -RSpec.shared_examples 'issue tracker service URL attribute' do |url_attr| +RSpec.shared_examples 'issue tracker integration URL attribute' do |url_attr| it { is_expected.to allow_value('https://example.com').for(url_attr) } it { is_expected.not_to allow_value('example.com').for(url_attr) } diff --git a/spec/support/shared_examples/models/member_shared_examples.rb b/spec/support/shared_examples/models/member_shared_examples.rb index 7ede6f0d8d4..c111d250d34 100644 --- a/spec/support/shared_examples/models/member_shared_examples.rb +++ b/spec/support/shared_examples/models/member_shared_examples.rb @@ -75,3 +75,259 @@ RSpec.shared_examples '#valid_level_roles' do |entity_name| expect(presenter.valid_level_roles).to eq(expected_roles) end end + +RSpec.shared_examples_for "member creation" do + let_it_be(:user) { create(:user) } + let_it_be(:admin) { create(:admin) } + + describe '#execute' do + it 'returns a Member object', :aggregate_failures do + member = described_class.new(source, user, :maintainer).execute + + expect(member).to be_a member_type + expect(member).to be_persisted + end + + context 'when admin mode is enabled', :enable_admin_mode do + it 'sets members.created_by to the given admin current_user' do + member = described_class.new(source, user, :maintainer, current_user: admin).execute + + expect(member.created_by).to eq(admin) + end + end + + context 'when admin mode is disabled' do + it 'rejects setting members.created_by to the given admin current_user' do + member = described_class.new(source, user, :maintainer, current_user: admin).execute + + expect(member.created_by).to be_nil + end + end + + it 'sets members.expires_at to the given expires_at' do + member = described_class.new(source, user, :maintainer, expires_at: Date.new(2016, 9, 22)).execute + + expect(member.expires_at).to eq(Date.new(2016, 9, 22)) + end + + described_class.access_levels.each do |sym_key, int_access_level| + it "accepts the :#{sym_key} symbol as access level", :aggregate_failures do + expect(source.users).not_to include(user) + + member = described_class.new(source, user.id, sym_key).execute + + expect(member.access_level).to eq(int_access_level) + expect(source.users.reload).to include(user) + end + + it "accepts the #{int_access_level} integer as access level", :aggregate_failures do + expect(source.users).not_to include(user) + + member = described_class.new(source, user.id, int_access_level).execute + + expect(member.access_level).to eq(int_access_level) + expect(source.users.reload).to include(user) + end + end + + context 'with no current_user' do + context 'when called with a known user id' do + it 'adds the user as a member' do + expect(source.users).not_to include(user) + + described_class.new(source, user.id, :maintainer).execute + + expect(source.users.reload).to include(user) + end + end + + context 'when called with an unknown user id' do + it 'adds the user as a member' do + expect(source.users).not_to include(user) + + described_class.new(source, non_existing_record_id, :maintainer).execute + + expect(source.users.reload).not_to include(user) + end + end + + context 'when called with a user object' do + it 'adds the user as a member' do + expect(source.users).not_to include(user) + + described_class.new(source, user, :maintainer).execute + + expect(source.users.reload).to include(user) + end + end + + context 'when called with a requester user object' do + before do + source.request_access(user) + end + + it 'adds the requester as a member', :aggregate_failures do + expect(source.users).not_to include(user) + expect(source.requesters.exists?(user_id: user)).to be_truthy + + expect do + described_class.new(source, user, :maintainer).execute + end.to raise_error(Gitlab::Access::AccessDeniedError) + + expect(source.users.reload).not_to include(user) + expect(source.requesters.reload.exists?(user_id: user)).to be_truthy + end + end + + context 'when called with a known user email' do + it 'adds the user as a member' do + expect(source.users).not_to include(user) + + described_class.new(source, user.email, :maintainer).execute + + expect(source.users.reload).to include(user) + end + end + + context 'when called with an unknown user email' do + it 'creates an invited member' do + expect(source.users).not_to include(user) + + described_class.new(source, 'user@example.com', :maintainer).execute + + expect(source.members.invite.pluck(:invite_email)).to include('user@example.com') + end + end + + context 'when called with an unknown user email starting with a number' do + it 'creates an invited member', :aggregate_failures do + email_starting_with_number = "#{user.id}_email@example.com" + + described_class.new(source, email_starting_with_number, :maintainer).execute + + expect(source.members.invite.pluck(:invite_email)).to include(email_starting_with_number) + expect(source.users.reload).not_to include(user) + end + end + end + + context 'when current_user can update member', :enable_admin_mode do + it 'creates the member' do + expect(source.users).not_to include(user) + + described_class.new(source, user, :maintainer, current_user: admin).execute + + expect(source.users.reload).to include(user) + end + + context 'when called with a requester user object' do + before do + source.request_access(user) + end + + it 'adds the requester as a member', :aggregate_failures do + expect(source.users).not_to include(user) + expect(source.requesters.exists?(user_id: user)).to be_truthy + + described_class.new(source, user, :maintainer, current_user: admin).execute + + expect(source.users.reload).to include(user) + expect(source.requesters.reload.exists?(user_id: user)).to be_falsy + end + end + end + + context 'when current_user cannot update member' do + it 'does not create the member', :aggregate_failures do + expect(source.users).not_to include(user) + + member = described_class.new(source, user, :maintainer, current_user: user).execute + + expect(source.users.reload).not_to include(user) + expect(member).not_to be_persisted + end + + context 'when called with a requester user object' do + before do + source.request_access(user) + end + + it 'does not destroy the requester', :aggregate_failures do + expect(source.users).not_to include(user) + expect(source.requesters.exists?(user_id: user)).to be_truthy + + described_class.new(source, user, :maintainer, current_user: user).execute + + expect(source.users.reload).not_to include(user) + expect(source.requesters.exists?(user_id: user)).to be_truthy + end + end + end + + context 'when member already exists' do + before do + source.add_user(user, :developer) + end + + context 'with no current_user' do + it 'updates the member' do + expect(source.users).to include(user) + + described_class.new(source, user, :maintainer).execute + + expect(source.members.find_by(user_id: user).access_level).to eq(Gitlab::Access::MAINTAINER) + end + end + + context 'when current_user can update member', :enable_admin_mode do + it 'updates the member' do + expect(source.users).to include(user) + + described_class.new(source, user, :maintainer, current_user: admin).execute + + expect(source.members.find_by(user_id: user).access_level).to eq(Gitlab::Access::MAINTAINER) + end + end + + context 'when current_user cannot update member' do + it 'does not update the member' do + expect(source.users).to include(user) + + described_class.new(source, user, :maintainer, current_user: user).execute + + expect(source.members.find_by(user_id: user).access_level).to eq(Gitlab::Access::DEVELOPER) + end + end + end + end + + describe '.add_users' do + let_it_be(:user1) { create(:user) } + let_it_be(:user2) { create(:user) } + + it 'returns a Member objects' do + members = described_class.add_users(source, [user1, user2], :maintainer) + + expect(members).to be_a Array + expect(members.size).to eq(2) + expect(members.first).to be_a member_type + expect(members.first).to be_persisted + end + + it 'returns an empty array' do + members = described_class.add_users(source, [], :maintainer) + + expect(members).to be_a Array + expect(members).to be_empty + end + + it 'supports different formats' do + list = ['joe@local.test', admin, user1.id, user2.id.to_s] + + members = described_class.add_users(source, list, :maintainer) + + expect(members.size).to eq(4) + expect(members.first).to be_invite + end + end +end diff --git a/spec/support/shared_examples/models/project_ci_cd_settings_shared_examples.rb b/spec/support/shared_examples/models/project_ci_cd_settings_shared_examples.rb new file mode 100644 index 00000000000..c92e819db19 --- /dev/null +++ b/spec/support/shared_examples/models/project_ci_cd_settings_shared_examples.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'ci_cd_settings delegation' do + let(:exclude_attributes) { [] } + + context 'when ci_cd_settings is destroyed but project is not' do + it 'allows methods delegated to ci_cd_settings to be nil', :aggregate_failures do + project = create(:project) + attributes = project.ci_cd_settings.attributes.keys - %w(id project_id) - exclude_attributes + project.ci_cd_settings.destroy! + project.reload + attributes.each do |attr| + method = project.respond_to?("ci_#{attr}") ? "ci_#{attr}" : attr + expect(project.send(method)).to be_nil, "#{attr} was not nil" + end + end + end +end + +RSpec.shared_examples 'a ci_cd_settings predicate method' do |prefix: ''| + using RSpec::Parameterized::TableSyntax + + let_it_be(:project) { create(:project) } + + context 'when ci_cd_settings is nil' do + before do + allow(project).to receive(:ci_cd_settings).and_return(nil) + end + + it 'returns false' do + expect(project.send("#{prefix}#{delegated_method}")).to be(false) + end + end + + context 'when ci_cd_settings is not nil' do + where(:delegated_method_return, :subject_return) do + true | true + false | false + end + + with_them do + let(:ci_cd_settings_double) { double('ProjectCiCdSetting') } + + before do + allow(project).to receive(:ci_cd_settings).and_return(ci_cd_settings_double) + allow(ci_cd_settings_double).to receive(delegated_method).and_return(delegated_method_return) + end + + it 'returns the expected boolean value' do + expect(project.send("#{prefix}#{delegated_method}")).to be(subject_return) + end + end + end +end diff --git a/spec/support/shared_examples/models/wiki_shared_examples.rb b/spec/support/shared_examples/models/wiki_shared_examples.rb index 2498bf35a09..bc5956e3eec 100644 --- a/spec/support/shared_examples/models/wiki_shared_examples.rb +++ b/spec/support/shared_examples/models/wiki_shared_examples.rb @@ -2,6 +2,7 @@ RSpec.shared_examples 'wiki model' do let_it_be(:user) { create(:user, :commit_email) } + let(:wiki_container) { raise NotImplementedError } let(:wiki_container_without_repo) { raise NotImplementedError } let(:wiki_lfs_enabled) { false } @@ -536,4 +537,98 @@ RSpec.shared_examples 'wiki model' do expect(subject.hook_attrs.keys).to contain_exactly(:web_url, :git_ssh_url, :git_http_url, :path_with_namespace, :default_branch) end end + + describe '#default_branch' do + subject { wiki.default_branch } + + before do + allow(Gitlab::DefaultBranch).to receive(:value).and_return('main') + end + + context 'when repository is not created' do + let(:wiki_container) { wiki_container_without_repo } + + it 'returns the instance default branch' do + expect(subject).to eq 'main' + end + end + + context 'when repository is empty' do + let(:wiki_container) { wiki_container_without_repo } + + before do + wiki.repository.create_if_not_exists + end + + it 'returns the instance default branch' do + expect(subject).to eq 'main' + end + end + + context 'when repository is not empty' do + it 'returns the repository default branch' do + wiki.create_page('index', 'test content') + + expect(subject).to eq wiki.repository.root_ref + end + end + end + + describe '#create_wiki_repository' do + let(:head_path) { Rails.root.join(TestEnv.repos_path, "#{wiki.disk_path}.git", 'HEAD') } + let(:default_branch) { 'foo' } + + before do + allow(Gitlab::CurrentSettings).to receive(:default_branch_name).and_return(default_branch) + end + + subject { wiki.create_wiki_repository } + + context 'when repository is not created' do + let(:wiki_container) { wiki_container_without_repo } + + it 'changes the HEAD reference to the default branch' do + expect(wiki.empty?).to eq true + + subject + + expect(File.read(head_path).squish).to eq "ref: refs/heads/#{default_branch}" + end + end + + context 'when repository is empty' do + let(:wiki_container) { wiki_container_without_repo } + + it 'changes the HEAD reference to the default branch' do + wiki.repository.create_if_not_exists + wiki.repository.raw_repository.write_ref('HEAD', 'refs/heads/bar') + + subject + + expect(File.read(head_path).squish).to eq "ref: refs/heads/#{default_branch}" + end + end + + context 'when repository is not empty' do + before do + wiki.create_page('index', 'test content') + end + + it 'does nothing when HEAD points to the right branch' do + expect(wiki.repository.raw_repository).not_to receive(:write_ref) + + subject + end + + context 'when HEAD points to the wrong branch' do + it 'rewrites HEAD with the right branch' do + wiki.repository.raw_repository.write_ref('HEAD', 'refs/heads/bar') + + subject + + expect(File.read(head_path).squish).to eq "ref: refs/heads/#{default_branch}" + end + end + end + end end diff --git a/spec/support/shared_examples/namespaces/traversal_examples.rb b/spec/support/shared_examples/namespaces/traversal_examples.rb index ccc64c80fd4..f09634556c3 100644 --- a/spec/support/shared_examples/namespaces/traversal_examples.rb +++ b/spec/support/shared_examples/namespaces/traversal_examples.rb @@ -12,16 +12,18 @@ RSpec.shared_examples 'namespace traversal' do it "makes a recursive query" do groups.each do |group| - expect { group.public_send(recursive_method).load }.to make_queries_matching(/WITH RECURSIVE/) + expect { group.public_send(recursive_method).try(:load) }.to make_queries_matching(/WITH RECURSIVE/) end end end - describe '#root_ancestor' do - let_it_be(:group) { create(:group) } - let_it_be(:nested_group) { create(:group, parent: group) } - let_it_be(:deep_nested_group) { create(:group, parent: nested_group) } + let_it_be(:group) { create(:group) } + let_it_be(:nested_group) { create(:group, parent: group) } + let_it_be(:deep_nested_group) { create(:group, parent: nested_group) } + let_it_be(:very_deep_nested_group) { create(:group, parent: deep_nested_group) } + let_it_be(:groups) { [group, nested_group, deep_nested_group, very_deep_nested_group] } + describe '#root_ancestor' do it 'returns the correct root ancestor' do expect(group.root_ancestor).to eq(group) expect(nested_group.root_ancestor).to eq(group) @@ -29,8 +31,6 @@ RSpec.shared_examples 'namespace traversal' do end describe '#recursive_root_ancestor' do - let(:groups) { [group, nested_group, deep_nested_group] } - it "is equivalent to #recursive_root_ancestor" do groups.each do |group| expect(group.root_ancestor).to eq(group.recursive_root_ancestor) @@ -40,12 +40,8 @@ RSpec.shared_examples 'namespace traversal' do end describe '#self_and_hierarchy' do - let!(:group) { create(:group, path: 'git_lab') } - let!(:nested_group) { create(:group, parent: group) } - let!(:deep_nested_group) { create(:group, parent: nested_group) } - let!(:very_deep_nested_group) { create(:group, parent: deep_nested_group) } - let!(:another_group) { create(:group, path: 'gitllab') } - let!(:another_group_nested) { create(:group, path: 'foo', parent: another_group) } + let!(:another_group) { create(:group) } + let!(:another_group_nested) { create(:group, parent: another_group) } it 'returns the correct tree' do expect(group.self_and_hierarchy).to contain_exactly(group, nested_group, deep_nested_group, very_deep_nested_group) @@ -54,18 +50,11 @@ RSpec.shared_examples 'namespace traversal' do end describe '#recursive_self_and_hierarchy' do - let(:groups) { [group, nested_group, very_deep_nested_group] } - it_behaves_like 'recursive version', :self_and_hierarchy end end describe '#ancestors' do - let_it_be(:group) { create(:group) } - let_it_be(:nested_group) { create(:group, parent: group) } - let_it_be(:deep_nested_group) { create(:group, parent: nested_group) } - let_it_be(:very_deep_nested_group) { create(:group, parent: deep_nested_group) } - it 'returns the correct ancestors' do # #reload is called to make sure traversal_ids are reloaded expect(very_deep_nested_group.reload.ancestors).to contain_exactly(group, nested_group, deep_nested_group) @@ -75,18 +64,28 @@ RSpec.shared_examples 'namespace traversal' do end describe '#recursive_ancestors' do - let(:groups) { [nested_group, deep_nested_group, very_deep_nested_group] } + let_it_be(:groups) { [nested_group, deep_nested_group, very_deep_nested_group] } it_behaves_like 'recursive version', :ancestors end end - describe '#self_and_ancestors' do - let(:group) { create(:group) } - let(:nested_group) { create(:group, parent: group) } - let(:deep_nested_group) { create(:group, parent: nested_group) } - let(:very_deep_nested_group) { create(:group, parent: deep_nested_group) } + describe '#ancestor_ids' do + it 'returns the correct ancestor ids' do + expect(very_deep_nested_group.ancestor_ids).to contain_exactly(group.id, nested_group.id, deep_nested_group.id) + expect(deep_nested_group.ancestor_ids).to contain_exactly(group.id, nested_group.id) + expect(nested_group.ancestor_ids).to contain_exactly(group.id) + expect(group.ancestor_ids).to be_empty + end + + describe '#recursive_ancestor_ids' do + let_it_be(:groups) { [nested_group, deep_nested_group, very_deep_nested_group] } + + it_behaves_like 'recursive version', :ancestor_ids + end + end + describe '#self_and_ancestors' do it 'returns the correct ancestors' do expect(very_deep_nested_group.self_and_ancestors).to contain_exactly(group, nested_group, deep_nested_group, very_deep_nested_group) expect(deep_nested_group.self_and_ancestors).to contain_exactly(group, nested_group, deep_nested_group) @@ -95,19 +94,30 @@ RSpec.shared_examples 'namespace traversal' do end describe '#recursive_self_and_ancestors' do - let(:groups) { [nested_group, deep_nested_group, very_deep_nested_group] } + let_it_be(:groups) { [nested_group, deep_nested_group, very_deep_nested_group] } it_behaves_like 'recursive version', :self_and_ancestors end end + describe '#self_and_ancestor_ids' do + it 'returns the correct ancestor ids' do + expect(very_deep_nested_group.self_and_ancestor_ids).to contain_exactly(group.id, nested_group.id, deep_nested_group.id, very_deep_nested_group.id) + expect(deep_nested_group.self_and_ancestor_ids).to contain_exactly(group.id, nested_group.id, deep_nested_group.id) + expect(nested_group.self_and_ancestor_ids).to contain_exactly(group.id, nested_group.id) + expect(group.self_and_ancestor_ids).to contain_exactly(group.id) + end + + describe '#recursive_self_and_ancestor_ids' do + let_it_be(:groups) { [nested_group, deep_nested_group, very_deep_nested_group] } + + it_behaves_like 'recursive version', :self_and_ancestor_ids + end + end + describe '#descendants' do - let!(:group) { create(:group, path: 'git_lab') } - let!(:nested_group) { create(:group, parent: group) } - let!(:deep_nested_group) { create(:group, parent: nested_group) } - let!(:very_deep_nested_group) { create(:group, parent: deep_nested_group) } - let!(:another_group) { create(:group, path: 'gitllab') } - let!(:another_group_nested) { create(:group, path: 'foo', parent: another_group) } + let!(:another_group) { create(:group) } + let!(:another_group_nested) { create(:group, parent: another_group) } it 'returns the correct descendants' do expect(very_deep_nested_group.descendants.to_a).to eq([]) @@ -117,19 +127,13 @@ RSpec.shared_examples 'namespace traversal' do end describe '#recursive_descendants' do - let(:groups) { [group, nested_group, deep_nested_group, very_deep_nested_group] } - it_behaves_like 'recursive version', :descendants end end describe '#self_and_descendants' do - let!(:group) { create(:group, path: 'git_lab') } - let!(:nested_group) { create(:group, parent: group) } - let!(:deep_nested_group) { create(:group, parent: nested_group) } - let!(:very_deep_nested_group) { create(:group, parent: deep_nested_group) } - let!(:another_group) { create(:group, path: 'gitllab') } - let!(:another_group_nested) { create(:group, path: 'foo', parent: another_group) } + let!(:another_group) { create(:group) } + let!(:another_group_nested) { create(:group, parent: another_group) } it 'returns the correct descendants' do expect(very_deep_nested_group.self_and_descendants).to contain_exactly(very_deep_nested_group) @@ -139,24 +143,18 @@ RSpec.shared_examples 'namespace traversal' do end describe '#recursive_self_and_descendants' do - let(:groups) { [group, nested_group, deep_nested_group, very_deep_nested_group] } + let_it_be(:groups) { [group, nested_group, deep_nested_group] } it_behaves_like 'recursive version', :self_and_descendants end end describe '#self_and_descendant_ids' do - let!(:group) { create(:group, path: 'git_lab') } - let!(:nested_group) { create(:group, parent: group) } - let!(:deep_nested_group) { create(:group, parent: nested_group) } - subject { group.self_and_descendant_ids.pluck(:id) } - it { is_expected.to contain_exactly(group.id, nested_group.id, deep_nested_group.id) } + it { is_expected.to contain_exactly(group.id, nested_group.id, deep_nested_group.id, very_deep_nested_group.id) } describe '#recursive_self_and_descendant_ids' do - let(:groups) { [group, nested_group, deep_nested_group] } - it_behaves_like 'recursive version', :self_and_descendant_ids end end diff --git a/spec/support/shared_examples/quick_actions/issuable/issuable_quick_actions_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/issuable_quick_actions_shared_examples.rb index 3cdba315d1f..4d142199c95 100644 --- a/spec/support/shared_examples/quick_actions/issuable/issuable_quick_actions_shared_examples.rb +++ b/spec/support/shared_examples/quick_actions/issuable/issuable_quick_actions_shared_examples.rb @@ -233,6 +233,7 @@ RSpec.shared_examples 'issuable quick actions' do context 'when user can update issuable' do let_it_be(:developer) { create(:user) } + let(:note_author) { developer } before do @@ -260,6 +261,7 @@ RSpec.shared_examples 'issuable quick actions' do context 'when user cannot update issuable' do let_it_be(:non_member) { create(:user) } + let(:note_author) { non_member } it 'applies commands that user can execute' do diff --git a/spec/support/shared_examples/quick_actions/issuable/time_tracking_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/time_tracking_quick_action_shared_examples.rb index 92849ddf1cb..052fd0622d0 100644 --- a/spec/support/shared_examples/quick_actions/issuable/time_tracking_quick_action_shared_examples.rb +++ b/spec/support/shared_examples/quick_actions/issuable/time_tracking_quick_action_shared_examples.rb @@ -72,6 +72,24 @@ RSpec.shared_examples 'issuable time tracker' do |issuable_type| end end + it 'shows the time tracking report when link is clicked' do + submit_time('/estimate 1w') + submit_time('/spend 1d') + + wait_for_requests + + page.within '.time-tracking-component-wrap' do + click_link 'Time tracking report' + + wait_for_requests + end + + page.within '#time-tracking-report' do + expect(find('tbody')).to have_content maintainer.name + expect(find('tbody')).to have_content '1d' + end + end + it 'hides the help state when close icon is clicked' do page.within '.time-tracking-component-wrap' do find('.help-button').click 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 0530aa8c760..1f68dd7a382 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 @@ -12,15 +12,17 @@ RSpec.shared_context 'Debian repository shared context' do |container_type, can_ let_it_be(:user, freeze: true) { create(:user) } let_it_be(:personal_access_token, freeze: true) { create(:personal_access_token, user: user) } - let_it_be(:private_distribution, freeze: true) { create("debian_#{container_type}_distribution", container: private_container, codename: 'existing-codename') } + let_it_be(:private_distribution, freeze: true) { create("debian_#{container_type}_distribution", :with_file, container: private_container, codename: 'existing-codename') } let_it_be(:private_component, freeze: true) { create("debian_#{container_type}_component", distribution: private_distribution, name: 'existing-component') } let_it_be(:private_architecture_all, freeze: true) { create("debian_#{container_type}_architecture", distribution: private_distribution, name: 'all') } let_it_be(:private_architecture, freeze: true) { create("debian_#{container_type}_architecture", distribution: private_distribution, name: 'existing-arch') } + let_it_be(:private_component_file) { create("debian_#{container_type}_component_file", component: private_component, architecture: private_architecture) } - let_it_be(:public_distribution, freeze: true) { create("debian_#{container_type}_distribution", container: public_container, codename: 'existing-codename') } + let_it_be(:public_distribution, freeze: true) { create("debian_#{container_type}_distribution", :with_file, container: public_container, codename: 'existing-codename') } let_it_be(:public_component, freeze: true) { create("debian_#{container_type}_component", distribution: public_distribution, name: 'existing-component') } let_it_be(:public_architecture_all, freeze: true) { create("debian_#{container_type}_architecture", distribution: public_distribution, name: 'all') } let_it_be(:public_architecture, freeze: true) { create("debian_#{container_type}_architecture", distribution: public_distribution, name: 'existing-arch') } + let_it_be(:public_component_file) { create("debian_#{container_type}_component_file", component: public_component, architecture: public_architecture) } if container_type == :group let_it_be(:private_project) { create(:project, :private, group: private_container) } @@ -40,14 +42,15 @@ RSpec.shared_context 'Debian repository shared context' do |container_type, can_ let(:visibility_level) { :public } let(:distribution) { { private: private_distribution, public: public_distribution }[visibility_level] } + let(:architecture) { { private: private_architecture, public: public_architecture }[visibility_level] } + let(:component) { { private: private_component, public: public_component }[visibility_level] } + let(:component_file) { { private: private_component_file, public: public_component_file }[visibility_level] } - let(:component) { 'main' } - let(:architecture) { 'amd64' } let(:source_package) { 'sample' } let(:letter) { source_package[0..2] == 'lib' ? source_package[0..3] : source_package[0] } let(:package_name) { 'libsample0' } let(:package_version) { '1.2.3~alpha2' } - let(:file_name) { "#{package_name}_#{package_version}_#{architecture}.deb" } + let(:file_name) { "#{package_name}_#{package_version}_#{architecture.name}.deb" } let(:method) { :get } diff --git a/spec/support/shared_examples/requests/api/graphql/noteable_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/noteable_shared_examples.rb index 9cf5bc04f65..7e1f4500779 100644 --- a/spec/support/shared_examples/requests/api/graphql/noteable_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/graphql/noteable_shared_examples.rb @@ -31,6 +31,28 @@ RSpec.shared_examples 'a noteable graphql type we can query' do expect(graphql_data_at(*path_to_noteable, :discussions, :nodes)) .to match_array(expected) end + + it 'can fetch discussion noteable' do + create(discussion_factory, project: project, noteable: noteable) + fields = + <<-QL.strip_heredoc + discussions { + nodes { + noteable { + __typename + ... on #{noteable.class.name.demodulize} { + id + } + } + } + } + QL + + post_graphql(query(fields), current_user: current_user) + + data = graphql_data_at(*path_to_noteable, :discussions, :nodes, :noteable, :id) + expect(data[0]).to eq(global_id_of(noteable)) + end end describe '.notes' do diff --git a/spec/support/shared_examples/requests/api/helm_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/helm_packages_shared_examples.rb index 585c4fb8a4e..1ad38a17f9c 100644 --- a/spec/support/shared_examples/requests/api/helm_packages_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/helm_packages_shared_examples.rb @@ -1,9 +1,9 @@ # frozen_string_literal: true -RSpec.shared_examples 'rejects helm packages access' do |user_type, status, add_member = true| +RSpec.shared_examples 'rejects helm packages access' do |user_type, status| context "for user type #{user_type}" do before do - project.send("add_#{user_type}", user) if add_member && user_type != :anonymous + project.send("add_#{user_type}", user) if user_type != :anonymous && user_type != :not_a_member end it_behaves_like 'returning response status', status @@ -18,19 +18,170 @@ RSpec.shared_examples 'rejects helm packages access' do |user_type, status, add_ end end -RSpec.shared_examples 'process helm download content request' do |user_type, status, add_member = true| +RSpec.shared_examples 'process helm service index request' do |user_type, status| context "for user type #{user_type}" do before do - project.send("add_#{user_type}", user) if add_member && user_type != :anonymous + project.send("add_#{user_type}", user) if user_type != :anonymous && user_type != :not_a_member end - it_behaves_like 'returning response status', status + it 'returns a valid YAML response', :aggregate_failures do + subject + + expect(response).to have_gitlab_http_status(status) + + expect(response.media_type).to eq('text/yaml') + expect(response.body).to start_with("---\napiVersion: v1\nentries:\n") + + yaml_response = YAML.safe_load(response.body) + + expect(yaml_response.keys).to contain_exactly('apiVersion', 'entries', 'generated', 'serverInfo') + expect(yaml_response['entries']).to be_a(Hash) + expect(yaml_response['entries'].keys).to contain_exactly(package.name) + expect(yaml_response['serverInfo']).to eq({ 'contextPath' => "/api/v4/projects/#{project.id}/packages/helm" }) + + package_entry = yaml_response['entries'][package.name] + + expect(package_entry.length).to eq(1) + expect(package_entry.first.keys).to contain_exactly('name', 'version', 'apiVersion', 'created', 'digest', 'urls') + expect(package_entry.first['digest']).to eq('fd2b2fa0329e80a2a602c2bb3b40608bcd6ee5cf96cf46fd0d2800a4c129c9db') + expect(package_entry.first['urls']).to eq(["charts/#{package.name}-#{package.version}.tgz"]) + end + end +end + +RSpec.shared_examples 'process helm workhorse authorization' do |user_type, status, test_bypass: false| + context "for user type #{user_type}" do + before do + project.send("add_#{user_type}", user) if user_type != :anonymous && user_type != :not_a_member + end + + it 'has the proper status and content type' do + subject + + expect(response).to have_gitlab_http_status(status) + expect(response.media_type).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE) + end + + context 'with a request that bypassed gitlab-workhorse' do + let(:headers) do + basic_auth_header(user.username, personal_access_token.token) + .merge(workhorse_headers) + .tap { |h| h.delete(Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER) } + end + + before do + project.add_maintainer(user) + end + + it_behaves_like 'returning response status', :forbidden + end + end +end + +RSpec.shared_examples 'process helm upload' do |user_type, status| + shared_examples 'creates helm package files' do + it 'creates package files' do + expect(::Packages::Helm::ExtractionWorker).to receive(:perform_async).once + expect { subject } + .to change { project.packages.count }.by(1) + .and change { Packages::PackageFile.count }.by(1) + expect(response).to have_gitlab_http_status(status) + + package_file = project.packages.last.package_files.reload.last + expect(package_file.file_name).to eq('package.tgz') + end + end + + context "for user type #{user_type}" do + before do + project.send("add_#{user_type}", user) if user_type != :anonymous && user_type != :not_a_member + end + + context 'with object storage disabled' do + before do + stub_package_file_object_storage(enabled: false) + end + + context 'without a file from workhorse' do + let(:send_rewritten_field) { false } + + it_behaves_like 'returning response status', :bad_request + end + + context 'with correct params' do + it_behaves_like 'package workhorse uploads' + it_behaves_like 'creates helm package files' + it_behaves_like 'a package tracking event', 'API::HelmPackages', 'push_package' + end + end + + context 'with object storage enabled' do + let(:tmp_object) do + fog_connection.directories.new(key: 'packages').files.create( # rubocop:disable Rails/SaveBang + key: "tmp/uploads/#{file_name}", + body: 'content' + ) + end + + let(:fog_file) { fog_to_uploaded_file(tmp_object) } + let(:params) { { chart: fog_file, 'chart.remote_id' => file_name } } + + context 'and direct upload enabled' do + let(:fog_connection) do + stub_package_file_object_storage(direct_upload: true) + end + + it_behaves_like 'creates helm package files' + + ['123123', '../../123123'].each do |remote_id| + context "with invalid remote_id: #{remote_id}" do + let(:params) do + { + chart: fog_file, + 'chart.remote_id' => remote_id + } + end + + it_behaves_like 'returning response status', :forbidden + end + end + end + + context 'and direct upload disabled' do + context 'and background upload disabled' do + let(:fog_connection) do + stub_package_file_object_storage(direct_upload: false, background_upload: false) + end + + it_behaves_like 'creates helm package files' + end + + context 'and background upload enabled' do + let(:fog_connection) do + stub_package_file_object_storage(direct_upload: false, background_upload: true) + end + + it_behaves_like 'creates helm package files' + end + end + end + + it_behaves_like 'background upload schedules a file migration' + end +end + +RSpec.shared_examples 'process helm download content request' do |user_type, status| + context "for user type #{user_type}" do + before do + project.send("add_#{user_type}", user) if user_type != :anonymous && user_type != :not_a_member + end it_behaves_like 'a package tracking event', 'API::HelmPackages', 'pull_package' - it 'returns a valid package archive' do + it 'returns expected status and a valid package archive' do subject + expect(response).to have_gitlab_http_status(status) expect(response.media_type).to eq('application/octet-stream') end end @@ -51,3 +202,69 @@ RSpec.shared_examples 'rejects helm access with unknown project id' do end end end + +RSpec.shared_examples 'handling helm chart index requests' do + context 'with valid project' do + subject { get api(url), headers: headers } + + using RSpec::Parameterized::TableSyntax + + context 'personal token' do + where(:visibility, :user_role, :shared_examples_name, :expected_status) do + :public | :guest | 'process helm service index request' | :success + :public | :not_a_member | 'process helm service index request' | :success + :public | :anonymous | 'process helm service index request' | :success + :private | :reporter | 'process helm service index request' | :success + :private | :guest | 'rejects helm packages access' | :forbidden + :private | :not_a_member | 'rejects helm packages access' | :not_found + :private | :anonymous | 'rejects helm packages access' | :unauthorized + end + + with_them do + let(:headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, personal_access_token.token) } + + before do + project.update!(visibility: visibility.to_s) + end + + it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status] + end + end + + context 'when an invalid token is passed' do + let(:headers) { basic_auth_header(user.username, 'wrong') } + + it_behaves_like 'returning response status', :unauthorized + end + + context 'with job token' do + where(:visibility, :user_role, :shared_examples_name, :expected_status) do + :public | :guest | 'process helm service index request' | :success + :public | :not_a_member | 'process helm service index request' | :success + :public | :anonymous | 'process helm service index request' | :success + :private | :reporter | 'process helm service index request' | :success + :private | :guest | 'rejects helm packages access' | :forbidden + :private | :not_a_member | 'rejects helm packages access' | :not_found + :private | :anonymous | 'rejects helm packages access' | :unauthorized + end + + with_them do + let_it_be(:ci_build) { create(:ci_build, project: project, user: user, status: :running) } + + let(:headers) { user_role == :anonymous ? {} : job_basic_auth_header(ci_build) } + + before do + project.update!(visibility: visibility.to_s) + end + + it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status] + end + end + end + + it_behaves_like 'deploy token for package GET requests' + + it_behaves_like 'rejects helm access with unknown project id' do + subject { get api(url) } + end +end diff --git a/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb index 617fdecbb5b..878cbc10a24 100644 --- a/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb @@ -136,8 +136,8 @@ RSpec.shared_examples 'process nuget workhorse authorization' do |user_type, sta end end -RSpec.shared_examples 'process nuget upload' do |user_type, status, add_member = true| - RSpec.shared_examples 'creates nuget package files' do +RSpec.shared_examples 'process nuget upload' do |user_type, status, add_member = true, symbol_package = false| + shared_examples 'creates nuget package files' do it 'creates package files' do expect(::Packages::Nuget::ExtractionWorker).to receive(:perform_async).once expect { subject } @@ -146,7 +146,7 @@ RSpec.shared_examples 'process nuget upload' do |user_type, status, add_member = expect(response).to have_gitlab_http_status(status) package_file = target.packages.last.package_files.reload.last - expect(package_file.file_name).to eq('package.nupkg') + expect(package_file.file_name).to eq(file_name) end end @@ -169,7 +169,12 @@ RSpec.shared_examples 'process nuget upload' do |user_type, status, add_member = context 'with correct params' do it_behaves_like 'package workhorse uploads' it_behaves_like 'creates nuget package files' - it_behaves_like 'a package tracking event', 'API::NugetPackages', 'push_package' + + if symbol_package + it_behaves_like 'a package tracking event', 'API::NugetPackages', 'push_symbol_package' + else + it_behaves_like 'a package tracking event', 'API::NugetPackages', 'push_package' + end end end @@ -300,6 +305,18 @@ RSpec.shared_examples 'process nuget download content request' do |user_type, st it_behaves_like 'rejects nuget packages access', :anonymous, :not_found end + context 'with symbol package' do + let(:format) { 'snupkg' } + + it 'returns a valid package archive' do + subject + + expect(response.media_type).to eq('application/octet-stream') + end + + it_behaves_like 'a package tracking event', 'API::NugetPackages', 'pull_symbol_package' + end + context 'with lower case package name' do let_it_be(:package_name) { 'dummy.package' } @@ -407,3 +424,114 @@ RSpec.shared_examples 'rejects nuget access with unknown target id' do end end end + +RSpec.shared_examples 'nuget authorize upload endpoint' do + using RSpec::Parameterized::TableSyntax + + context 'with valid project' do + where(:visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do + 'PUBLIC' | :developer | true | true | 'process nuget workhorse authorization' | :success + 'PUBLIC' | :guest | true | true | 'rejects nuget packages access' | :forbidden + 'PUBLIC' | :developer | true | false | 'rejects nuget packages access' | :unauthorized + 'PUBLIC' | :guest | true | false | 'rejects nuget packages access' | :unauthorized + 'PUBLIC' | :developer | false | true | 'rejects nuget packages access' | :forbidden + 'PUBLIC' | :guest | false | true | 'rejects nuget packages access' | :forbidden + 'PUBLIC' | :developer | false | false | 'rejects nuget packages access' | :unauthorized + 'PUBLIC' | :guest | false | false | 'rejects nuget packages access' | :unauthorized + 'PUBLIC' | :anonymous | false | true | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :developer | true | true | 'process nuget workhorse authorization' | :success + 'PRIVATE' | :guest | true | true | 'rejects nuget packages access' | :forbidden + 'PRIVATE' | :developer | true | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :guest | true | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :developer | false | true | 'rejects nuget packages access' | :not_found + 'PRIVATE' | :guest | false | true | 'rejects nuget packages access' | :not_found + 'PRIVATE' | :developer | false | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :guest | false | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :anonymous | false | true | 'rejects nuget packages access' | :unauthorized + end + + with_them do + let(:token) { user_token ? personal_access_token.token : 'wrong' } + let(:user_headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) } + let(:headers) { user_headers.merge(workhorse_headers) } + + before do + update_visibility_to(Gitlab::VisibilityLevel.const_get(visibility_level, false)) + end + + it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member] + end + end + + it_behaves_like 'deploy token for package uploads' + + it_behaves_like 'job token for package uploads', authorize_endpoint: true do + let_it_be(:job) { create(:ci_build, :running, user: user, project: project) } + end + + it_behaves_like 'rejects nuget access with unknown target id' + + it_behaves_like 'rejects nuget access with invalid target id' +end + +RSpec.shared_examples 'nuget upload endpoint' do |symbol_package: false| + using RSpec::Parameterized::TableSyntax + + context 'with valid project' do + where(:visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do + 'PUBLIC' | :developer | true | true | 'process nuget upload' | :created + 'PUBLIC' | :guest | true | true | 'rejects nuget packages access' | :forbidden + 'PUBLIC' | :developer | true | false | 'rejects nuget packages access' | :unauthorized + 'PUBLIC' | :guest | true | false | 'rejects nuget packages access' | :unauthorized + 'PUBLIC' | :developer | false | true | 'rejects nuget packages access' | :forbidden + 'PUBLIC' | :guest | false | true | 'rejects nuget packages access' | :forbidden + 'PUBLIC' | :developer | false | false | 'rejects nuget packages access' | :unauthorized + 'PUBLIC' | :guest | false | false | 'rejects nuget packages access' | :unauthorized + 'PUBLIC' | :anonymous | false | true | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :developer | true | true | 'process nuget upload' | :created + 'PRIVATE' | :guest | true | true | 'rejects nuget packages access' | :forbidden + 'PRIVATE' | :developer | true | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :guest | true | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :developer | false | true | 'rejects nuget packages access' | :not_found + 'PRIVATE' | :guest | false | true | 'rejects nuget packages access' | :not_found + 'PRIVATE' | :developer | false | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :guest | false | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :anonymous | false | true | 'rejects nuget packages access' | :unauthorized + end + + with_them do + let(:token) { user_token ? personal_access_token.token : 'wrong' } + let(:user_headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) } + let(:headers) { user_headers.merge(workhorse_headers) } + let(:snowplow_gitlab_standard_context) { { project: project, user: user, namespace: project.namespace } } + + before do + update_visibility_to(Gitlab::VisibilityLevel.const_get(visibility_level, false)) + end + + it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member], symbol_package + end + end + + it_behaves_like 'deploy token for package uploads' + + it_behaves_like 'job token for package uploads' do + let_it_be(:job) { create(:ci_build, :running, user: user, project: project) } + end + + it_behaves_like 'rejects nuget access with unknown target id' + + it_behaves_like 'rejects nuget access with invalid target id' + + context 'file size above maximum limit' do + let(:headers) { basic_auth_header(deploy_token.username, deploy_token.token).merge(workhorse_headers) } + + before do + allow_next_instance_of(UploadedFile) do |uploaded_file| + allow(uploaded_file).to receive(:size).and_return(project.actual_limits.nuget_max_file_size + 1) + end + end + + it_behaves_like 'returning response status', :bad_request + end +end diff --git a/spec/support/shared_examples/requests/api/packages_shared_examples.rb b/spec/support/shared_examples/requests/api/packages_shared_examples.rb index 42c29084d7b..ecde4ee8565 100644 --- a/spec/support/shared_examples/requests/api/packages_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/packages_shared_examples.rb @@ -100,7 +100,7 @@ RSpec.shared_examples 'job token for package GET requests' do end end -RSpec.shared_examples 'job token for package uploads' do |authorize_endpoint: false| +RSpec.shared_examples 'job token for package uploads' do |authorize_endpoint: false, accept_invalid_username: false| context 'with job token headers' do let(:headers) { basic_auth_header(::Gitlab::Auth::CI_JOB_USER, job.token).merge(workhorse_headers) } @@ -133,7 +133,11 @@ RSpec.shared_examples 'job token for package uploads' do |authorize_endpoint: fa context 'invalid user' do let(:headers) { basic_auth_header('foo', job.token).merge(workhorse_headers) } - it_behaves_like 'returning response status', :unauthorized + if accept_invalid_username + it_behaves_like 'returning response status', :success + else + it_behaves_like 'returning response status', :unauthorized + end end end end @@ -143,7 +147,7 @@ RSpec.shared_examples 'a package tracking event' do |category, action| stub_feature_flags(collect_package_events: true) end - it "creates a gitlab tracking event #{action}", :snowplow do + it "creates a gitlab tracking event #{action}", :snowplow, :aggregate_failures do expect { subject }.to change { Packages::Event.count }.by(1) expect_snowplow_event(category: category, action: action, **snowplow_gitlab_standard_context) diff --git a/spec/support/shared_examples/services/alert_management/alert_processing/alert_firing_shared_examples.rb b/spec/support/shared_examples/services/alert_management/alert_processing/alert_firing_shared_examples.rb index 218a3462c35..92a7d7ab3a3 100644 --- a/spec/support/shared_examples/services/alert_management/alert_processing/alert_firing_shared_examples.rb +++ b/spec/support/shared_examples/services/alert_management/alert_processing/alert_firing_shared_examples.rb @@ -13,7 +13,7 @@ RSpec.shared_examples 'creates an alert management alert or errors' do it 'executes the alert service hooks' do expect_next_instance_of(AlertManagement::Alert) do |alert| - expect(alert).to receive(:execute_services) + expect(alert).to receive(:execute_integrations) end subject @@ -84,7 +84,7 @@ end # - `alert`, the alert for which events should be incremented RSpec.shared_examples 'adds an alert management alert event' do specify do - expect(alert).not_to receive(:execute_services) + expect(alert).not_to receive(:execute_integrations) expect { subject }.to change { alert.reload.events }.by(1) diff --git a/spec/support/shared_examples/services/alert_management/alert_processing/incident_creation_shared_examples.rb b/spec/support/shared_examples/services/alert_management/alert_processing/incident_creation_shared_examples.rb index c6ac07b6dd5..98834f01ce2 100644 --- a/spec/support/shared_examples/services/alert_management/alert_processing/incident_creation_shared_examples.rb +++ b/spec/support/shared_examples/services/alert_management/alert_processing/incident_creation_shared_examples.rb @@ -20,7 +20,7 @@ end RSpec.shared_examples 'processes incident issues' do |with_issue: false| before do allow_next_instance_of(AlertManagement::Alert) do |alert| - allow(alert).to receive(:execute_services) + allow(alert).to receive(:execute_integrations) end end 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 ba176b616c3..eafcbd77040 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 @@ -157,8 +157,13 @@ end RSpec.shared_examples 'a container registry auth service' do include_context 'container registry auth service context' + before do + stub_feature_flags(container_registry_migration_phase1: false) + end + describe '#full_access_token' do let_it_be(:project) { create(:project) } + let(:token) { described_class.full_access_token(project.full_path) } subject { { token: token } } @@ -172,6 +177,7 @@ RSpec.shared_examples 'a container registry auth service' do describe '#pull_access_token' do let_it_be(:project) { create(:project) } + let(:token) { described_class.pull_access_token(project.full_path) } subject { { token: token } } @@ -432,6 +438,7 @@ RSpec.shared_examples 'a container registry auth service' do context 'for external user' do context 'disallow anyone to pull or push images' do let_it_be(:current_user) { create(:user, external: true) } + let(:current_params) do { scopes: ["repository:#{project.full_path}:pull,push"] } end @@ -442,6 +449,7 @@ RSpec.shared_examples 'a container registry auth service' do context 'disallow anyone to delete images' do let_it_be(:current_user) { create(:user, external: true) } + let(:current_params) do { scopes: ["repository:#{project.full_path}:*"] } end @@ -452,6 +460,7 @@ RSpec.shared_examples 'a container registry auth service' do context 'disallow anyone to delete images since registry 2.7' do let_it_be(:current_user) { create(:user, external: true) } + let(:current_params) do { scopes: ["repository:#{project.full_path}:delete"] } end @@ -620,6 +629,22 @@ RSpec.shared_examples 'a container registry auth service' do end end end + + context 'for project with private container registry' do + let_it_be(:project, reload: true) { create(:project, :public) } + + before do + project.project_feature.update!(container_registry_access_level: ProjectFeature::PRIVATE) + end + + it_behaves_like 'pullable for being team member' + + context 'when you are admin' do + let_it_be(:current_user) { create(:admin) } + + it_behaves_like 'pullable for being team member' + end + end end context 'when pushing' do diff --git a/spec/support/shared_examples/services/jira_import/user_mapper_services_shared_examples.rb b/spec/support/shared_examples/services/jira_import/user_mapper_services_shared_examples.rb index cbe5c7d89db..0151723793e 100644 --- a/spec/support/shared_examples/services/jira_import/user_mapper_services_shared_examples.rb +++ b/spec/support/shared_examples/services/jira_import/user_mapper_services_shared_examples.rb @@ -3,7 +3,7 @@ RSpec.shared_examples 'mapping jira users' do let(:client) { double } - let_it_be(:jira_service) { create(:jira_service, project: project, active: true) } + let_it_be(:jira_integration) { create(:jira_integration, project: project, active: true) } before do allow(subject).to receive(:client).and_return(client) diff --git a/spec/support/shared_examples/services/packages_shared_examples.rb b/spec/support/shared_examples/services/packages_shared_examples.rb index 72878e925dc..6bc4f171d9c 100644 --- a/spec/support/shared_examples/services/packages_shared_examples.rb +++ b/spec/support/shared_examples/services/packages_shared_examples.rb @@ -43,6 +43,7 @@ end RSpec.shared_examples 'assigns status to package' do context 'with status param' do let_it_be(:status) { 'hidden' } + let(:params) { super().merge(status: status) } it 'assigns the status to the package' do diff --git a/spec/support/shared_examples/services/projects/update_repository_storage_service_shared_examples.rb b/spec/support/shared_examples/services/projects/update_repository_storage_service_shared_examples.rb index 275ddebc18c..14af35e58b7 100644 --- a/spec/support/shared_examples/services/projects/update_repository_storage_service_shared_examples.rb +++ b/spec/support/shared_examples/services/projects/update_repository_storage_service_shared_examples.rb @@ -123,9 +123,10 @@ RSpec.shared_examples 'moves repository to another storage' do |repository_type| .with(repository.raw) .and_raise(Gitlab::Git::CommandError) - result = subject.execute + expect do + subject.execute + end.to raise_error(Gitlab::Git::CommandError) - expect(result).to be_error expect(project).not_to be_repository_read_only expect(project.repository_storage).to eq('default') expect(repository_storage_move).to be_failed @@ -149,9 +150,10 @@ RSpec.shared_examples 'moves repository to another storage' do |repository_type| expect(original_repository_double).to receive(:remove) .and_raise(Gitlab::Git::CommandError) - result = subject.execute + expect do + subject.execute + end.to raise_error(Gitlab::Git::CommandError) - expect(result).to be_error expect(repository_storage_move).to be_cleanup_failed end end @@ -170,9 +172,10 @@ RSpec.shared_examples 'moves repository to another storage' do |repository_type| allow(repository_double).to receive(:checksum) .and_return('not matching checksum') - result = subject.execute + expect do + subject.execute + end.to raise_error(UpdateRepositoryStorageMethods::Error, /Failed to verify \w+ repository checksum from \w+ to not matching checksum/) - expect(result).to be_error expect(project).not_to be_repository_read_only expect(project.repository_storage).to eq('default') end diff --git a/spec/support/shared_examples/services/service_ping/complete_service_ping_payload_shared_examples.rb b/spec/support/shared_examples/services/service_ping/complete_service_ping_payload_shared_examples.rb new file mode 100644 index 00000000000..8dcff99fb6f --- /dev/null +++ b/spec/support/shared_examples/services/service_ping/complete_service_ping_payload_shared_examples.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'complete service ping payload' do + it_behaves_like 'service ping payload with all expected metrics' do + let(:expected_metrics) do + standard_metrics + subscription_metrics + operational_metrics + optional_metrics + end + end +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 new file mode 100644 index 00000000000..535e7291b7e --- /dev/null +++ b/spec/support/shared_examples/services/service_ping/service_ping_payload_with_all_expected_metrics_shared_examples.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'service ping payload with all expected metrics' do + specify do + aggregate_failures do + expected_metrics.each do |metric| + is_expected.to have_usage_metric metric['key_path'] + end + end + end +end 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 new file mode 100644 index 00000000000..9f18174cbc7 --- /dev/null +++ b/spec/support/shared_examples/services/service_ping/service_ping_payload_without_restricted_metrics_shared_examples.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'service ping payload without restricted metrics' do + specify do + aggregate_failures do + restricted_metrics.each do |metric| + is_expected.not_to have_usage_metric metric['key_path'] + end + end + end +end diff --git a/spec/support/shared_examples/services/snippets_shared_examples.rb b/spec/support/shared_examples/services/snippets_shared_examples.rb index 0c4db7ded69..5a44f739b27 100644 --- a/spec/support/shared_examples/services/snippets_shared_examples.rb +++ b/spec/support/shared_examples/services/snippets_shared_examples.rb @@ -1,23 +1,6 @@ # frozen_string_literal: true RSpec.shared_examples 'checking spam' do - let(:request) { double(:request, headers: headers) } - let(:headers) { nil } - let(:api) { true } - let(:captcha_response) { 'abc123' } - let(:spam_log_id) { 1 } - let(:disable_spam_action_service) { false } - - let(:extra_opts) do - { - request: request, - api: api, - captcha_response: captcha_response, - spam_log_id: spam_log_id, - disable_spam_action_service: disable_spam_action_service - } - end - before do allow_next_instance_of(UserAgentDetailService) do |instance| allow(instance).to receive(:create) @@ -25,73 +8,20 @@ RSpec.shared_examples 'checking spam' do end it 'executes SpamActionService' do - spam_params = Spam::SpamParams.new( - api: api, - captcha_response: captcha_response, - spam_log_id: spam_log_id - ) expect_next_instance_of( Spam::SpamActionService, { spammable: kind_of(Snippet), - request: request, + spam_params: spam_params, user: an_instance_of(User), action: action } ) do |instance| - expect(instance).to receive(:execute).with(spam_params: spam_params) + expect(instance).to receive(:execute) end subject end - - context 'when CAPTCHA arguments are passed in the headers' do - let(:headers) do - { - 'X-GitLab-Spam-Log-Id' => spam_log_id, - 'X-GitLab-Captcha-Response' => captcha_response - } - end - - let(:extra_opts) do - { - request: request, - api: api, - disable_spam_action_service: disable_spam_action_service - } - end - - it 'executes the SpamActionService correctly' do - spam_params = Spam::SpamParams.new( - api: api, - captcha_response: captcha_response, - spam_log_id: spam_log_id - ) - expect_next_instance_of( - Spam::SpamActionService, - { - spammable: kind_of(Snippet), - request: request, - user: an_instance_of(User), - action: action - } - ) do |instance| - expect(instance).to receive(:execute).with(spam_params: spam_params) - end - - subject - end - end - - context 'when spam action service is disabled' do - let(:disable_spam_action_service) { true } - - it 'request parameter is not passed to the service' do - expect(Spam::SpamActionService).not_to receive(:new) - - subject - end - end end shared_examples 'invalid params error response' do diff --git a/spec/support/shared_examples/services/wikis/create_attachment_service_shared_examples.rb b/spec/support/shared_examples/services/wikis/create_attachment_service_shared_examples.rb index 555a6d5eed0..1646c18a0ed 100644 --- a/spec/support/shared_examples/services/wikis/create_attachment_service_shared_examples.rb +++ b/spec/support/shared_examples/services/wikis/create_attachment_service_shared_examples.rb @@ -25,6 +25,25 @@ RSpec.shared_examples 'Wikis::CreateAttachmentService#execute' do |container_typ container.add_developer(user) end + context 'creates wiki repository if it does not exist' do + let(:container) { create(container_type) } # rubocop:disable Rails/SaveBang + + it 'creates wiki repository' do + expect { service.execute }.to change { container.wiki.repository.exists? }.to(true) + end + + context 'if an error is raised creating the repository' do + it 'catches error and return gracefully' do + allow(container.wiki).to receive(:repository_exists?).and_return(false) + + result = service.execute + + expect(result[:status]).to eq :error + expect(result[:message]).to eq 'Error creating the wiki repository' + end + end + end + context 'creates branch if it does not exists' do let(:branch_name) { 'new_branch' } let(:opts) { file_opts.merge(branch_name: branch_name) } diff --git a/spec/support/shared_examples/workers/in_product_marketing_email_shared_example.rb b/spec/support/shared_examples/workers/in_product_marketing_email_shared_example.rb deleted file mode 100644 index c4391f61369..00000000000 --- a/spec/support/shared_examples/workers/in_product_marketing_email_shared_example.rb +++ /dev/null @@ -1,15 +0,0 @@ -# frozen_string_literal: true - -RSpec.shared_examples 'in-product marketing email' do - before do - stub_application_setting(in_product_marketing_emails_enabled: in_product_marketing_emails_enabled) - stub_experiment(in_product_marketing_emails: experiment_active) - allow(::Gitlab).to receive(:com?).and_return(is_gitlab_com) - end - - it 'executes the email service service' do - expect(Namespaces::InProductMarketingEmailsService).to receive(:send_for_all_tracks_and_intervals).exactly(executes_service).times - - subject.perform - end -end diff --git a/spec/support/sidekiq.rb b/spec/support/sidekiq.rb index 374997af1ec..dc475b92c0b 100644 --- a/spec/support/sidekiq.rb +++ b/spec/support/sidekiq.rb @@ -20,4 +20,25 @@ RSpec.configure do |config| config.around(:example, :sidekiq_inline) do |example| gitlab_sidekiq_inline { example.run } end + + # Some specs need to run mailers through Sidekiq explicitly, rather + # than the ActiveJob test adapter. There is a Rails bug that means we + # have to do some extra steps to make this happen: + # https://github.com/rails/rails/issues/37270 + # + # In particular, we can't use an `around` hook because then the 'before' part + # of that will run before the `before_setup` hook in ActiveJob::TestHelper, + # which doesn't do what we want. + # + config.before(:example, :sidekiq_mailers) do + queue_adapter_changed_jobs.each { |k| k.queue_adapter = :sidekiq } + queue_adapter_changed_jobs.each(&:disable_test_adapter) + end + + config.after(:example, :sidekiq_mailers) do + queue_adapter_changed_jobs.each do |klass| + klass.queue_adapter = :test + klass.enable_test_adapter(ActiveJob::QueueAdapters::TestAdapter.new) + end + end end |