diff options
Diffstat (limited to 'spec/support')
110 files changed, 2613 insertions, 722 deletions
diff --git a/spec/support/atlassian/jira_connect/schemata.rb b/spec/support/atlassian/jira_connect/schemata.rb index d056c7cacf3..61e8aa8e15c 100644 --- a/spec/support/atlassian/jira_connect/schemata.rb +++ b/spec/support/atlassian/jira_connect/schemata.rb @@ -74,7 +74,7 @@ module Atlassian 'deploymentSequenceNumber' => { 'type' => 'integer' }, 'updateSequenceNumber' => { 'type' => 'integer' }, 'associations' => { - 'type' => 'array', + 'type' => %w(array), 'items' => association_type, 'minItems' => 1 }, diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb index be2b41d6997..f9a28c8e40b 100644 --- a/spec/support/capybara.rb +++ b/spec/support/capybara.rb @@ -157,7 +157,7 @@ RSpec.configure do |config| unless session.current_window.size == CAPYBARA_WINDOW_SIZE begin session.current_window.resize_to(*CAPYBARA_WINDOW_SIZE) - rescue # ? + rescue StandardError # ? end end end @@ -170,14 +170,16 @@ RSpec.configure do |config| Capybara.raise_server_errors = false example.run + ensure + Capybara.raise_server_errors = true + end + config.append_after do |example| if example.metadata[:screenshot] screenshot = example.metadata[:screenshot][:image] || example.metadata[:screenshot][:html] + screenshot&.delete_prefix!(ENV.fetch('CI_PROJECT_DIR', '')) example.metadata[:stdout] = %{[[ATTACHMENT|#{screenshot}]]} end - - ensure - Capybara.raise_server_errors = true end config.after(:example, :js) do |example| diff --git a/spec/support/db_cleaner.rb b/spec/support/db_cleaner.rb index 77e1f6bcaa3..ff913ebf22b 100644 --- a/spec/support/db_cleaner.rb +++ b/spec/support/db_cleaner.rb @@ -16,4 +16,4 @@ module DbCleaner end end -DbCleaner.prepend_if_ee('EE::DbCleaner') +DbCleaner.prepend_mod_with('DbCleaner') diff --git a/spec/support/factory_bot.rb b/spec/support/factory_bot.rb index 5761e05d541..5f22fa11e9e 100644 --- a/spec/support/factory_bot.rb +++ b/spec/support/factory_bot.rb @@ -2,6 +2,14 @@ FactoryBot::SyntaxRunner.class_eval do include RSpec::Mocks::ExampleMethods + + # FactoryBot doesn't allow yet to add a helper that can be used in factories + # While the fixture_file_upload helper is reasonable to be used there: + # + # https://github.com/thoughtbot/factory_bot/issues/564#issuecomment-389491577 + def fixture_file_upload(*args, **kwargs) + Rack::Test::UploadedFile.new(*args, **kwargs) + end end # Patching FactoryBot to allow stubbing non AR models diff --git a/spec/support/factory_default.rb b/spec/support/factory_default.rb index e116c28f132..31af022f6c0 100644 --- a/spec/support/factory_default.rb +++ b/spec/support/factory_default.rb @@ -1,5 +1,17 @@ # frozen_string_literal: true +module Gitlab + module FreezeFactoryDefault + def set_factory_default(name, obj, preserve_traits: nil) + obj.freeze unless obj.frozen? + + super + end + end +end + +TestProf::FactoryDefault::DefaultSyntax.prepend Gitlab::FreezeFactoryDefault + RSpec.configure do |config| config.after do |ex| TestProf::FactoryDefault.reset unless ex.metadata[:factory_default] == :keep diff --git a/spec/support/gitlab/usage/metrics_instrumentation_shared_examples.rb b/spec/support/gitlab/usage/metrics_instrumentation_shared_examples.rb new file mode 100644 index 00000000000..c9ff566e94c --- /dev/null +++ b/spec/support/gitlab/usage/metrics_instrumentation_shared_examples.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'a correct instrumented metric value' do |options, expected_value| + let(:time_frame) { options[:time_frame] } + + before do + allow(ActiveRecord::Base.connection).to receive(:transaction_open?).and_return(false) + end + + it 'has correct value' do + expect(described_class.new(time_frame: time_frame).value).to eq(expected_value) + end +end diff --git a/spec/support/gitlab_experiment.rb b/spec/support/gitlab_experiment.rb index bd0c88f8049..b84adf82d29 100644 --- a/spec/support/gitlab_experiment.rb +++ b/spec/support/gitlab_experiment.rb @@ -12,10 +12,6 @@ class ApplicationExperiment # rubocop:disable Gitlab/NamespacedClass super(...) Feature.persist_used!(feature_flag_name) end - - def should_track? - true - end end RSpec.configure do |config| diff --git a/spec/support/helpers/board_helpers.rb b/spec/support/helpers/board_helpers.rb index 6e145fed733..c4e69d06f52 100644 --- a/spec/support/helpers/board_helpers.rb +++ b/spec/support/helpers/board_helpers.rb @@ -4,6 +4,23 @@ module BoardHelpers def click_card(card) within card do first('.board-card-number').click + wait_for_requests + end + end + + def load_board(board_path) + visit board_path + + wait_for_requests + end + + def click_card_and_edit_label + click_card(card) + + page.within(labels_select) do + click_button 'Edit' + + wait_for_requests end end end diff --git a/spec/support/helpers/cycle_analytics_helpers.rb b/spec/support/helpers/cycle_analytics_helpers.rb index 9e62eef14de..5510284b30d 100644 --- a/spec/support/helpers/cycle_analytics_helpers.rb +++ b/spec/support/helpers/cycle_analytics_helpers.rb @@ -3,6 +3,38 @@ module CycleAnalyticsHelpers include GitHelpers + def toggle_value_stream_dropdown + page.find('[data-testid="dropdown-value-streams"]').click + end + + def add_custom_stage_to_form + page.find_button(s_('CreateValueStreamForm|Add another stage')).click + + index = page.all('[data-testid="value-stream-stage-fields"]').length + last_stage = page.all('[data-testid="value-stream-stage-fields"]').last + + within last_stage do + find('[name*="custom-stage-name-"]').fill_in with: "Cool custom stage - name #{index}" + select_dropdown_option_by_value "custom-stage-start-event-", :merge_request_created + select_dropdown_option_by_value "custom-stage-end-event-", :merge_request_merged + end + end + + def save_value_stream(custom_value_stream_name) + fill_in 'create-value-stream-name', with: custom_value_stream_name + + page.find_button(s_('CreateValueStreamForm|Create Value Stream')).click + wait_for_requests + end + + def create_custom_value_stream(custom_value_stream_name) + toggle_value_stream_dropdown + page.find_button(_('Create new Value Stream')).click + + add_custom_stage_to_form + save_value_stream(custom_value_stream_name) + end + def wait_for_stages_to_load(selector = '.js-path-navigation') expect(page).to have_selector selector wait_for_requests @@ -93,17 +125,17 @@ module CycleAnalyticsHelpers target_branch: 'master' } - mr = MergeRequests::CreateService.new(project, user, opts).execute + mr = MergeRequests::CreateService.new(project: project, current_user: user, params: opts).execute NewMergeRequestWorker.new.perform(mr, user) mr end def merge_merge_requests_closing_issue(user, project, issue) merge_requests = Issues::ReferencedMergeRequestsService - .new(project, user) + .new(project: project, current_user: user) .closed_by_merge_requests(issue) - merge_requests.each { |merge_request| MergeRequests::MergeService.new(project, user, sha: merge_request.diff_head_sha).execute(merge_request) } + merge_requests.each { |merge_request| MergeRequests::MergeService.new(project: project, current_user: user, params: { sha: merge_request.diff_head_sha }).execute(merge_request) } end def deploy_master(user, project, environment: 'production') diff --git a/spec/support/helpers/dns_helpers.rb b/spec/support/helpers/dns_helpers.rb index 1795b0a9ac3..ba32ccbb6f1 100644 --- a/spec/support/helpers/dns_helpers.rb +++ b/spec/support/helpers/dns_helpers.rb @@ -18,7 +18,7 @@ module DnsHelpers def stub_invalid_dns! allow(Addrinfo).to receive(:getaddrinfo).with(/\Afoobar\.\w|(\d{1,3}\.){4,}\d{1,3}\z/i, anything, nil, :STREAM) do - raise SocketError.new("getaddrinfo: Name or service not known") + raise SocketError, "getaddrinfo: Name or service not known" end end diff --git a/spec/support/helpers/features/invite_members_modal_helper.rb b/spec/support/helpers/features/invite_members_modal_helper.rb new file mode 100644 index 00000000000..1127c817656 --- /dev/null +++ b/spec/support/helpers/features/invite_members_modal_helper.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +module Spec + module Support + module Helpers + module Features + module InviteMembersModalHelper + def invite_member(name, role: 'Guest', expires_at: nil) + click_on 'Invite members' + + page.within '#invite-members-modal' do + fill_in 'Select members or type email addresses', with: name + + wait_for_requests + click_button name + choose_options(role, expires_at) + + click_button 'Invite' + + page.refresh + end + end + + def invite_group(name, role: 'Guest', expires_at: nil) + click_on 'Invite a group' + + click_on 'Select a group' + wait_for_requests + click_button name + choose_options(role, expires_at) + + click_button 'Invite' + + page.refresh + end + + def choose_options(role, expires_at) + unless role == 'Guest' + click_button 'Guest' + wait_for_requests + click_button role + end + + fill_in 'YYYY-MM-DD', with: expires_at.try(:strftime, '%Y-%m-%d') + end + end + end + end + end +end diff --git a/spec/support/helpers/features/members_table_helpers.rb b/spec/support/helpers/features/members_table_helpers.rb index 4a0e218ed3e..2e86e014a1b 100644 --- a/spec/support/helpers/features/members_table_helpers.rb +++ b/spec/support/helpers/features/members_table_helpers.rb @@ -27,10 +27,6 @@ module Spec all_rows[2] end - def invite_users_form - page.find('[data-testid="invite-users-form"]') - end - def find_row(name) page.within(members_table) do page.find('tbody > tr', text: name) diff --git a/spec/support/helpers/gitaly_setup.rb b/spec/support/helpers/gitaly_setup.rb new file mode 100644 index 00000000000..2ce4bcfa943 --- /dev/null +++ b/spec/support/helpers/gitaly_setup.rb @@ -0,0 +1,195 @@ +# frozen_string_literal: true + +# This file contains environment settings for gitaly when it's running +# as part of the gitlab-ce/ee test suite. +# +# Please be careful when modifying this file. Your changes must work +# both for local development rspec runs, and in CI. + +require 'securerandom' +require 'socket' +require 'logger' + +module GitalySetup + LOGGER = begin + default_name = ENV['CI'] ? 'DEBUG' : 'WARN' + level_name = ENV['GITLAB_TESTING_LOG_LEVEL']&.upcase + level = Logger.const_get(level_name || default_name, true) # rubocop: disable Gitlab/ConstGetInheritFalse + Logger.new(STDOUT, level: level, formatter: ->(_, _, _, msg) { msg }) + end + + def tmp_tests_gitaly_dir + File.expand_path('../../../tmp/tests/gitaly', __dir__) + end + + def tmp_tests_gitaly_bin_dir + File.join(tmp_tests_gitaly_dir, '_build', 'bin') + end + + def tmp_tests_gitlab_shell_dir + File.expand_path('../../../tmp/tests/gitlab-shell', __dir__) + end + + def rails_gitlab_shell_secret + File.expand_path('../../../.gitlab_shell_secret', __dir__) + end + + def gemfile + File.join(tmp_tests_gitaly_dir, 'ruby', 'Gemfile') + end + + def gemfile_dir + File.dirname(gemfile) + end + + def gitlab_shell_secret_file + File.join(tmp_tests_gitlab_shell_dir, '.gitlab_shell_secret') + end + + def env + { + 'HOME' => File.expand_path('tmp/tests'), + 'GEM_PATH' => Gem.path.join(':'), + 'BUNDLE_APP_CONFIG' => File.join(gemfile_dir, '.bundle'), + 'BUNDLE_INSTALL_FLAGS' => nil, + 'BUNDLE_GEMFILE' => gemfile, + 'RUBYOPT' => nil, + + # Git hooks can't run during tests as the internal API is not running. + 'GITALY_TESTING_NO_GIT_HOOKS' => "1", + 'GITALY_TESTING_ENABLE_ALL_FEATURE_FLAGS' => "true" + } + end + + # rubocop:disable GitlabSecurity/SystemCommandInjection + def set_bundler_config + system('bundle config set --local jobs 4', chdir: gemfile_dir) + system('bundle config set --local retry 3', chdir: gemfile_dir) + + if ENV['CI'] + bundle_path = File.expand_path('../../../vendor/gitaly-ruby', __dir__) + system('bundle', 'config', 'set', '--local', 'path', bundle_path, chdir: gemfile_dir) + end + end + # rubocop:enable GitlabSecurity/SystemCommandInjection + + def config_path(service) + case service + when :gitaly + File.join(tmp_tests_gitaly_dir, 'config.toml') + when :gitaly2 + File.join(tmp_tests_gitaly_dir, 'gitaly2.config.toml') + when :praefect + File.join(tmp_tests_gitaly_dir, 'praefect.config.toml') + end + end + + def service_binary(service) + case service + when :gitaly, :gitaly2 + 'gitaly' + when :praefect + 'praefect' + end + end + + def install_gitaly_gems + system(env, "make #{tmp_tests_gitaly_dir}/.ruby-bundle", chdir: tmp_tests_gitaly_dir) # rubocop:disable GitlabSecurity/SystemCommandInjection + end + + def build_gitaly + system(env, 'make', chdir: tmp_tests_gitaly_dir) # rubocop:disable GitlabSecurity/SystemCommandInjection + end + + def start_gitaly + start(:gitaly) + end + + def start_gitaly2 + start(:gitaly2) + end + + def start_praefect + start(:praefect) + end + + def start(service) + args = ["#{tmp_tests_gitaly_bin_dir}/#{service_binary(service)}"] + args.push("-config") if service == :praefect + args.push(config_path(service)) + pid = spawn(env, *args, [:out, :err] => "log/#{service}-test.log") + + begin + try_connect!(service) + rescue StandardError + Process.kill('TERM', pid) + raise + end + + pid + end + + # Taken from Gitlab::Shell.generate_and_link_secret_token + def ensure_gitlab_shell_secret! + secret_file = rails_gitlab_shell_secret + shell_link = gitlab_shell_secret_file + + unless File.size?(secret_file) + File.write(secret_file, SecureRandom.hex(16)) + end + + unless File.exist?(shell_link) + FileUtils.ln_s(secret_file, shell_link) + end + end + + def check_gitaly_config! + LOGGER.debug "Checking gitaly-ruby Gemfile...\n" + + unless File.exist?(gemfile) + message = "#{gemfile} does not exist." + message += "\n\nThis might have happened if the CI artifacts for this build were destroyed." if ENV['CI'] + abort message + end + + LOGGER.debug "Checking gitaly-ruby bundle...\n" + out = ENV['CI'] ? STDOUT : '/dev/null' + abort 'bundle check failed' unless system(env, 'bundle', 'check', out: out, chdir: File.dirname(gemfile)) + end + + def read_socket_path(service) + # This code needs to work in an environment where we cannot use bundler, + # so we cannot easily use the toml-rb gem. This ad-hoc parser should be + # good enough. + config_text = IO.read(config_path(service)) + + config_text.lines.each do |line| + match_data = line.match(/^\s*socket_path\s*=\s*"([^"]*)"$/) + + return match_data[1] if match_data + end + + raise "failed to find socket_path in #{config_path(service)}" + end + + def try_connect!(service) + LOGGER.debug "Trying to connect to #{service}: " + timeout = 20 + delay = 0.1 + socket = read_socket_path(service) + + Integer(timeout / delay).times do + UNIXSocket.new(socket) + LOGGER.debug " OK\n" + + return + rescue Errno::ENOENT, Errno::ECONNREFUSED + LOGGER.debug '.' + sleep delay + end + + LOGGER.warn " FAILED to connect to #{service}\n" + + raise "could not connect to #{socket}" + end +end diff --git a/spec/support/helpers/graphql_helpers.rb b/spec/support/helpers/graphql_helpers.rb index 9d6c6ab93e4..5dc6945ec5e 100644 --- a/spec/support/helpers/graphql_helpers.rb +++ b/spec/support/helpers/graphql_helpers.rb @@ -142,9 +142,9 @@ module GraphqlHelpers Class.new(::Types::BaseObject) { graphql_name name } end - def resolver_instance(resolver_class, obj: nil, ctx: {}, field: nil, schema: GitlabSchema) + def resolver_instance(resolver_class, obj: nil, ctx: {}, field: nil, schema: GitlabSchema, subscription_update: false) if ctx.is_a?(Hash) - q = double('Query', schema: schema) + q = double('Query', schema: schema, subscription_update?: subscription_update) ctx = GraphQL::Query::Context.new(query: q, object: obj, values: ctx) end diff --git a/spec/support/helpers/ldap_helpers.rb b/spec/support/helpers/ldap_helpers.rb index 8154e3a4fc9..2f5f8be518c 100644 --- a/spec/support/helpers/ldap_helpers.rb +++ b/spec/support/helpers/ldap_helpers.rb @@ -71,4 +71,4 @@ module LdapHelpers end end -LdapHelpers.include_if_ee('EE::LdapHelpers') +LdapHelpers.include_mod_with('LdapHelpers') diff --git a/spec/support/helpers/license_helper.rb b/spec/support/helpers/license_helper.rb index a1defba9ccb..61afb2171f0 100644 --- a/spec/support/helpers/license_helper.rb +++ b/spec/support/helpers/license_helper.rb @@ -7,4 +7,4 @@ module LicenseHelpers end end -LicenseHelpers.prepend_if_ee('EE::LicenseHelpers') +LicenseHelpers.prepend_mod_with('LicenseHelpers') diff --git a/spec/support/helpers/login_helpers.rb b/spec/support/helpers/login_helpers.rb index 86022e16d71..fc3eb976276 100644 --- a/spec/support/helpers/login_helpers.rb +++ b/spec/support/helpers/login_helpers.rb @@ -237,4 +237,4 @@ module LoginHelpers end end -LoginHelpers.prepend_if_ee('EE::LoginHelpers') +LoginHelpers.prepend_mod_with('LoginHelpers') diff --git a/spec/support/helpers/migrations_helpers.rb b/spec/support/helpers/migrations_helpers.rb index 2c31a608b35..fa50b234bd5 100644 --- a/spec/support/helpers/migrations_helpers.rb +++ b/spec/support/helpers/migrations_helpers.rb @@ -172,4 +172,4 @@ module MigrationsHelpers end end -MigrationsHelpers.prepend_if_ee('EE::MigrationsHelpers') +MigrationsHelpers.prepend_mod_with('MigrationsHelpers') diff --git a/spec/support/helpers/next_found_instance_of.rb b/spec/support/helpers/next_found_instance_of.rb index feb63f90211..c8cdbaf2c5d 100644 --- a/spec/support/helpers/next_found_instance_of.rb +++ b/spec/support/helpers/next_found_instance_of.rb @@ -22,7 +22,7 @@ module NextFoundInstanceOf private def check_if_active_record!(klass) - raise ArgumentError.new(ERROR_MESSAGE) unless klass < ActiveRecord::Base + raise ArgumentError, ERROR_MESSAGE unless klass < ActiveRecord::Base end def stub_allocate(target, klass) diff --git a/spec/support/helpers/query_recorder.rb b/spec/support/helpers/query_recorder.rb index 2d880c7a8fe..05afbc336da 100644 --- a/spec/support/helpers/query_recorder.rb +++ b/spec/support/helpers/query_recorder.rb @@ -13,6 +13,10 @@ module ActiveRecord @skip_cached = skip_cached @query_recorder_debug = ENV['QUERY_RECORDER_DEBUG'] || query_recorder_debug @log_file = log_file + record(&block) if block_given? + end + + def record(&block) # force replacement of bind parameters to give tests the ability to check for ids ActiveRecord::Base.connection.unprepared_statement do ActiveSupport::Notifications.subscribed(method(:callback), 'sql.active_record', &block) diff --git a/spec/support/helpers/redis_without_keys.rb b/spec/support/helpers/redis_without_keys.rb index e030f1028f7..ff64a3cf08e 100644 --- a/spec/support/helpers/redis_without_keys.rb +++ b/spec/support/helpers/redis_without_keys.rb @@ -4,7 +4,7 @@ class Redis ForbiddenCommand = Class.new(StandardError) def keys(*args) - raise ForbiddenCommand.new("Don't use `Redis#keys` as it iterates over all "\ - "keys in redis. Use `Redis#scan_each` instead.") + raise ForbiddenCommand, "Don't use `Redis#keys` as it iterates over all "\ + "keys in redis. Use `Redis#scan_each` instead." end end diff --git a/spec/support/helpers/reload_helpers.rb b/spec/support/helpers/reload_helpers.rb index 60811e4604f..368ebaaba8a 100644 --- a/spec/support/helpers/reload_helpers.rb +++ b/spec/support/helpers/reload_helpers.rb @@ -2,7 +2,7 @@ module ReloadHelpers def reload_models(*models) - models.map(&:reload) + models.compact.map(&:reload) end def subject_and_reload(*models) diff --git a/spec/support/helpers/require_migration.rb b/spec/support/helpers/require_migration.rb index c2902aa4ec7..8de71d3073f 100644 --- a/spec/support/helpers/require_migration.rb +++ b/spec/support/helpers/require_migration.rb @@ -20,7 +20,7 @@ class RequireMigration class << self def require_migration!(file_name) file_paths = search_migration_file(file_name) - raise AutoLoadError.new(file_name) unless file_paths.first + raise AutoLoadError, file_name unless file_paths.first require file_paths.first end @@ -41,7 +41,7 @@ class RequireMigration end end -RequireMigration.prepend_if_ee('EE::RequireMigration') +RequireMigration.prepend_mod_with('RequireMigration') def require_migration!(file_name = nil) location_info = caller_locations.first.path.match(RequireMigration::SPEC_FILE_PATTERN) diff --git a/spec/support/helpers/snowplow_helpers.rb b/spec/support/helpers/snowplow_helpers.rb index 70a4eadd8de..553739b5d30 100644 --- a/spec/support/helpers/snowplow_helpers.rb +++ b/spec/support/helpers/snowplow_helpers.rb @@ -60,6 +60,10 @@ module SnowplowHelpers .with(category, action, **kwargs).at_least(:once) end + def match_snowplow_context_schema(schema_path:, context:) + expect(context).to match_snowplow_schema(schema_path) + end + # Asserts that no call to `Gitlab::Tracking#event` was made. # # Example: @@ -71,7 +75,11 @@ module SnowplowHelpers # expect_no_snowplow_event # end # end - def expect_no_snowplow_event - expect(Gitlab::Tracking).not_to have_received(:event) # rubocop:disable RSpec/ExpectGitlabTracking + def expect_no_snowplow_event(category: nil, action: nil, **kwargs) + if category && action + expect(Gitlab::Tracking).not_to have_received(:event).with(category, action, **kwargs) # rubocop:disable RSpec/ExpectGitlabTracking + else + expect(Gitlab::Tracking).not_to have_received(:event) # rubocop:disable RSpec/ExpectGitlabTracking + end end end diff --git a/spec/support/helpers/stub_configuration.rb b/spec/support/helpers/stub_configuration.rb index 9851a3de9e9..8c60dc30cdb 100644 --- a/spec/support/helpers/stub_configuration.rb +++ b/spec/support/helpers/stub_configuration.rb @@ -163,4 +163,4 @@ end require_relative '../../../ee/spec/support/helpers/ee/stub_configuration' if Dir.exist?("#{__dir__}/../../../ee") -StubConfiguration.prepend_if_ee('EE::StubConfiguration') +StubConfiguration.prepend_mod_with('StubConfiguration') diff --git a/spec/support/helpers/stub_gitlab_calls.rb b/spec/support/helpers/stub_gitlab_calls.rb index 4da8f760056..3824ff2b68d 100644 --- a/spec/support/helpers/stub_gitlab_calls.rb +++ b/spec/support/helpers/stub_gitlab_calls.rb @@ -153,4 +153,4 @@ module StubGitlabCalls end end -StubGitlabCalls.prepend_if_ee('EE::StubGitlabCalls') +StubGitlabCalls.prepend_mod_with('StubGitlabCalls') diff --git a/spec/support/helpers/test_env.rb b/spec/support/helpers/test_env.rb index 7ba15a9c00b..40a3dbfbf25 100644 --- a/spec/support/helpers/test_env.rb +++ b/spec/support/helpers/test_env.rb @@ -52,7 +52,7 @@ module TestEnv 'wip' => 'b9238ee', 'csv' => '3dd0896', 'v1.1.0' => 'b83d6e3', - 'add-ipython-files' => '93ee732', + 'add-ipython-files' => 'f6b7a70', 'add-pdf-file' => 'e774ebd', 'squash-large-files' => '54cec52', 'add-pdf-text-binary' => '79faa7b', @@ -266,7 +266,7 @@ module TestEnv Integer(sleep_time / sleep_interval).times do Socket.unix(socket) return - rescue + rescue StandardError sleep sleep_interval end @@ -612,5 +612,5 @@ end require_relative('../../../ee/spec/support/helpers/ee/test_env') if Gitlab.ee? -::TestEnv.prepend_if_ee('::EE::TestEnv') -::TestEnv.extend_if_ee('::EE::TestEnv') +::TestEnv.prepend_mod_with('TestEnv') +::TestEnv.extend_mod_with('TestEnv') diff --git a/spec/support/helpers/usage_data_helpers.rb b/spec/support/helpers/usage_data_helpers.rb index d05676a649e..c6176b5bcbc 100644 --- a/spec/support/helpers/usage_data_helpers.rb +++ b/spec/support/helpers/usage_data_helpers.rb @@ -98,7 +98,6 @@ module UsageDataHelpers projects_prometheus_active projects_with_repositories_enabled projects_with_error_tracking_enabled - projects_with_alerts_service_enabled projects_with_enabled_alert_integrations projects_with_prometheus_alerts projects_with_tracing_enabled diff --git a/spec/support/matchers/access_matchers_generic.rb b/spec/support/matchers/access_matchers_generic.rb index 13955750f4f..a38a83a2547 100644 --- a/spec/support/matchers/access_matchers_generic.rb +++ b/spec/support/matchers/access_matchers_generic.rb @@ -35,7 +35,7 @@ module AccessMatchersGeneric run_matcher(action, role, @membership, @owned_objects) do |action| action.call - rescue => e + rescue StandardError => e @error = e raise unless e.is_a?(ERROR_CLASS) end diff --git a/spec/support/matchers/markdown_matchers.rb b/spec/support/matchers/markdown_matchers.rb index 47cffad8c41..dfdb5bc01ae 100644 --- a/spec/support/matchers/markdown_matchers.rb +++ b/spec/support/matchers/markdown_matchers.rb @@ -291,4 +291,4 @@ module RSpec::Matchers::DSL::Macros end end -MarkdownMatchers.prepend_if_ee('EE::MarkdownMatchers') +MarkdownMatchers.prepend_mod_with('MarkdownMatchers') diff --git a/spec/support/matchers/schema_matcher.rb b/spec/support/matchers/schema_matcher.rb index f0e7a52c51e..94e4359b1dd 100644 --- a/spec/support/matchers/schema_matcher.rb +++ b/spec/support/matchers/schema_matcher.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true module SchemaPath + @schema_cache = {} + def self.expand(schema, dir = nil) return schema unless schema.is_a?(String) @@ -12,38 +14,56 @@ module SchemaPath Rails.root.join(dir.to_s, 'spec', "fixtures/api/schemas/#{schema}.json").to_s end + + def self.validator(schema_path) + unless @schema_cache.key?(schema_path) + @schema_cache[schema_path] = JSONSchemer.schema(schema_path, ref_resolver: SchemaPath.file_ref_resolver) + end + + @schema_cache[schema_path] + end + + def self.file_ref_resolver + proc do |uri| + file = Rails.root.join(uri.path) + raise StandardError, "Ref file #{uri.path} must be json" unless uri.path.ends_with?('.json') + raise StandardError, "File #{file.to_path} doesn't exists" unless file.exist? + + Gitlab::Json.parse(File.read(file)) + end + end end RSpec::Matchers.define :match_response_schema do |schema, dir: nil, **options| match do |response| - @errors = JSON::Validator.fully_validate( - SchemaPath.expand(schema, dir), response.body, options) + schema_path = Pathname.new(SchemaPath.expand(schema, dir)) + validator = SchemaPath.validator(schema_path) - @errors.empty? - end + data = Gitlab::Json.parse(response.body) - failure_message do |response| - "didn't match the schema defined by #{SchemaPath.expand(schema, dir)}" \ - " The validation errors were:\n#{@errors.join("\n")}" + validator.valid?(data) end end -RSpec::Matchers.define :match_schema do |schema, dir: nil, **options| +RSpec::Matchers.define :match_snowplow_schema do |schema, dir: nil, **options| match do |data| - @errors = JSON::Validator.fully_validate( - SchemaPath.expand(schema, dir), data, options) - - @errors.empty? - end + schema_path = Pathname.new(Rails.root.join(dir.to_s, 'spec', "fixtures/product_intelligence/#{schema}.json").to_s) + validator = SchemaPath.validator(schema_path) - failure_message do |response| - "didn't match the schema defined by #{schema_name(schema, dir)}" \ - " The validation errors were:\n#{@errors.join("\n")}" + validator.valid?(data.stringify_keys) end +end - def schema_name(schema, dir) - return 'provided schema' unless schema.is_a?(String) +RSpec::Matchers.define :match_schema do |schema, dir: nil, **options| + match do |data| + schema = SchemaPath.expand(schema, dir) + schema = Pathname.new(schema) if schema.is_a?(String) + validator = SchemaPath.validator(schema) - SchemaPath.expand(schema, dir) + if data.is_a?(String) + validator.valid?(Gitlab::Json.parse(data)) + else + validator.valid?(data) + end end end diff --git a/spec/support/renameable_upload.rb b/spec/support/renameable_upload.rb index f7f00181605..08e8d07ed4d 100644 --- a/spec/support/renameable_upload.rb +++ b/spec/support/renameable_upload.rb @@ -5,7 +5,7 @@ class RenameableUpload < SimpleDelegator # Get a fixture file with a new unique name, and the same extension def self.unique_file(name) - upload = new(fixture_file_upload("spec/fixtures/#{name}")) + upload = new(Rack::Test::UploadedFile.new("spec/fixtures/#{name}")) ext = File.extname(name) new_name = File.basename(FactoryBot.generate(:filename), '.*') upload.original_filename = new_name + ext diff --git a/spec/support/services/issuable_update_service_shared_examples.rb b/spec/support/services/issuable_update_service_shared_examples.rb index 8867a1e3a90..4d2843af1c4 100644 --- a/spec/support/services/issuable_update_service_shared_examples.rb +++ b/spec/support/services/issuable_update_service_shared_examples.rb @@ -12,13 +12,13 @@ RSpec.shared_examples 'issuable update service' do context 'to reopened' do it 'executes hooks only once' do - described_class.new(project, user, state_event: 'reopen').execute(closed_issuable) + described_class.new(project: project, current_user: user, params: { state_event: 'reopen' }).execute(closed_issuable) end end context 'to closed' do it 'executes hooks only once' do - described_class.new(project, user, state_event: 'close').execute(open_issuable) + described_class.new(project: project, current_user: user, params: { state_event: 'close' }).execute(open_issuable) end end end diff --git a/spec/support/shared_contexts/navbar_structure_context.rb b/spec/support/shared_contexts/navbar_structure_context.rb index 78d14ecb880..4f8e88ae9da 100644 --- a/spec/support/shared_contexts/navbar_structure_context.rb +++ b/spec/support/shared_contexts/navbar_structure_context.rb @@ -24,16 +24,56 @@ 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(:structure) do [ - { - nav_item: _('Project overview'), - nav_sub_items: [ - _('Details'), - _('Activity'), - _('Releases') - ] - }, + project_information_nav_item, { nav_item: _('Repository'), nav_sub_items: [ @@ -52,7 +92,6 @@ RSpec.shared_context 'project navbar structure' do nav_sub_items: [ _('List'), _('Boards'), - _('Labels'), _('Service Desk'), _('Milestones'), (_('Iterations') if Gitlab.ee?) @@ -73,23 +112,7 @@ RSpec.shared_context 'project navbar structure' do ] }, security_and_compliance_nav_item, - { - nav_item: _('Operations'), - nav_sub_items: [ - _('Metrics'), - _('Logs'), - _('Tracing'), - _('Error Tracking'), - _('Alerts'), - _('Incidents'), - _('Serverless'), - _('Terraform'), - _('Kubernetes'), - _('Environments'), - _('Feature Flags'), - _('Product Analytics') - ] - }, + monitor_nav_item, analytics_nav_item, { nav_item: _('Wiki'), @@ -100,20 +123,8 @@ RSpec.shared_context 'project navbar structure' do nav_sub_items: [] }, { - nav_item: _('Members'), - nav_sub_items: [] - }, - { nav_item: _('Settings'), - nav_sub_items: [ - _('General'), - _('Integrations'), - _('Webhooks'), - _('Access Tokens'), - _('Repository'), - _('CI/CD'), - _('Operations') - ].compact + nav_sub_items: settings_menu_items } ].compact end @@ -124,8 +135,7 @@ RSpec.shared_context 'group navbar structure' do { nav_item: _('Analytics'), nav_sub_items: [ - _('Contribution'), - _('DevOps Adoption') + _('Contribution') ] } end @@ -171,23 +181,31 @@ 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'), + _('Board'), + _('Milestones') + ] + end + let(:structure) do [ - { - nav_item: _('Group overview'), - nav_sub_items: [ - _('Details'), - _('Activity') - ] - }, + group_information_nav_item, { nav_item: _('Issues'), - nav_sub_items: [ - _('List'), - _('Board'), - _('Labels'), - _('Milestones') - ] + nav_sub_items: issues_nav_items }, { nav_item: _('Merge requests'), @@ -199,11 +217,7 @@ RSpec.shared_context 'group navbar structure' do nav_item: _('Kubernetes'), nav_sub_items: [] }, - (analytics_nav_item if Gitlab.ee?), - { - nav_item: _('Members'), - nav_sub_items: [] - } + (analytics_nav_item if Gitlab.ee?) ] end end diff --git a/spec/support/shared_contexts/policies/project_policy_shared_context.rb b/spec/support/shared_contexts/policies/project_policy_shared_context.rb index 266c8d5ee84..35dc709b5d9 100644 --- a/spec/support/shared_contexts/policies/project_policy_shared_context.rb +++ b/spec/support/shared_contexts/policies/project_policy_shared_context.rb @@ -15,7 +15,7 @@ RSpec.shared_context 'ProjectPolicy context' do let(:base_guest_permissions) do %i[ - award_emoji create_issue create_merge_request_in create_note + award_emoji create_issue create_incident create_merge_request_in create_note create_project read_issue_board read_issue read_issue_iid read_issue_link read_label read_issue_board_list read_milestone read_note read_project read_project_for_iids read_project_member read_release read_snippet diff --git a/spec/support/shared_contexts/project_service_shared_context.rb b/spec/support/shared_contexts/project_service_shared_context.rb index a8e75c624e8..0e3540a3e15 100644 --- a/spec/support/shared_contexts/project_service_shared_context.rb +++ b/spec/support/shared_contexts/project_service_shared_context.rb @@ -22,7 +22,7 @@ RSpec.shared_context 'project service activation' do end def click_active_checkbox - find('input[name="service[active]"]').click + find('label', text: 'Active').click end def click_save_integration diff --git a/spec/support/shared_contexts/services_shared_context.rb b/spec/support/shared_contexts/services_shared_context.rb index f250632ff51..34c92367efa 100644 --- a/spec/support/shared_contexts/services_shared_context.rb +++ b/spec/support/shared_contexts/services_shared_context.rb @@ -1,12 +1,12 @@ # frozen_string_literal: true -Service.available_services_names.each do |service| +Integration.available_services_names.each do |service| RSpec.shared_context service do include JiraServiceHelper if service == 'jira' let(:dashed_service) { service.dasherize } let(:service_method) { "#{service}_service".to_sym } - let(:service_klass) { "#{service}_service".classify.constantize } + let(:service_klass) { Integration.service_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 } } @@ -30,6 +30,8 @@ Service.available_services_names.each do |service| hash.merge!(k => '1,2,3') elsif service == 'emails_on_push' && k == :recipients hash.merge!(k => 'foo@bar.com') + elsif service == 'slack' || service == 'mattermost' && k == :labels_to_be_notified_behavior + hash.merge!(k => "match_any") else hash.merge!(k => "someword") end @@ -47,8 +49,9 @@ Service.available_services_names.each do |service| stub_jira_service_test if service == 'jira' end - def initialize_service(service) + 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 diff --git a/spec/support/shared_contexts/spam_constants.rb b/spec/support/shared_contexts/spam_constants.rb index 813f9d00123..e88a7c1b0df 100644 --- a/spec/support/shared_contexts/spam_constants.rb +++ b/spec/support/shared_contexts/spam_constants.rb @@ -6,5 +6,6 @@ RSpec.shared_context 'includes Spam constants' do stub_const('DISALLOW', Spam::SpamConstants::DISALLOW) stub_const('ALLOW', Spam::SpamConstants::ALLOW) stub_const('BLOCK_USER', Spam::SpamConstants::BLOCK_USER) + stub_const('NOOP', Spam::SpamConstants::NOOP) end end diff --git a/spec/support/shared_examples/alert_notification_service_shared_examples.rb b/spec/support/shared_examples/alert_notification_service_shared_examples.rb deleted file mode 100644 index fc935effe0e..00000000000 --- a/spec/support/shared_examples/alert_notification_service_shared_examples.rb +++ /dev/null @@ -1,44 +0,0 @@ -# frozen_string_literal: true - -RSpec.shared_examples 'Alert Notification Service sends notification email' do - let(:notification_service) { spy } - - it 'sends a notification' do - expect(NotificationService) - .to receive(:new) - .and_return(notification_service) - - expect(notification_service) - .to receive_message_chain(:async, :prometheus_alerts_fired) - - expect(subject).to be_success - end -end - -RSpec.shared_examples 'Alert Notification Service sends no notifications' do |http_status: nil| - it 'does not notify' do - expect(NotificationService).not_to receive(:new) - - if http_status.present? - expect(subject).to be_error - expect(subject.http_status).to eq(http_status) - else - expect(subject).to be_success - end - end -end - -RSpec.shared_examples 'creates status-change system note for an auto-resolved alert' do - it 'has 2 new system notes' do - expect { subject }.to change(Note, :count).by(2) - expect(Note.last.note).to include('Resolved') - end -end - -# Requires `source` to be defined -RSpec.shared_examples 'creates single system note based on the source of the alert' do - it 'has one new system note' do - expect { subject }.to change(Note, :count).by(1) - expect(Note.last.note).to include(source) - end -end diff --git a/spec/support/shared_examples/boards/lists/update_service_shared_examples.rb b/spec/support/shared_examples/boards/lists/update_service_shared_examples.rb index d8a74f2582d..1fab31cd513 100644 --- a/spec/support/shared_examples/boards/lists/update_service_shared_examples.rb +++ b/spec/support/shared_examples/boards/lists/update_service_shared_examples.rb @@ -2,14 +2,30 @@ RSpec.shared_examples 'moving list' do context 'when user can admin list' do - it 'calls Lists::MoveService to update list position' do + before do board.resource_parent.add_developer(user) + end + + context 'when the new position is valid' do + it 'calls Lists::MoveService to update list position' do + expect_next_instance_of(Boards::Lists::MoveService, board.resource_parent, user, params) do |move_service| + expect(move_service).to receive(:execute).with(list).and_call_original + end - expect_next_instance_of(Boards::Lists::MoveService, board.resource_parent, user, params) do |move_service| - expect(move_service).to receive(:execute).with(list).and_call_original + service.execute(list) end - service.execute(list) + it 'returns a success response' do + expect(service.execute(list)).to be_success + end + end + + context 'when the new position is invalid' do + let(:params) { { position: 10 } } + + it 'returns error response' do + expect(service.execute(list)).to be_error + end end end @@ -19,6 +35,10 @@ RSpec.shared_examples 'moving list' do service.execute(list) end + + it 'returns an error response' do + expect(service.execute(list)).to be_error + 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 dd71107455f..70a684c12bf 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 @@ -40,7 +40,7 @@ RSpec.shared_examples 'project access tokens available #create' do it 'returns success message' do subject - expect(response.flash[:notice]).to match('Your new project access token has been created.') + expect(controller).to set_flash[:notice].to match('Your new project access token has been created.') end it 'creates project access token' do @@ -88,7 +88,7 @@ RSpec.shared_examples 'project access tokens available #create' do it 'shows a failure alert' do subject - expect(response.flash[:alert]).to match("Failed to create new project access token: Failed!") + expect(controller).to set_flash[:alert].to match("Failed to create new project access token: Failed!") end end end diff --git a/spec/support/shared_examples/controllers/issuable_notes_filter_shared_examples.rb b/spec/support/shared_examples/controllers/issuable_notes_filter_shared_examples.rb index 5ecc5c08bbd..a4eb6a839c0 100644 --- a/spec/support/shared_examples/controllers/issuable_notes_filter_shared_examples.rb +++ b/spec/support/shared_examples/controllers/issuable_notes_filter_shared_examples.rb @@ -35,7 +35,7 @@ RSpec.shared_examples 'issuable notes filter' do get :discussions, params: params.merge(notes_filter: notes_filter) end - it 'does not set notes filter when database is in read only mode' do + it 'does not set notes filter when database is in read-only mode' do allow(Gitlab::Database).to receive(:read_only?).and_return(true) notes_filter = UserPreference::NOTES_FILTERS[:only_comments] 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 0a040557ffe..cfee26a0d6a 100644 --- a/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb +++ b/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb @@ -130,8 +130,8 @@ RSpec.shared_examples 'wiki controller actions' do it_behaves_like 'fetching history', :ok do let(:allow_read_wiki) { true } - it 'assigns @page_versions' do - expect(assigns(:page_versions)).to be_present + it 'assigns @commits' do + expect(assigns(:commits)).to be_present end end diff --git a/spec/support/shared_examples/features/board_sidebar_labels_examples.rb b/spec/support/shared_examples/features/board_sidebar_labels_examples.rb new file mode 100644 index 00000000000..520980c2615 --- /dev/null +++ b/spec/support/shared_examples/features/board_sidebar_labels_examples.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +RSpec.shared_context 'labels from nested groups and projects' do + let_it_be(:group) { create(:group) } + let_it_be(:group_label) { create(:group_label, group: group, name: 'Group label') } + let_it_be(:project) { create(:project, group: group) } + let_it_be(:project_label) { create(:label, project: project, name: 'Project label') } + + let_it_be(:subgroup) { create(:group, parent: group) } + let_it_be(:subgroup_label) { create(:group_label, group: subgroup, name: 'Subgroup label') } + let_it_be(:subproject) { create(:project, group: subgroup) } + let_it_be(:subproject_label) { create(:label, project: subproject, name: 'Subproject label') } + + let_it_be(:subgroup2) { create(:group, parent: group) } + let_it_be(:subgroup2_label) { create(:group_label, group: subgroup2, name: 'Subgroup2 label') } + + let_it_be(:maintainer) { create(:user) } + + let(:labels_select) { find("[data-testid='sidebar-labels']") } + let(:labels_dropdown) { labels_select.find('[data-testid="dropdown-content"]')} + + before do + group.add_maintainer(maintainer) + + sign_in(maintainer) + end +end + +RSpec.shared_examples "an issue from a subgroup's project is selected" do + context 'when editing labels' do + before do + click_card_and_edit_label + end + + it 'displays the label from the top-level group' do + expect(labels_dropdown).to have_content(group_label.name) + end + + it 'displays the label from the subgroup' do + expect(labels_dropdown).to have_content(subgroup_label.name) + end + + it 'displays the label from the project' do + expect(labels_dropdown).to have_content(subproject_label.name) + end + + it "does not display labels from the subgroup's siblings (project or group)" do + aggregate_failures do + expect(labels_dropdown).not_to have_content(project_label.name) + expect(labels_dropdown).not_to have_content(subgroup2_label.name) + end + end + end +end + +RSpec.shared_examples 'an issue from a direct descendant project is selected' do + context 'when editing labels' do + before do + click_card_and_edit_label + end + + it 'displays the label from the top-level group' do + expect(labels_dropdown).to have_content(group_label.name) + end + + it 'displays the label from the project' do + expect(labels_dropdown).to have_content(project_label.name) + end + + it "does not display labels from the project's siblings or their descendents" do + aggregate_failures do + expect(labels_dropdown).not_to have_content(subgroup_label.name) + expect(labels_dropdown).not_to have_content(subproject_label.name) + end + end + end +end diff --git a/spec/support/shared_examples/features/issuable_invite_members_shared_examples.rb b/spec/support/shared_examples/features/issuable_invite_members_shared_examples.rb index 49c3674277d..736c353c2aa 100644 --- a/spec/support/shared_examples/features/issuable_invite_members_shared_examples.rb +++ b/spec/support/shared_examples/features/issuable_invite_members_shared_examples.rb @@ -22,32 +22,7 @@ RSpec.shared_examples 'issuable invite members experiments' do end end - context 'when invite_members_version_b experiment is enabled' do - before do - stub_experiment_for_subject(invite_members_version_b: true) - end - - it 'shows a link for inviting members and follows through to modal' do - project.add_developer(user) - visit issuable_path - - find('.block.assignee .edit-link').click - - wait_for_requests - - page.within '.dropdown-menu-user' do - expect(page).to have_link('Invite Members', href: '#') - expect(page).to have_selector('[data-track-event="click_invite_members_version_b"]') - expect(page).to have_selector('[data-track-label="edit_assignee"]') - end - - click_link 'Invite Members' - - expect(page).to have_content("Oops, this feature isn't ready yet") - end - end - - context 'when invite_members_version_b experiment is disabled' do + context 'when user cannot invite members in assignee dropdown' do it 'shows author in assignee dropdown and no invite link' do project.add_developer(user) visit issuable_path diff --git a/spec/support/shared_examples/features/sidebar_shared_examples.rb b/spec/support/shared_examples/features/sidebar_shared_examples.rb index 429efbe6ba0..c9508818f74 100644 --- a/spec/support/shared_examples/features/sidebar_shared_examples.rb +++ b/spec/support/shared_examples/features/sidebar_shared_examples.rb @@ -44,22 +44,24 @@ RSpec.shared_examples 'issue boards sidebar' do context 'in notifications subscription' do it 'displays notifications toggle', :aggregate_failures do page.within('[data-testid="sidebar-notifications"]') do - expect(page).to have_selector('[data-testid="notification-subscribe-toggle"]') + expect(page).to have_selector('[data-testid="subscription-toggle"]') expect(page).to have_content('Notifications') - expect(page).not_to have_content('Notifications have been disabled by the project or group owner') + expect(page).not_to have_content('Disabled by project owner') end end it 'shows toggle as on then as off as user toggles to subscribe and unsubscribe', :aggregate_failures do - toggle = find('[data-testid="notification-subscribe-toggle"]') + wait_for_requests - toggle.click + click_button 'Notifications' - expect(toggle).to have_css("button.is-checked") + expect(page).to have_button('Notifications', class: 'is-checked') - toggle.click + click_button 'Notifications' - expect(toggle).not_to have_css("button.is-checked") + wait_for_requests + + expect(page).not_to have_button('Notifications', class: 'is-checked') end context 'when notifications have been disabled' do @@ -71,9 +73,28 @@ RSpec.shared_examples 'issue boards sidebar' do it 'displays a message that notifications have been disabled' do page.within('[data-testid="sidebar-notifications"]') do - expect(page).not_to have_selector('[data-testid="notification-subscribe-toggle"]') - expect(page).to have_content('Notifications have been disabled by the project or group owner') + expect(page).to have_button('Notifications', class: 'is-disabled') + expect(page).to have_content('Disabled by project owner') + end + end + end + end + + context 'confidentiality' do + it 'make issue confidential' do + page.within('.confidentiality') do + expect(page).to have_content('Not confidential') + + click_button 'Edit' + expect(page).to have_css('.sidebar-item-warning-message') + + within('.sidebar-item-warning-message') do + click_button 'Turn on' end + + wait_for_requests + + expect(page).to have_content('This issue is confidential') end end end diff --git a/spec/support/shared_examples/features/variable_list_shared_examples.rb b/spec/support/shared_examples/features/variable_list_shared_examples.rb index 2fd88b610e9..4b94411f009 100644 --- a/spec/support/shared_examples/features/variable_list_shared_examples.rb +++ b/spec/support/shared_examples/features/variable_list_shared_examples.rb @@ -8,7 +8,7 @@ RSpec.shared_examples 'variable list' do end it 'adds a new CI variable' do - click_button('Add Variable') + click_button('Add variable') fill_variable('key', 'key_value') do click_button('Add variable') @@ -22,7 +22,7 @@ RSpec.shared_examples 'variable list' do end it 'adds a new protected variable' do - click_button('Add Variable') + click_button('Add variable') fill_variable('key', 'key_value') do click_button('Add variable') @@ -37,7 +37,7 @@ RSpec.shared_examples 'variable list' do end it 'defaults to unmasked' do - click_button('Add Variable') + click_button('Add variable') fill_variable('key', 'key_value') do click_button('Add variable') @@ -149,7 +149,7 @@ RSpec.shared_examples 'variable list' do end it 'shows a validation error box about duplicate keys' do - click_button('Add Variable') + click_button('Add variable') fill_variable('key', 'key_value') do click_button('Add variable') @@ -157,7 +157,7 @@ RSpec.shared_examples 'variable list' do wait_for_requests - click_button('Add Variable') + click_button('Add variable') fill_variable('key', 'key_value') do click_button('Add variable') @@ -170,7 +170,7 @@ RSpec.shared_examples 'variable list' do end it 'prevents a variable to be added if no values are provided when a variable is set to masked' do - click_button('Add Variable') + click_button('Add variable') page.within('#add-ci-variable') do find('[data-qa-selector="ci_variable_key_field"] input').set('empty_mask_key') @@ -182,7 +182,7 @@ RSpec.shared_examples 'variable list' do end it 'shows validation error box about unmaskable values' do - click_button('Add Variable') + click_button('Add variable') fill_variable('empty_mask_key', '???', protected: true, masked: true) do expect(page).to have_content('This variable can not be masked') @@ -192,7 +192,7 @@ RSpec.shared_examples 'variable list' do it 'handles multiple edits and a deletion' do # Create two variables - click_button('Add Variable') + click_button('Add variable') fill_variable('akey', 'akeyvalue') do click_button('Add variable') @@ -200,7 +200,7 @@ RSpec.shared_examples 'variable list' do wait_for_requests - click_button('Add Variable') + click_button('Add variable') fill_variable('zkey', 'zkeyvalue') do click_button('Add variable') @@ -224,7 +224,7 @@ RSpec.shared_examples 'variable list' do wait_for_requests # Add another variable - click_button('Add Variable') + click_button('Add variable') fill_variable('ckey', 'ckeyvalue') do click_button('Add variable') @@ -249,7 +249,7 @@ RSpec.shared_examples 'variable list' do end it 'defaults to protected' do - click_button('Add Variable') + click_button('Add variable') page.within('#add-ci-variable') do expect(find('[data-testid="ci-variable-protected-checkbox"]')).to be_checked @@ -269,7 +269,7 @@ RSpec.shared_examples 'variable list' do end it 'defaults to unprotected' do - click_button('Add Variable') + click_button('Add variable') page.within('#add-ci-variable') do expect(find('[data-testid="ci-variable-protected-checkbox"]')).not_to be_checked 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 8a6d5d88ca6..f2576931642 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 @@ -24,8 +24,8 @@ RSpec.shared_examples 'User creates wiki page' do page.within(".wiki-form") do fill_in(:wiki_content, with: "") - page.execute_script("window.onbeforeunload = null") 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") 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 d185e9dd81c..db2a96d9649 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 @@ -93,8 +93,8 @@ RSpec.shared_examples 'User updates wiki page' do it 'shows a validation error message if the form is force submitted', :js do fill_in(:wiki_content, with: '') - page.execute_script("window.onbeforeunload = null") 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') @@ -117,14 +117,6 @@ RSpec.shared_examples 'User updates wiki page' do expect(page).to have_selector('.atwho-view') end - it 'shows the error message', :js do - wiki_page.update(content: 'Update') # rubocop:disable Rails/SaveBang - - click_button('Save changes') - - expect(page).to have_content('Someone edited the page the same time you did.') - end - it 'updates a page', :js do fill_in('Content', with: 'Updated Wiki Content') click_on('Save changes') @@ -145,6 +137,18 @@ RSpec.shared_examples 'User updates wiki page' do end it_behaves_like 'wiki file attachments' + + context 'when multiple people edit the page at the same time' do + it 'preserves user changes in the wiki editor', :js do + wiki_page.update(content: 'Some Other Updates') # rubocop:disable Rails/SaveBang + + fill_in('Content', with: 'Updated Wiki Content') + click_on('Save changes') + + expect(page).to have_content('Someone edited the page the same time you did.') + expect(find('textarea#wiki_content').value).to eq('Updated Wiki Content') + end + end end context 'when the page is in a subdir', :js do diff --git a/spec/support/shared_examples/finders/packages_shared_examples.rb b/spec/support/shared_examples/finders/packages_shared_examples.rb index 2d4e8d0df1f..b3ec2336cca 100644 --- a/spec/support/shared_examples/finders/packages_shared_examples.rb +++ b/spec/support/shared_examples/finders/packages_shared_examples.rb @@ -20,9 +20,11 @@ end RSpec.shared_examples 'concerning package statuses' do let_it_be(:hidden_package) { create(:maven_package, :hidden, project: project) } + let_it_be(:error_package) { create(:maven_package, :error, project: project) } - context 'hidden packages' do + context 'displayable packages' do it { is_expected.not_to include(hidden_package) } + it { is_expected.to include(error_package) } end context 'with status param' do diff --git a/spec/support/shared_examples/graphql/mutations/boards/update_list_shared_examples.rb b/spec/support/shared_examples/graphql/mutations/boards/update_list_shared_examples.rb new file mode 100644 index 00000000000..4385cd519be --- /dev/null +++ b/spec/support/shared_examples/graphql/mutations/boards/update_list_shared_examples.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'update board list mutation' do + describe '#resolve' do + let(:mutation) { described_class.new(object: nil, context: { current_user: current_user }, field: nil) } + let(:list_update_params) { { position: 1, collapsed: true } } + + subject { mutation.resolve(list: list, **list_update_params) } + + before_all do + group.add_reporter(reporter) + group.add_guest(guest) + list.update_preferences_for(reporter, collapsed: false) + end + + context 'with permission to admin board lists' do + let(:current_user) { reporter } + + it 'updates the list position and collapsed state as expected' do + subject + + reloaded_list = list.reload + expect(reloaded_list.position).to eq(1) + expect(reloaded_list.collapsed?(current_user)).to eq(true) + end + end + + context 'with permission to read board lists' do + let(:current_user) { guest } + + it 'updates the list collapsed state but not the list position' do + subject + + reloaded_list = list.reload + expect(reloaded_list.position).to eq(0) + expect(reloaded_list.collapsed?(current_user)).to eq(true) + end + end + + context 'without permission to read board lists' do + let(:current_user) { create(:user) } + + it 'raises Resource Not Found error' do + expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) + end + end + end +end diff --git a/spec/support/shared_examples/graphql/mutations/security/ci_configuration_shared_examples.rb b/spec/support/shared_examples/graphql/mutations/security/ci_configuration_shared_examples.rb new file mode 100644 index 00000000000..2bb3d807aa7 --- /dev/null +++ b/spec/support/shared_examples/graphql/mutations/security/ci_configuration_shared_examples.rb @@ -0,0 +1,114 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.shared_examples_for 'graphql mutations security ci configuration' do + let_it_be(:project) { create(:project, :public, :repository) } + let_it_be(:user) { create(:user) } + + let(:branch) do + "set-secret-config" + end + + let(:success_path) do + "http://127.0.0.1:3000/root/demo-historic-secrets/-/merge_requests/new?" + end + + let(:service_response) do + ServiceResponse.success(payload: { branch: branch, success_path: success_path }) + end + + let(:error) { "An error occured!" } + + let(:service_error_response) do + ServiceResponse.error(message: error) + end + + specify { expect(described_class).to require_graphql_authorizations(:push_code) } + + describe '#resolve' do + let(:result) { subject } + + it 'raises an error if the resource is not accessible to the user' do + expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) + end + + context 'when user does not have enough permissions' do + before do + project.add_guest(user) + end + + it 'raises an error' do + expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) + end + end + + context 'when user is a maintainer of a different project' do + before do + create(:project_empty_repo).add_maintainer(user) + end + + it 'raises an error' do + expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) + end + end + + context 'when the user does not have permission to create a new branch' do + let(:error_message) { 'You are not allowed to create protected branches on this project.' } + + before do + project.add_developer(user) + + allow_next_instance_of(::Files::MultiService) do |multi_service| + allow(multi_service).to receive(:execute).and_raise(Gitlab::Git::PreReceiveError.new("GitLab: #{error_message}")) + end + end + + it 'returns an array of errors' do + expect(result).to match( + branch: be_nil, + success_path: be_nil, + errors: match_array([error_message]) + ) + end + end + + context 'when the user can create a merge request' do + before do + project.add_developer(user) + end + + context 'when service successfully generates a path to create a new merge request' do + before do + allow_next_instance_of(service) do |service| + allow(service).to receive(:execute).and_return(service_response) + end + end + + it 'returns a success path' do + expect(result).to match( + branch: branch, + success_path: success_path, + errors: [] + ) + end + end + + context 'when service can not generate any path to create a new merge request' do + before do + allow_next_instance_of(service) do |service| + allow(service).to receive(:execute).and_return(service_error_response) + end + end + + it 'returns an array of errors' do + expect(result).to match( + branch: be_nil, + success_path: be_nil, + errors: match_array([error]) + ) + end + end + end + end +end diff --git a/spec/support/shared_examples/graphql/resolvers/packages_resolvers_shared_examples.rb b/spec/support/shared_examples/graphql/resolvers/packages_resolvers_shared_examples.rb new file mode 100644 index 00000000000..3d6fec85490 --- /dev/null +++ b/spec/support/shared_examples/graphql/resolvers/packages_resolvers_shared_examples.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'group and projects packages resolver' do + context 'without sort' do + let_it_be(:npm_package) { create(:package, project: project) } + + it { is_expected.to contain_exactly(npm_package) } + end + + context 'with sorting and filtering' do + let_it_be(:conan_package) do + create(:conan_package, name: 'bar', project: project, created_at: 1.day.ago, version: "1.0.0", status: 'default') + end + + let_it_be(:maven_package) do + create(:maven_package, name: 'foo', project: project, created_at: 1.hour.ago, version: "2.0.0", status: 'error') + end + + let_it_be(:repository3) do + create(:maven_package, name: 'baz', project: project, created_at: 1.minute.ago, version: nil) + end + + [:created_desc, :name_desc, :version_desc, :type_asc].each do |order| + context "#{order}" do + let(:args) { { sort: order } } + + it { is_expected.to eq([maven_package, conan_package]) } + end + end + + [:created_asc, :name_asc, :version_asc, :type_desc].each do |order| + context "#{order}" do + let(:args) { { sort: order } } + + it { is_expected.to eq([conan_package, maven_package]) } + end + end + + context 'filter by package_name' do + let(:args) { { package_name: 'bar', sort: :created_desc } } + + it { is_expected.to eq([conan_package]) } + end + + context 'filter by package_type' do + let(:args) { { package_type: 'conan', sort: :created_desc } } + + it { is_expected.to eq([conan_package]) } + end + + context 'filter by status' do + let(:args) { { status: 'error', sort: :created_desc } } + + it { is_expected.to eq([maven_package]) } + end + + context 'include_versionless' do + let(:args) { { include_versionless: true, sort: :created_desc } } + + it { is_expected.to include(repository3) } + end + end +end diff --git a/spec/support/shared_examples/lib/gitlab/diff_file_collections_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/diff_file_collections_shared_examples.rb index c9e03ced0dd..1f7325df11a 100644 --- a/spec/support/shared_examples/lib/gitlab/diff_file_collections_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/diff_file_collections_shared_examples.rb @@ -166,16 +166,6 @@ shared_examples_for 'sortable diff files' do it 'returns sorted diff files' do expect(raw_diff_files_paths).to eq(sorted_diff_files_paths) end - - context 'when sort_diffs feature flag is disabled' do - before do - stub_feature_flags(sort_diffs: false) - end - - it 'returns unsorted diff files' do - expect(raw_diff_files_paths).to eq(unsorted_diff_files_paths) - end - end end end end diff --git a/spec/support/shared_examples/lib/gitlab/jwt_token_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/jwt_token_shared_examples.rb new file mode 100644 index 00000000000..5c92bb3b0d4 --- /dev/null +++ b/spec/support/shared_examples/lib/gitlab/jwt_token_shared_examples.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'a gitlab jwt token' do + let_it_be(:base_secret) { SecureRandom.base64(64) } + + let(:jwt_secret) do + OpenSSL::HMAC.hexdigest( + 'SHA256', + base_secret, + described_class::HMAC_KEY + ) + end + + before do + allow(Settings).to receive(:attr_encrypted_db_key_base).and_return(base_secret) + end + + describe '#secret' do + subject { described_class.secret } + + it { is_expected.to eq(jwt_secret) } + end + + describe '#decode' do + let(:encoded_jwt_token) { jwt_token.encoded } + + subject(:decoded_jwt_token) { described_class.decode(encoded_jwt_token) } + + context 'with a custom payload' do + let(:personal_access_token) { create(:personal_access_token) } + let(:jwt_token) { described_class.new.tap { |jwt_token| jwt_token['token'] = personal_access_token.token } } + + it 'returns the correct token' do + expect(decoded_jwt_token['token']).to eq jwt_token['token'] + end + + it 'returns nil and logs the exception after expiration' do + travel_to((described_class::HMAC_EXPIRES_IN + 1.minute).ago) do + encoded_jwt_token + end + + expect(Gitlab::ErrorTracking).to receive(:track_exception) + .with(instance_of(JWT::ExpiredSignature)) + + expect(decoded_jwt_token).to be_nil + end + end + end +end diff --git a/spec/support/shared_examples/lib/gitlab/sql/set_operator_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/sql/set_operator_shared_examples.rb index aa6a51c3646..8d758ed1655 100644 --- a/spec/support/shared_examples/lib/gitlab/sql/set_operator_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/sql/set_operator_shared_examples.rb @@ -21,7 +21,7 @@ RSpec.shared_examples 'SQL set operator' do |operator_keyword| expect(set_operator.to_sql).to eq("(#{to_sql(relation_1)})\n#{operator_keyword}\n(#{to_sql(relation_2)})") end - it 'skips Model.none segements' do + it 'skips Model.none segments' do empty_relation = User.none set_operator = described_class.new([empty_relation, relation_1, relation_2]) diff --git a/spec/support/shared_examples/models/chat_service_shared_examples.rb b/spec/support/shared_examples/models/chat_service_shared_examples.rb index 59e249bb865..4a47aad0957 100644 --- a/spec/support/shared_examples/models/chat_service_shared_examples.rb +++ b/spec/support/shared_examples/models/chat_service_shared_examples.rb @@ -163,7 +163,7 @@ RSpec.shared_examples "chat service" do |service_name| context "with issue events" do let(:opts) { { title: "Awesome issue", description: "please fix" } } let(:sample_data) do - service = Issues::CreateService.new(project, user, opts) + service = Issues::CreateService.new(project: project, current_user: user, params: opts) issue = service.execute service.hook_data(issue, "open") end @@ -182,7 +182,7 @@ RSpec.shared_examples "chat service" do |service_name| end let(:sample_data) do - service = MergeRequests::CreateService.new(project, user, opts) + service = MergeRequests::CreateService.new(project: project, current_user: user, params: opts) merge_request = service.execute service.hook_data(merge_request, "open") end diff --git a/spec/support/shared_examples/models/chat_slash_commands_shared_examples.rb b/spec/support/shared_examples/models/chat_slash_commands_shared_examples.rb index 0ee24dd93d7..49729afce61 100644 --- a/spec/support/shared_examples/models/chat_slash_commands_shared_examples.rb +++ b/spec/support/shared_examples/models/chat_slash_commands_shared_examples.rb @@ -81,7 +81,7 @@ RSpec.shared_examples 'chat slash commands service' do end context 'when the user is authenticated' do - let!(:chat_name) { create(:chat_name, service: subject) } + let!(:chat_name) { create(:chat_name, integration: subject) } let(:params) { { token: 'token', team_id: chat_name.team_id, user_id: chat_name.chat_id } } subject do diff --git a/spec/support/shared_examples/models/clusters/elastic_stack_client_shared.rb b/spec/support/shared_examples/models/clusters/elastic_stack_client_shared.rb new file mode 100644 index 00000000000..d3ce916cd64 --- /dev/null +++ b/spec/support/shared_examples/models/clusters/elastic_stack_client_shared.rb @@ -0,0 +1,82 @@ +# frozen_string_literal: true + +# Input +# - factory: [:clusters_applications_elastic_stack, :clusters_integrations_elastic_stack] +RSpec.shared_examples 'cluster-based #elasticsearch_client' do |factory| + describe '#elasticsearch_client' do + context 'cluster is nil' do + subject { build(factory, cluster: nil) } + + it 'returns nil' do + expect(subject.cluster).to be_nil + expect(subject.elasticsearch_client).to be_nil + end + end + + context "cluster doesn't have kubeclient" do + let(:cluster) { create(:cluster) } + + subject { create(factory, cluster: cluster) } + + it 'returns nil' do + expect(subject.elasticsearch_client).to be_nil + end + end + + context 'cluster has kubeclient' do + let(:cluster) { create(:cluster, :project, :provided_by_gcp) } + let(:kubernetes_url) { subject.cluster.platform_kubernetes.api_url } + let(:kube_client) { subject.cluster.kubeclient.core_client } + + subject { create(factory, cluster: cluster) } + + before do + subject.cluster.platform_kubernetes.namespace = 'a-namespace' + stub_kubeclient_discover(cluster.platform_kubernetes.api_url) + + create(:cluster_kubernetes_namespace, + cluster: cluster, + cluster_project: cluster.cluster_project, + project: cluster.cluster_project.project) + end + + it 'creates proxy elasticsearch_client' do + expect(subject.elasticsearch_client).to be_instance_of(Elasticsearch::Transport::Client) + end + + it 'copies proxy_url, options and headers from kube client to elasticsearch_client' do + expect(Elasticsearch::Client) + .to(receive(:new)) + .with(url: a_valid_url) + .and_call_original + + client = subject.elasticsearch_client + faraday_connection = client.transport.connections.first.connection + + expect(faraday_connection.headers["Authorization"]).to eq(kube_client.headers[:Authorization]) + expect(faraday_connection.ssl.cert_store).to be_instance_of(OpenSSL::X509::Store) + expect(faraday_connection.ssl.verify).to eq(1) + expect(faraday_connection.options.timeout).to be_nil + end + + context 'when cluster is not reachable' do + before do + allow(kube_client).to receive(:proxy_url).and_raise(Kubeclient::HttpError.new(401, 'Unauthorized', nil)) + end + + it 'returns nil' do + expect(subject.elasticsearch_client).to be_nil + end + end + + context 'when timeout is provided' do + it 'sets timeout in elasticsearch_client' do + client = subject.elasticsearch_client(timeout: 123) + faraday_connection = client.transport.connections.first.connection + + expect(faraday_connection.options.timeout).to eq(123) + end + end + end + end +end diff --git a/spec/support/shared_examples/models/concerns/bulk_insert_safe_shared_examples.rb b/spec/support/shared_examples/models/concerns/bulk_insert_safe_shared_examples.rb index 3db5d7a8d7d..ec9756007f1 100644 --- a/spec/support/shared_examples/models/concerns/bulk_insert_safe_shared_examples.rb +++ b/spec/support/shared_examples/models/concerns/bulk_insert_safe_shared_examples.rb @@ -30,10 +30,6 @@ RSpec.shared_examples 'a BulkInsertSafe model' do |klass| expect { target_class.set_callback(name) {} }.not_to raise_error end end - - it 'does not raise an error when the call is triggered by belongs_to' do - expect { target_class.belongs_to(:other_record) }.not_to raise_error - end end describe '.bulk_insert!' do diff --git a/spec/support/shared_examples/models/concerns/cron_schedulable_shared_examples.rb b/spec/support/shared_examples/models/concerns/cron_schedulable_shared_examples.rb new file mode 100644 index 00000000000..47a02a8fa49 --- /dev/null +++ b/spec/support/shared_examples/models/concerns/cron_schedulable_shared_examples.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'handles set_next_run_at' do + context 'when schedule runs every minute' do + it "updates next_run_at to the worker's execution time" do + travel_to(1.day.ago) do + expect(schedule.next_run_at).to eq(cron_worker_next_run_at) + end + end + end + + context 'when there are two different schedules in the same time zones' do + it 'sets the sames next_run_at' do + expect(schedule_1.next_run_at).to eq(schedule_2.next_run_at) + end + end + + context 'when cron is updated for existing schedules' do + it 'updates next_run_at automatically' do + expect { schedule.update!(cron: new_cron) }.to change { schedule.next_run_at } + end + end +end diff --git a/spec/support/shared_examples/models/concerns/repository_storage_movable_shared_examples.rb b/spec/support/shared_examples/models/concerns/repository_storage_movable_shared_examples.rb index 819cf6018fe..3f1588c46b3 100644 --- a/spec/support/shared_examples/models/concerns/repository_storage_movable_shared_examples.rb +++ b/spec/support/shared_examples/models/concerns/repository_storage_movable_shared_examples.rb @@ -36,7 +36,7 @@ RSpec.shared_examples 'handles repository moves' do container.set_repository_read_only! expect(subject).not_to be_valid - expect(subject.errors[error_key].first).to match(/is read only/) + expect(subject.errors[error_key].first).to match(/is read-only/) end end end 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 17948d648cb..d23f95b2e9e 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 @@ -58,6 +58,19 @@ RSpec.shared_examples 'value stream analytics stage' do it { expect(stage).not_to be_valid } end + + # rubocop: disable Rails/SaveBang + describe '.by_value_stream' do + it 'finds stages by value stream' do + stage1 = create(factory) + create(factory) # other stage with different value stream + + result = described_class.by_value_stream(stage1.value_stream) + + expect(result).to eq([stage1]) + end + end + # rubocop: enable Rails/SaveBang end describe '#subject_class' do diff --git a/spec/support/shared_examples/models/packages/debian/architecture_shared_examples.rb b/spec/support/shared_examples/models/packages/debian/architecture_shared_examples.rb index fbb94b4f5c1..33a04059491 100644 --- a/spec/support/shared_examples/models/packages/debian/architecture_shared_examples.rb +++ b/spec/support/shared_examples/models/packages/debian/architecture_shared_examples.rb @@ -3,8 +3,8 @@ require 'spec_helper' RSpec.shared_examples 'Debian Distribution Architecture' do |factory, container, can_freeze| - let_it_be_with_refind(:architecture) { create(factory) } # rubocop:disable Rails/SaveBang - let_it_be(:architecture_same_distribution, freeze: can_freeze) { create(factory, distribution: architecture.distribution) } + let_it_be_with_refind(:architecture) { create(factory, name: 'name1') } + let_it_be(:architecture_same_distribution, freeze: can_freeze) { create(factory, distribution: architecture.distribution, name: 'name2') } let_it_be(:architecture_same_name, freeze: can_freeze) { create(factory, name: architecture.name) } subject { architecture } @@ -30,20 +30,22 @@ RSpec.shared_examples 'Debian Distribution Architecture' do |factory, container, end describe 'scopes' do + describe '.ordered_by_name' do + subject { described_class.with_distribution(architecture.distribution).ordered_by_name } + + it { expect(subject).to match_array([architecture, architecture_same_distribution]) } + end + describe '.with_distribution' do subject { described_class.with_distribution(architecture.distribution) } - it 'does not return other distributions' do - expect(subject.to_a).to match_array([architecture, architecture_same_distribution]) - end + it { expect(subject).to match_array([architecture, architecture_same_distribution]) } end describe '.with_name' do subject { described_class.with_name(architecture.name) } - it 'does not return other distributions' do - expect(subject.to_a).to match_array([architecture, architecture_same_name]) - end + it { expect(subject).to match_array([architecture, architecture_same_name]) } end end end diff --git a/spec/support/shared_examples/models/packages/debian/component_file_shared_example.rb b/spec/support/shared_examples/models/packages/debian/component_file_shared_example.rb index 02ced49ee94..e6b16d5881d 100644 --- a/spec/support/shared_examples/models/packages/debian/component_file_shared_example.rb +++ b/spec/support/shared_examples/models/packages/debian/component_file_shared_example.rb @@ -114,11 +114,7 @@ RSpec.shared_examples 'Debian Component File' do |container_type, can_freeze| subject { described_class.with_container(container2) } it do - queries = ActiveRecord::QueryRecorder.new do - expect(subject.to_a).to contain_exactly(component_file_other_container) - end - - expect(queries.count).to eq(1) + expect(subject.to_a).to contain_exactly(component_file_other_container) end end @@ -126,11 +122,7 @@ RSpec.shared_examples 'Debian Component File' do |container_type, can_freeze| subject { described_class.with_codename_or_suite(distribution2.codename) } it do - queries = ActiveRecord::QueryRecorder.new do - expect(subject.to_a).to contain_exactly(component_file_other_container) - end - - expect(queries.count).to eq(1) + expect(subject.to_a).to contain_exactly(component_file_other_container) end end @@ -138,11 +130,7 @@ RSpec.shared_examples 'Debian Component File' do |container_type, can_freeze| subject { described_class.with_component_name(component1_2.name) } it do - queries = ActiveRecord::QueryRecorder.new do - expect(subject.to_a).to contain_exactly(component_file_other_component) - end - - expect(queries.count).to eq(1) + expect(subject.to_a).to contain_exactly(component_file_other_component) end end @@ -150,14 +138,7 @@ RSpec.shared_examples 'Debian Component File' do |container_type, can_freeze| subject { described_class.with_file_type(:source) } it do - # let_it_be_with_refind triggers a query - component_file_with_file_type_source - - queries = ActiveRecord::QueryRecorder.new do - expect(subject.to_a).to contain_exactly(component_file_with_file_type_source) - end - - expect(queries.count).to eq(1) + expect(subject.to_a).to contain_exactly(component_file_with_file_type_source) end end @@ -165,11 +146,7 @@ RSpec.shared_examples 'Debian Component File' do |container_type, can_freeze| subject { described_class.with_architecture_name(architecture1_2.name) } it do - queries = ActiveRecord::QueryRecorder.new do - expect(subject.to_a).to contain_exactly(component_file_other_architecture) - end - - expect(queries.count).to eq(1) + expect(subject.to_a).to contain_exactly(component_file_other_architecture) end end @@ -177,11 +154,7 @@ RSpec.shared_examples 'Debian Component File' do |container_type, can_freeze| subject { described_class.with_compression_type(:xz) } it do - queries = ActiveRecord::QueryRecorder.new do - expect(subject.to_a).to contain_exactly(component_file_other_compression_type) - end - - expect(queries.count).to eq(1) + expect(subject.to_a).to contain_exactly(component_file_other_compression_type) end end @@ -189,11 +162,19 @@ RSpec.shared_examples 'Debian Component File' do |container_type, can_freeze| subject { described_class.with_file_sha256('other_sha256') } it do - queries = ActiveRecord::QueryRecorder.new do - expect(subject.to_a).to contain_exactly(component_file_other_file_sha256) - end + expect(subject.to_a).to contain_exactly(component_file_other_file_sha256) + end + end + + describe '.created_before' do + let_it_be(:component_file1) { create("debian_#{container_type}_component_file", component: component1_1, architecture: architecture1_1, created_at: 4.hours.ago) } + let_it_be(:component_file2) { create("debian_#{container_type}_component_file", component: component1_1, architecture: architecture1_1, created_at: 3.hours.ago) } + let_it_be(:component_file3) { create("debian_#{container_type}_component_file", component: component1_1, architecture: architecture1_1, created_at: 1.hour.ago) } - expect(queries.count).to eq(1) + subject { described_class.created_before(2.hours.ago) } + + it do + expect(subject.to_a).to contain_exactly(component_file1, component_file2) end end end diff --git a/spec/support/shared_examples/models/packages/debian/component_shared_examples.rb b/spec/support/shared_examples/models/packages/debian/component_shared_examples.rb index 23e76d32fb0..635d45f40e5 100644 --- a/spec/support/shared_examples/models/packages/debian/component_shared_examples.rb +++ b/spec/support/shared_examples/models/packages/debian/component_shared_examples.rb @@ -3,8 +3,8 @@ require 'spec_helper' RSpec.shared_examples 'Debian Distribution Component' do |factory, container, can_freeze| - let_it_be_with_refind(:component) { create(factory) } # rubocop:disable Rails/SaveBang - let_it_be(:component_same_distribution, freeze: can_freeze) { create(factory, distribution: component.distribution) } + let_it_be_with_refind(:component) { create(factory, name: 'name1') } + let_it_be(:component_same_distribution, freeze: can_freeze) { create(factory, distribution: component.distribution, name: 'name2') } let_it_be(:component_same_name, freeze: can_freeze) { create(factory, name: component.name) } subject { component } @@ -32,6 +32,14 @@ RSpec.shared_examples 'Debian Distribution Component' do |factory, container, ca end describe 'scopes' do + describe '.ordered_by_name' do + subject { described_class.with_distribution(component.distribution).ordered_by_name } + + it 'sorts by name' do + expect(subject.to_a).to eq([component, component_same_distribution]) + end + end + describe '.with_distribution' do subject { described_class.with_distribution(component.distribution) } diff --git a/spec/support/shared_examples/models/packages/debian/distribution_shared_examples.rb b/spec/support/shared_examples/models/packages/debian/distribution_shared_examples.rb index 9eacacf725f..8693d6868e9 100644 --- a/spec/support/shared_examples/models/packages/debian/distribution_shared_examples.rb +++ b/spec/support/shared_examples/models/packages/debian/distribution_shared_examples.rb @@ -19,11 +19,6 @@ RSpec.shared_examples 'Debian Distribution' do |factory, container, can_freeze| it { is_expected.to have_many(:components).class_name("Packages::Debian::#{container.capitalize}Component").inverse_of(:distribution) } it { is_expected.to have_many(:architectures).class_name("Packages::Debian::#{container.capitalize}Architecture").inverse_of(:distribution) } - - if container != :group - it { is_expected.to have_many(:publications).class_name('Packages::Debian::Publication').inverse_of(:distribution).with_foreign_key(:distribution_id) } - it { is_expected.to have_many(:packages).class_name('Packages::Package').through(:publications) } - end end describe 'validations' do @@ -228,4 +223,44 @@ RSpec.shared_examples 'Debian Distribution' do |factory, container, can_freeze| end end end + + if container == :project + describe 'project distribution specifics' do + describe 'relationships' do + it { is_expected.to have_many(:publications).class_name('Packages::Debian::Publication').inverse_of(:distribution).with_foreign_key(:distribution_id) } + it { is_expected.to have_many(:packages).class_name('Packages::Package').through(:publications) } + it { is_expected.to have_many(:package_files).class_name('Packages::PackageFile').through(:packages) } + end + end + else + describe 'group distribution specifics' do + let_it_be(:public_project) { create(:project, :public, group: distribution_with_suite.container)} + let_it_be(:public_distribution_with_same_codename) { create(:debian_project_distribution, container: public_project, codename: distribution_with_suite.codename) } + let_it_be(:public_package_with_same_codename) { create(:debian_package, project: public_project, published_in: public_distribution_with_same_codename)} + let_it_be(:public_distribution_with_same_suite) { create(:debian_project_distribution, container: public_project, suite: distribution_with_suite.suite) } + let_it_be(:public_package_with_same_suite) { create(:debian_package, project: public_project, published_in: public_distribution_with_same_suite)} + + let_it_be(:private_project) { create(:project, :private, group: distribution_with_suite.container)} + let_it_be(:private_distribution_with_same_codename) { create(:debian_project_distribution, container: private_project, codename: distribution_with_suite.codename) } + let_it_be(:private_package_with_same_codename) { create(:debian_package, project: private_project, published_in: private_distribution_with_same_codename)} + let_it_be(:private_distribution_with_same_suite) { create(:debian_project_distribution, container: private_project, suite: distribution_with_suite.suite) } + let_it_be(:private_package_with_same_suite) { create(:debian_package, project: private_project, published_in: private_distribution_with_same_codename)} + + describe '#packages' do + subject { distribution_with_suite.packages } + + it 'returns only public packages with same codename' do + expect(subject.to_a).to contain_exactly(public_package_with_same_codename) + end + end + + describe '#package_files' do + subject { distribution_with_suite.package_files } + + it 'returns only files from public packages with same codename' do + expect(subject.to_a).to contain_exactly(*public_package_with_same_codename.package_files) + end + end + end + end end diff --git a/spec/support/shared_examples/models/slack_mattermost_notifications_shared_examples.rb b/spec/support/shared_examples/models/slack_mattermost_notifications_shared_examples.rb index 71a76121d38..09b7d1be704 100644 --- a/spec/support/shared_examples/models/slack_mattermost_notifications_shared_examples.rb +++ b/spec/support/shared_examples/models/slack_mattermost_notifications_shared_examples.rb @@ -201,7 +201,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do |service_name| context 'deployment events' do let_it_be(:deployment) { create(:deployment) } - let(:data) { Gitlab::DataBuilder::Deployment.build(deployment) } + let(:data) { Gitlab::DataBuilder::Deployment.build(deployment, Time.current) } it_behaves_like 'calls the service API with the event message', /Deploy to (.*?) created/ end diff --git a/spec/support/shared_examples/models/wiki_shared_examples.rb b/spec/support/shared_examples/models/wiki_shared_examples.rb index 6b243aef3e6..2498bf35a09 100644 --- a/spec/support/shared_examples/models/wiki_shared_examples.rb +++ b/spec/support/shared_examples/models/wiki_shared_examples.rb @@ -469,34 +469,30 @@ RSpec.shared_examples 'wiki model' do end describe '#delete_page' do - shared_examples 'delete_page operations' do - let(:page) { create(:wiki_page, wiki: wiki) } + let(:page) { create(:wiki_page, wiki: wiki) } - it 'deletes the page' do - subject.delete_page(page) + it 'deletes the page' do + subject.delete_page(page) - expect(subject.list_pages.count).to eq(0) - end + expect(subject.list_pages.count).to eq(0) + end - it 'sets the correct commit email' do - subject.delete_page(page) + it 'sets the correct commit email' do + subject.delete_page(page) - expect(user.commit_email).not_to eq(user.email) - expect(commit.author_email).to eq(user.commit_email) - expect(commit.committer_email).to eq(user.commit_email) - end + expect(user.commit_email).not_to eq(user.email) + expect(commit.author_email).to eq(user.commit_email) + expect(commit.committer_email).to eq(user.commit_email) + end - it 'runs after_wiki_activity callbacks' do - page + it 'runs after_wiki_activity callbacks' do + page - expect(subject).to receive(:after_wiki_activity) + expect(subject).to receive(:after_wiki_activity) - subject.delete_page(page) - end + subject.delete_page(page) end - it_behaves_like 'delete_page operations' - context 'when an error is raised' do it 'logs the error and returns false' do page = build(:wiki_page, wiki: wiki) @@ -509,14 +505,6 @@ RSpec.shared_examples 'wiki model' do expect(subject.delete_page(page)).to be_falsey end end - - context 'when feature flag :gitaly_replace_wiki_delete_page is disabled' do - before do - stub_feature_flags(gitaly_replace_wiki_delete_page: false) - end - - it_behaves_like 'delete_page operations' - end end describe '#ensure_repository' do diff --git a/spec/support/shared_examples/namespaces/namespace_traversal_examples.rb b/spec/support/shared_examples/namespaces/traversal_examples.rb index 36e5808fa28..77a1705627e 100644 --- a/spec/support/shared_examples/namespaces/namespace_traversal_examples.rb +++ b/spec/support/shared_examples/namespaces/traversal_examples.rb @@ -39,16 +39,17 @@ RSpec.shared_examples 'namespace traversal' do end describe '#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) } + 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 - expect(very_deep_nested_group.ancestors).to include(group, nested_group, deep_nested_group) - expect(deep_nested_group.ancestors).to include(group, nested_group) - expect(nested_group.ancestors).to include(group) - expect(group.ancestors).to eq([]) + # #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) + expect(deep_nested_group.reload.ancestors).to contain_exactly(group, nested_group) + expect(nested_group.reload.ancestors).to contain_exactly(group) + expect(group.reload.ancestors).to eq([]) end describe '#recursive_ancestors' do diff --git a/spec/support/shared_examples/policies/project_policy_shared_examples.rb b/spec/support/shared_examples/policies/project_policy_shared_examples.rb index d05e5eb9120..013c9b61b99 100644 --- a/spec/support/shared_examples/policies/project_policy_shared_examples.rb +++ b/spec/support/shared_examples/policies/project_policy_shared_examples.rb @@ -57,7 +57,7 @@ RSpec.shared_examples 'project policies as anonymous' do context 'when a project has pending invites' do let(:group) { create(:group, :public) } let(:project) { create(:project, :public, namespace: group) } - let(:user_permissions) { [:create_merge_request_in, :create_project, :create_issue, :create_note, :upload_file, :award_emoji] } + let(:user_permissions) { [:create_merge_request_in, :create_project, :create_issue, :create_note, :upload_file, :award_emoji, :create_incident] } let(:anonymous_permissions) { guest_permissions - user_permissions } let(:current_user) { anonymous } diff --git a/spec/support/shared_examples/requests/api/conan_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/conan_packages_shared_examples.rb index 87aaac673c1..c938c6432fe 100644 --- a/spec/support/shared_examples/requests/api/conan_packages_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/conan_packages_shared_examples.rb @@ -106,7 +106,7 @@ RSpec.shared_examples 'conan authenticate endpoint' do expect(payload['user_id']).to eq(personal_access_token.user_id) duration = payload['exp'] - payload['iat'] - expect(duration).to eq(1.hour) + expect(duration).to eq(::Gitlab::ConanToken::CONAN_TOKEN_EXPIRE_TIME) end end end @@ -661,7 +661,7 @@ RSpec.shared_examples 'workhorse package file upload endpoint' do end RSpec.shared_examples 'creates build_info when there is a job' do - context 'with job token' do + context 'with job token', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/294047' do let(:jwt) { build_jwt_from_job(job) } it 'creates a build_info record' do 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 acaa0d8c2bc..dfd19167dcd 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 @@ -1,20 +1,16 @@ # frozen_string_literal: true -RSpec.shared_context 'Debian repository shared context' do |object_type| +RSpec.shared_context 'Debian repository shared context' do |container_type, can_freeze| include_context 'workhorse headers' before do stub_feature_flags(debian_packages: true) end - if object_type == :project - let(:project) { create(:project, :public) } - elsif object_type == :group - let(:group) { create(:group, :public) } - end - - let(:user) { create(:user) } - let(:personal_access_token) { create(:personal_access_token, user: user) } + let_it_be(:private_container, freeze: can_freeze) { create(container_type, :private) } + let_it_be(:public_container, freeze: can_freeze) { create(container_type, :public) } + let_it_be(:user, freeze: true) { create(:user) } + let_it_be(:personal_access_token, freeze: true) { create(:personal_access_token, user: user) } let(:distribution) { 'bullseye' } let(:component) { 'main' } @@ -36,7 +32,7 @@ RSpec.shared_context 'Debian repository shared context' do |object_type| end end - let(:params) { workhorse_params } + let(:api_params) { workhorse_params } let(:auth_headers) { {} } let(:wh_headers) do @@ -57,12 +53,12 @@ RSpec.shared_context 'Debian repository shared context' do |object_type| api(url), method: method, file_key: :file, - params: params, + params: api_params, headers: headers, send_rewritten_field: send_rewritten_field ) else - send method, api(url), headers: headers, params: params + send method, api(url), headers: headers, params: api_params end end end @@ -81,289 +77,190 @@ RSpec.shared_context 'Debian repository auth headers' do |user_role, user_token, end end -RSpec.shared_context 'Debian repository project access' do |project_visibility_level, user_role, user_token, auth_method| +RSpec.shared_context 'Debian repository access' do |visibility_level, user_role, add_member, user_token, auth_method| include_context 'Debian repository auth headers', user_role, user_token, auth_method do + let(:containers) { { private: private_container, public: public_container } } + let(:container) { containers[visibility_level] } + before do - project.update_column(:visibility_level, Gitlab::VisibilityLevel.const_get(project_visibility_level, false)) + container.send("add_#{user_role}", user) if add_member && user_role != :anonymous end end end -RSpec.shared_examples 'Debian project repository GET request' do |user_role, add_member, status, body| - context "for user type #{user_role}" do - before do - project.send("add_#{user_role}", user) if add_member && user_role != :anonymous - end +RSpec.shared_examples 'Debian repository GET request' do |status, body = nil| + and_body = body.nil? ? '' : ' and expected body' - and_body = body.nil? ? '' : ' and expected body' + it "returns #{status}#{and_body}" do + subject - it "returns #{status}#{and_body}" do - subject + expect(response).to have_gitlab_http_status(status) - expect(response).to have_gitlab_http_status(status) - - unless body.nil? - expect(response.body).to eq(body) - end + unless body.nil? + expect(response.body).to eq(body) end end end -RSpec.shared_examples 'Debian project repository PUT request' do |user_role, add_member, status, body| - context "for user type #{user_role}" do - before do - project.send("add_#{user_role}", user) if add_member && user_role != :anonymous - end +RSpec.shared_examples 'Debian repository upload request' do |status, body = nil| + and_body = body.nil? ? '' : ' and expected body' - and_body = body.nil? ? '' : ' and expected body' + if status == :created + it 'creates package files', :aggregate_failures do + pending "Debian package creation not implemented" - if status == :created - it 'creates package files', :aggregate_failures do - pending "Debian package creation not implemented" - expect { subject } - .to change { project.packages.debian.count }.by(1) + expect { subject } + .to change { container.packages.debian.count }.by(1) - expect(response).to have_gitlab_http_status(status) - expect(response.media_type).to eq('text/plain') + expect(response).to have_gitlab_http_status(status) + expect(response.media_type).to eq('text/plain') - unless body.nil? - expect(response.body).to eq(body) - end + unless body.nil? + expect(response.body).to eq(body) end - it_behaves_like 'a package tracking event', described_class.name, 'push_package' - else - it "returns #{status}#{and_body}", :aggregate_failures do - subject + end + it_behaves_like 'a package tracking event', described_class.name, 'push_package' + else + it "returns #{status}#{and_body}", :aggregate_failures do + subject - expect(response).to have_gitlab_http_status(status) + expect(response).to have_gitlab_http_status(status) - unless body.nil? - expect(response.body).to eq(body) - end + unless body.nil? + expect(response.body).to eq(body) end end end end -RSpec.shared_examples 'Debian project repository PUT authorize request' do |user_role, add_member, status, body, is_authorize| - context "for user type #{user_role}" do - before do - project.send("add_#{user_role}", user) if add_member && user_role != :anonymous - end - - and_body = body.nil? ? '' : ' and expected body' +RSpec.shared_examples 'Debian repository upload authorize request' do |status, body = nil| + and_body = body.nil? ? '' : ' and expected body' - if status == :created - it 'authorizes package file upload', :aggregate_failures do - subject + if status == :created + it 'authorizes package file upload', :aggregate_failures do + subject - expect(response).to have_gitlab_http_status(:ok) - expect(response.media_type).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE) - expect(json_response['TempPath']).to eq(Packages::PackageFileUploader.workhorse_local_upload_path) - expect(json_response['RemoteObject']).to be_nil - expect(json_response['MaximumSize']).to be_nil - end + expect(response).to have_gitlab_http_status(:ok) + expect(response.media_type).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE) + expect(json_response['TempPath']).to eq(Packages::PackageFileUploader.workhorse_local_upload_path) + expect(json_response['RemoteObject']).to be_nil + expect(json_response['MaximumSize']).to be_nil + end - context 'without a valid token' do - let(:workhorse_token) { 'invalid' } + context 'without a valid token' do + let(:workhorse_token) { 'invalid' } - it 'rejects request' do - subject + it 'rejects request' do + subject - expect(response).to have_gitlab_http_status(:forbidden) - end + expect(response).to have_gitlab_http_status(:forbidden) end + end - context 'bypassing gitlab-workhorse' do - let(:workhorse_headers) { {} } + context 'bypassing gitlab-workhorse' do + let(:workhorse_headers) { {} } - it 'rejects request' do - subject + it 'rejects request' do + subject - expect(response).to have_gitlab_http_status(:forbidden) - end + expect(response).to have_gitlab_http_status(:forbidden) end - else - it "returns #{status}#{and_body}", :aggregate_failures do - subject + end + else + it "returns #{status}#{and_body}", :aggregate_failures do + subject - expect(response).to have_gitlab_http_status(status) + expect(response).to have_gitlab_http_status(status) - unless body.nil? - expect(response.body).to eq(body) - end + unless body.nil? + expect(response.body).to eq(body) end end end end -RSpec.shared_examples 'rejects Debian access with unknown project id' do - context 'with an unknown project' do - let(:project) { double(id: non_existing_record_id) } +RSpec.shared_examples 'rejects Debian access with unknown container id' do + context 'with an unknown container' do + let(:container) { double(id: non_existing_record_id) } context 'as anonymous' do - it_behaves_like 'Debian project repository GET request', :anonymous, true, :unauthorized, nil + it_behaves_like 'Debian repository GET request', :unauthorized, nil end context 'as authenticated user' do subject { get api(url), headers: basic_auth_header(user.username, personal_access_token.token) } - it_behaves_like 'Debian project repository GET request', :anonymous, true, :not_found, nil + it_behaves_like 'Debian repository GET request', :not_found, nil end end end -RSpec.shared_examples 'Debian project repository GET endpoint' do |success_status, success_body| - context 'with valid project' do +RSpec.shared_examples 'Debian repository read endpoint' do |desired_behavior, success_status, success_body| + context 'with valid container' do using RSpec::Parameterized::TableSyntax - where(:project_visibility_level, :user_role, :member, :user_token, :expected_status, :expected_body) do - 'PUBLIC' | :developer | true | true | success_status | success_body - 'PUBLIC' | :guest | true | true | success_status | success_body - 'PUBLIC' | :developer | true | false | success_status | success_body - 'PUBLIC' | :guest | true | false | success_status | success_body - 'PUBLIC' | :developer | false | true | success_status | success_body - 'PUBLIC' | :guest | false | true | success_status | success_body - 'PUBLIC' | :developer | false | false | success_status | success_body - 'PUBLIC' | :guest | false | false | success_status | success_body - 'PUBLIC' | :anonymous | false | true | success_status | success_body - 'PRIVATE' | :developer | true | true | success_status | success_body - 'PRIVATE' | :guest | true | true | :forbidden | nil - 'PRIVATE' | :developer | true | false | :unauthorized | nil - 'PRIVATE' | :guest | true | false | :unauthorized | nil - 'PRIVATE' | :developer | false | true | :not_found | nil - 'PRIVATE' | :guest | false | true | :not_found | nil - 'PRIVATE' | :developer | false | false | :unauthorized | nil - 'PRIVATE' | :guest | false | false | :unauthorized | nil - 'PRIVATE' | :anonymous | false | true | :unauthorized | nil + where(:visibility_level, :user_role, :member, :user_token, :expected_status, :expected_body) do + :public | :developer | true | true | success_status | success_body + :public | :guest | true | true | success_status | success_body + :public | :developer | true | false | success_status | success_body + :public | :guest | true | false | success_status | success_body + :public | :developer | false | true | success_status | success_body + :public | :guest | false | true | success_status | success_body + :public | :developer | false | false | success_status | success_body + :public | :guest | false | false | success_status | success_body + :public | :anonymous | false | true | success_status | success_body + :private | :developer | true | true | success_status | success_body + :private | :guest | true | true | :forbidden | nil + :private | :developer | true | false | :unauthorized | nil + :private | :guest | true | false | :unauthorized | nil + :private | :developer | false | true | :not_found | nil + :private | :guest | false | true | :not_found | nil + :private | :developer | false | false | :unauthorized | nil + :private | :guest | false | false | :unauthorized | nil + :private | :anonymous | false | true | :unauthorized | nil end with_them do - include_context 'Debian repository project access', params[:project_visibility_level], params[:user_role], params[:user_token], :basic do - it_behaves_like 'Debian project repository GET request', params[:user_role], params[:member], params[:expected_status], params[:expected_body] + include_context 'Debian repository access', params[:visibility_level], params[:user_role], params[:member], params[:user_token], :basic do + it_behaves_like "Debian repository #{desired_behavior}", params[:expected_status], params[:expected_body] end end end - it_behaves_like 'rejects Debian access with unknown project id' -end - -RSpec.shared_examples 'Debian project repository PUT endpoint' do |success_status, success_body, is_authorize = false| - context 'with valid project' do - using RSpec::Parameterized::TableSyntax - - where(:project_visibility_level, :user_role, :member, :user_token, :expected_status, :expected_body) do - 'PUBLIC' | :developer | true | true | success_status | nil - 'PUBLIC' | :guest | true | true | :forbidden | nil - 'PUBLIC' | :developer | true | false | :unauthorized | nil - 'PUBLIC' | :guest | true | false | :unauthorized | nil - 'PUBLIC' | :developer | false | true | :forbidden | nil - 'PUBLIC' | :guest | false | true | :forbidden | nil - 'PUBLIC' | :developer | false | false | :unauthorized | nil - 'PUBLIC' | :guest | false | false | :unauthorized | nil - 'PUBLIC' | :anonymous | false | true | :unauthorized | nil - 'PRIVATE' | :developer | true | true | success_status | nil - 'PRIVATE' | :guest | true | true | :forbidden | nil - 'PRIVATE' | :developer | true | false | :unauthorized | nil - 'PRIVATE' | :guest | true | false | :unauthorized | nil - 'PRIVATE' | :developer | false | true | :not_found | nil - 'PRIVATE' | :guest | false | true | :not_found | nil - 'PRIVATE' | :developer | false | false | :unauthorized | nil - 'PRIVATE' | :guest | false | false | :unauthorized | nil - 'PRIVATE' | :anonymous | false | true | :unauthorized | nil - end - - with_them do - include_context 'Debian repository project access', params[:project_visibility_level], params[:user_role], params[:user_token], :basic do - desired_behavior = if is_authorize - 'Debian project repository PUT authorize request' - else - 'Debian project repository PUT request' - end - - it_behaves_like desired_behavior, params[:user_role], params[:member], params[:expected_status], params[:expected_body] - end - end - end - - it_behaves_like 'rejects Debian access with unknown project id' -end - -RSpec.shared_context 'Debian repository group access' do |group_visibility_level, user_role, user_token, auth_method| - include_context 'Debian repository auth headers', user_role, user_token, auth_method do - before do - group.update_column(:visibility_level, Gitlab::VisibilityLevel.const_get(group_visibility_level, false)) - end - end -end - -RSpec.shared_examples 'Debian group repository GET request' do |user_role, add_member, status, body| - context "for user type #{user_role}" do - before do - group.send("add_#{user_role}", user) if add_member && user_role != :anonymous - end - - and_body = body.nil? ? '' : ' and expected body' - - it "returns #{status}#{and_body}" do - subject - - expect(response).to have_gitlab_http_status(status) - - unless body.nil? - expect(response.body).to eq(body) - end - end - end -end - -RSpec.shared_examples 'rejects Debian access with unknown group id' do - context 'with an unknown group' do - let(:group) { double(id: non_existing_record_id) } - - context 'as anonymous' do - it_behaves_like 'Debian group repository GET request', :anonymous, true, :unauthorized, nil - end - - context 'as authenticated user' do - subject { get api(url), headers: basic_auth_header(user.username, personal_access_token.token) } - - it_behaves_like 'Debian group repository GET request', :anonymous, true, :not_found, nil - end - end + it_behaves_like 'rejects Debian access with unknown container id' end -RSpec.shared_examples 'Debian group repository GET endpoint' do |success_status, success_body| - context 'with valid group' do +RSpec.shared_examples 'Debian repository write endpoint' do |desired_behavior, success_status, success_body| + context 'with valid container' do using RSpec::Parameterized::TableSyntax - where(:group_visibility_level, :user_role, :member, :user_token, :expected_status, :expected_body) do - 'PUBLIC' | :developer | true | true | success_status | success_body - 'PUBLIC' | :guest | true | true | success_status | success_body - 'PUBLIC' | :developer | true | false | success_status | success_body - 'PUBLIC' | :guest | true | false | success_status | success_body - 'PUBLIC' | :developer | false | true | success_status | success_body - 'PUBLIC' | :guest | false | true | success_status | success_body - 'PUBLIC' | :developer | false | false | success_status | success_body - 'PUBLIC' | :guest | false | false | success_status | success_body - 'PUBLIC' | :anonymous | false | true | success_status | success_body - 'PRIVATE' | :developer | true | true | success_status | success_body - 'PRIVATE' | :guest | true | true | :forbidden | nil - 'PRIVATE' | :developer | true | false | :unauthorized | nil - 'PRIVATE' | :guest | true | false | :unauthorized | nil - 'PRIVATE' | :developer | false | true | :not_found | nil - 'PRIVATE' | :guest | false | true | :not_found | nil - 'PRIVATE' | :developer | false | false | :unauthorized | nil - 'PRIVATE' | :guest | false | false | :unauthorized | nil - 'PRIVATE' | :anonymous | false | true | :unauthorized | nil + where(:visibility_level, :user_role, :member, :user_token, :expected_status, :expected_body) do + :public | :developer | true | true | success_status | success_body + :public | :guest | true | true | :forbidden | nil + :public | :developer | true | false | :unauthorized | nil + :public | :guest | true | false | :unauthorized | nil + :public | :developer | false | true | :forbidden | nil + :public | :guest | false | true | :forbidden | nil + :public | :developer | false | false | :unauthorized | nil + :public | :guest | false | false | :unauthorized | nil + :public | :anonymous | false | true | :unauthorized | nil + :private | :developer | true | true | success_status | success_body + :private | :guest | true | true | :forbidden | nil + :private | :developer | true | false | :unauthorized | nil + :private | :guest | true | false | :unauthorized | nil + :private | :developer | false | true | :not_found | nil + :private | :guest | false | true | :not_found | nil + :private | :developer | false | false | :unauthorized | nil + :private | :guest | false | false | :unauthorized | nil + :private | :anonymous | false | true | :unauthorized | nil end with_them do - include_context 'Debian repository group access', params[:group_visibility_level], params[:user_role], params[:user_token], :basic do - it_behaves_like 'Debian group repository GET request', params[:user_role], params[:member], params[:expected_status], params[:expected_body] + include_context 'Debian repository access', params[:visibility_level], params[:user_role], params[:member], params[:user_token], :basic do + it_behaves_like "Debian repository #{desired_behavior}", params[:expected_status], params[:expected_body] end end end - it_behaves_like 'rejects Debian access with unknown group id' + it_behaves_like 'rejects Debian access with unknown container id' end diff --git a/spec/support/shared_examples/requests/api/graphql/mutations/boards/update_list_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/mutations/boards/update_list_shared_examples.rb new file mode 100644 index 00000000000..9b55b0f061f --- /dev/null +++ b/spec/support/shared_examples/requests/api/graphql/mutations/boards/update_list_shared_examples.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'a GraphQL request to update board list' do + context 'the user is not allowed to read board lists' do + it_behaves_like 'a mutation that returns a top-level access error' + end + + before do + list.update_preferences_for(current_user, collapsed: false) + end + + context 'when user has permissions to admin board lists' do + before do + group.add_reporter(current_user) + end + + it 'updates the list position and collapsed state' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(response).to have_gitlab_http_status(:success) + expect(mutation_response['list']).to include( + 'position' => 1, + 'collapsed' => true + ) + end + end + + context 'when user has permissions to read board lists' do + before do + group.add_guest(current_user) + end + + it 'updates the list collapsed state but not the list position' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(response).to have_gitlab_http_status(:success) + expect(mutation_response['list']).to include( + 'position' => 0, + 'collapsed' => true + ) + end + end +end diff --git a/spec/support/shared_examples/requests/api/graphql/mutations/destroy_list_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/mutations/destroy_list_shared_examples.rb new file mode 100644 index 00000000000..0cec67ff541 --- /dev/null +++ b/spec/support/shared_examples/requests/api/graphql/mutations/destroy_list_shared_examples.rb @@ -0,0 +1,87 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.shared_examples 'board lists destroy request' do + include GraphqlHelpers + + subject { post_graphql_mutation(mutation, current_user: current_user) } + + shared_examples 'does not destroy the list and returns an error' do + it 'does not destroy the list' do + expect { subject }.not_to change { klass.count } + end + + it 'returns an error and not nil list' do + subject + + expect(mutation_response['errors']).not_to be_empty + expect(mutation_response['list']).not_to be_nil + end + end + + context 'when the user does not have permission' do + it 'does not destroy the list' do + expect { subject }.not_to change { klass.count } + end + + it 'returns an error' do + subject + + expect(graphql_errors.first['message']).to include("The resource that you are attempting to access does not exist or you don't have permission to perform this action") + end + end + + context 'when the user has permission' do + before do + group.add_maintainer(current_user) + end + + context 'when given id is not for a list' do + # could be any non-list thing + let_it_be(:list) { group } + + it 'returns an error' do + subject + + expect(graphql_errors.first['message']).to include('does not represent an instance of') + end + end + + context 'when list does not exist' do + let(:variables) do + { + list_id: "gid://gitlab/#{klass}/#{non_existing_record_id}" + } + end + + it 'returns a top level error' do + subject + + expect(graphql_errors.first['message']).to include('No object found for') + end + end + + context 'when everything is ok' do + it 'destroys the list' do + expect { subject }.to change { klass.count }.by(-1) + end + + it 'returns an empty list' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(mutation_response).to have_key('list') + expect(mutation_response['list']).to be_nil + expect(mutation_response['errors']).to be_empty + end + end + + context 'when the list is not destroyable' do + before do + list.update!(list_type: :backlog) + end + + it_behaves_like 'does not destroy the list and returns an error' + end + end +end diff --git a/spec/support/shared_examples/requests/api/graphql/packages/group_and_project_packages_list_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/packages/group_and_project_packages_list_shared_examples.rb index 66fbfa798b0..af4c9286e7c 100644 --- a/spec/support/shared_examples/requests/api/graphql/packages/group_and_project_packages_list_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/graphql/packages/group_and_project_packages_list_shared_examples.rb @@ -3,6 +3,38 @@ RSpec.shared_examples 'group and project packages query' do include GraphqlHelpers + let_it_be(:versionaless_package) { create(:maven_package, project: project1, version: nil) } + let_it_be(:maven_package) { create(:maven_package, project: project1, name: 'tab', version: '4.0.0', created_at: 5.days.ago) } + let_it_be(:package) { create(:npm_package, project: project1, name: 'uab', version: '5.0.0', created_at: 4.days.ago) } + let_it_be(:composer_package) { create(:composer_package, project: project2, name: 'vab', version: '6.0.0', created_at: 3.days.ago) } + let_it_be(:debian_package) { create(:debian_package, project: project2, name: 'zab', version: '7.0.0', created_at: 2.days.ago) } + let_it_be(:composer_metadatum) do + create(:composer_metadatum, package: composer_package, + target_sha: 'afdeh', + composer_json: { name: 'x', type: 'y', license: 'z', version: 1 }) + end + + let(:package_names) { graphql_data_at(resource_type, :packages, :nodes, :name) } + let(:target_shas) { graphql_data_at(resource_type, :packages, :nodes, :metadata, :target_sha) } + let(:packages) { graphql_data_at(resource_type, :packages, :nodes) } + + let(:fields) do + <<~QUERY + nodes { + #{all_graphql_fields_for('packages'.classify, excluded: ['project'])} + metadata { #{query_graphql_fragment('ComposerMetadata')} } + } + QUERY + end + + let(:query) do + graphql_query_for( + resource_type, + { 'fullPath' => resource.full_path }, + query_graphql_field('packages', {}, fields) + ) + end + context 'when user has access to the resource' do before do resource.add_reporter(current_user) @@ -48,4 +80,101 @@ RSpec.shared_examples 'group and project packages query' do expect(packages).to be_nil end end + + describe 'sorting and pagination' do + let_it_be(:ascending_packages) { [maven_package, package, composer_package, debian_package].map { |package| global_id_of(package)} } + + let(:data_path) { [resource_type, :packages] } + + before do + resource.add_reporter(current_user) + end + + [:CREATED_ASC, :NAME_ASC, :VERSION_ASC, :TYPE_ASC].each do |order| + context "#{order}" do + it_behaves_like 'sorted paginated query' do + let(:sort_param) { order } + let(:first_param) { 4 } + let(:expected_results) { ascending_packages } + end + end + end + + [:CREATED_DESC, :NAME_DESC, :VERSION_DESC, :TYPE_DESC].each do |order| + context "#{order}" do + it_behaves_like 'sorted paginated query' do + let(:sort_param) { order } + let(:first_param) { 4 } + let(:expected_results) { ascending_packages.reverse } + end + end + end + + context 'with an invalid sort' do + let(:query) do + graphql_query_for( + resource_type, + { 'fullPath' => resource.full_path }, + query_nodes(:packages, :name, args: { sort: :WRONG_ORDER }) + ) + end + + before do + post_graphql(query, current_user: current_user) + end + + it 'throws an error' do + expect_graphql_errors_to_include(/Argument \'sort\' on Field \'packages\' has an invalid value/) + end + end + + def pagination_query(params) + graphql_query_for(resource_type, { 'fullPath' => resource.full_path }, + query_nodes(:packages, :id, include_pagination_info: true, args: params) + ) + end + end + + describe 'filtering' do + subject { packages } + + let(:query) do + graphql_query_for( + resource_type, + { 'fullPath' => resource.full_path }, + query_nodes(:packages, :name, args: params) + ) + end + + before do + resource.add_reporter(current_user) + post_graphql(query, current_user: current_user) + end + + context 'package_name' do + let(:params) { { package_name: maven_package.name } } + + it { is_expected.to contain_exactly({ "name" => maven_package.name }) } + end + + context 'package_type' do + let(:params) { { package_type: :COMPOSER } } + + it { is_expected.to contain_exactly({ "name" => composer_package.name }) } + end + + context 'status' do + let_it_be(:errored_package) { create(:maven_package, project: project1, status: 'error') } + + let(:params) { { status: :ERROR } } + + it { is_expected.to contain_exactly({ "name" => errored_package.name }) } + end + + context 'include_versionless' do + let(:params) { { include_versionless: true } } + + it { is_expected.to include({ "name" => versionaless_package.name }) } + end + end end diff --git a/spec/support/shared_examples/requests/api/issuable_update_shared_examples.rb b/spec/support/shared_examples/requests/api/issuable_update_shared_examples.rb index ded381fd402..a3378d4619b 100644 --- a/spec/support/shared_examples/requests/api/issuable_update_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/issuable_update_shared_examples.rb @@ -3,7 +3,7 @@ RSpec.shared_examples 'issuable update endpoint' do let(:area) { entity.class.name.underscore.pluralize } - describe 'PUT /projects/:id/issues/:issue_id' do + describe 'PUT /projects/:id/issues/:issue_iid' do let(:url) { "/projects/#{project.id}/#{area}/#{entity.iid}" } it 'clears labels when labels param is nil' do diff --git a/spec/support/shared_examples/requests/api/multiple_and_scoped_issue_boards_shared_examples.rb b/spec/support/shared_examples/requests/api/multiple_and_scoped_issue_boards_shared_examples.rb index 54aa9d47dd8..fa111ca5811 100644 --- a/spec/support/shared_examples/requests/api/multiple_and_scoped_issue_boards_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/multiple_and_scoped_issue_boards_shared_examples.rb @@ -14,7 +14,6 @@ RSpec.shared_examples 'multiple and scoped issue boards' do |route_definition| post api(root_url, user), params: { name: "new board" } expect(response).to have_gitlab_http_status(:created) - expect(response).to match_response_schema('public_api/v4/board', dir: "ee") end end diff --git a/spec/support/shared_examples/requests/api/terraform/modules/v1/packages_shared_examples.rb b/spec/support/shared_examples/requests/api/terraform/modules/v1/packages_shared_examples.rb new file mode 100644 index 00000000000..70cc9b1e6b5 --- /dev/null +++ b/spec/support/shared_examples/requests/api/terraform/modules/v1/packages_shared_examples.rb @@ -0,0 +1,251 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'when package feature is disabled' do + before do + stub_config(packages: { enabled: false }) + end + + it_behaves_like 'returning response status', :not_found +end + +RSpec.shared_examples 'without authentication' do + it_behaves_like 'returning response status', :unauthorized +end + +RSpec.shared_examples 'with authentication' do + where(:user_role, :token_header, :token_type, :valid_token, :status) do + :guest | 'PRIVATE-TOKEN' | :personal_access_token | true | :not_found + :guest | 'PRIVATE-TOKEN' | :personal_access_token | false | :unauthorized + :guest | 'DEPLOY-TOKEN' | :deploy_token | true | :not_found + :guest | 'DEPLOY-TOKEN' | :deploy_token | false | :unauthorized + :guest | 'JOB-TOKEN' | :job_token | true | :not_found + :guest | 'JOB-TOKEN' | :job_token | false | :unauthorized + :reporter | 'PRIVATE-TOKEN' | :personal_access_token | true | :not_found + :reporter | 'PRIVATE-TOKEN' | :personal_access_token | false | :unauthorized + :reporter | 'DEPLOY-TOKEN' | :deploy_token | true | :not_found + :reporter | 'DEPLOY-TOKEN' | :deploy_token | false | :unauthorized + :reporter | 'JOB-TOKEN' | :job_token | true | :not_found + :reporter | 'JOB-TOKEN' | :job_token | false | :unauthorized + :developer | 'PRIVATE-TOKEN' | :personal_access_token | true | :not_found + :developer | 'PRIVATE-TOKEN' | :personal_access_token | false | :unauthorized + :developer | 'DEPLOY-TOKEN' | :deploy_token | true | :not_found + :developer | 'DEPLOY-TOKEN' | :deploy_token | false | :unauthorized + :developer | 'JOB-TOKEN' | :job_token | true | :not_found + :developer | 'JOB-TOKEN' | :job_token | false | :unauthorized + end + + with_them do + before do + project.send("add_#{user_role}", user) unless user_role == :anonymous + end + + let(:token) { valid_token ? tokens[token_type] : 'invalid-token123' } + let(:headers) { { token_header => token } } + + it_behaves_like 'returning response status', params[:status] + end +end + +RSpec.shared_examples 'an unimplemented route' do + it_behaves_like 'without authentication' + it_behaves_like 'with authentication' + it_behaves_like 'when package feature is disabled' +end + +RSpec.shared_examples 'grants terraform module download' do |user_type, status, add_member = true| + context "for user type #{user_type}" do + before do + group.send("add_#{user_type}", user) if add_member && user_type != :anonymous + end + + it_behaves_like 'returning response status', status + + it 'returns a valid response' do + subject + + expect(response.headers).to include 'X-Terraform-Get' + end + end +end + +RSpec.shared_examples 'returns terraform module packages' do |user_type, status, add_member = true| + context "for user type #{user_type}" do + before do + group.send("add_#{user_type}", user) if add_member && user_type != :anonymous + end + + it_behaves_like 'returning response status', status + + it 'returning a valid response' do + subject + + expect(json_response).to match_schema('public_api/v4/packages/terraform/modules/v1/versions') + end + end +end + +RSpec.shared_examples 'returns no terraform module packages' do |user_type, status, add_member = true| + context "for user type #{user_type}" do + before do + group.send("add_#{user_type}", user) if add_member && user_type != :anonymous + end + + it_behaves_like 'returning response status', status + + it 'returns a response with no versions' do + subject + + expect(json_response['modules'][0]['versions'].size).to eq(0) + end + end +end + +RSpec.shared_examples 'grants terraform module packages access' do |user_type, status, add_member = true| + context "for user type #{user_type}" do + before do + project.send("add_#{user_type}", user) if add_member && user_type != :anonymous + end + + it_behaves_like 'returning response status', status + end +end + +RSpec.shared_examples 'grants terraform module package file access' do |user_type, status, add_member = true| + context "for user type #{user_type}" do + before do + project.send("add_#{user_type}", user) if add_member && user_type != :anonymous + end + + it_behaves_like 'returning response status', status + it_behaves_like 'a package tracking event', described_class.name, 'pull_package' + end +end + +RSpec.shared_examples 'rejects terraform module packages access' do |user_type, status, add_member = true| + context "for user type #{user_type}" do + before do + project.send("add_#{user_type}", user) if add_member && user_type != :anonymous + end + + it_behaves_like 'returning response status', status + end +end + +RSpec.shared_examples 'process terraform module workhorse authorization' do |user_type, status, add_member = true| + context "for user type #{user_type}" do + before do + project.send("add_#{user_type}", user) if add_member && user_type != :anonymous + end + + it_behaves_like 'returning response status', status + + it 'has the proper content type' do + subject + + 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 + { 'HTTP_PRIVATE_TOKEN' => 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 terraform module upload' do |user_type, status, add_member = true| + RSpec.shared_examples 'creates terraform module package files' do + it 'creates package files', :aggregate_failures do + 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('mymodule-mysystem-1.0.0.tgz') + end + end + + context "for user type #{user_type}" do + before do + project.send("add_#{user_type}", user) if add_member && user_type != :anonymous + 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 terraform module package files' + it_behaves_like 'a package tracking event', described_class.name, '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) { { file: fog_file, 'file.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 terraform module package files' + + ['123123', '../../123123'].each do |remote_id| + context "with invalid remote_id: #{remote_id}" do + let(:params) do + { + file: fog_file, + 'file.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 terraform module 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 terraform module package files' + end + end + end + end +end diff --git a/spec/support/shared_examples/requests/api/time_tracking_shared_examples.rb b/spec/support/shared_examples/requests/api/time_tracking_shared_examples.rb index fb6d6603beb..afc902dd184 100644 --- a/spec/support/shared_examples/requests/api/time_tracking_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/time_tracking_shared_examples.rb @@ -125,6 +125,22 @@ RSpec.shared_examples 'time tracking endpoints' do |issuable_name| expect(json_response['message']['base'].first).to eq(_('Time to subtract exceeds the total time spent')) end end + + if issuable_name == 'merge_request' + it 'calls update service with :use_specialized_service param' do + expect(::MergeRequests::UpdateService).to receive(:new).with(project: project, current_user: user, params: hash_including(use_specialized_service: true)) + + post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/add_spent_time", user), params: { duration: '2h' } + end + end + + if issuable_name == 'issue' + it 'calls update service without :use_specialized_service param' do + expect(::Issues::UpdateService).to receive(:new).with(project: project, current_user: user, params: hash_not_including(use_specialized_service: true)) + + post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/add_spent_time", user), params: { duration: '2h' } + end + end end describe "POST /projects/:id/#{issuable_collection_name}/:#{issuable_name}_id/reset_spent_time" do diff --git a/spec/support/shared_examples/requests/clusters/integrations_controller_shared_examples.rb b/spec/support/shared_examples/requests/clusters/integrations_controller_shared_examples.rb index 490c7d12115..91fdcbd9b1d 100644 --- a/spec/support/shared_examples/requests/clusters/integrations_controller_shared_examples.rb +++ b/spec/support/shared_examples/requests/clusters/integrations_controller_shared_examples.rb @@ -2,7 +2,7 @@ RSpec.shared_examples '#create_or_update action' do let(:params) do - { integration: { application_type: Clusters::Applications::Prometheus.application_name, enabled: true } } + { integration: { application_type: 'prometheus', enabled: true } } end let(:path) { raise NotImplementedError } diff --git a/spec/support/shared_examples/requests/rack_attack_shared_examples.rb b/spec/support/shared_examples/requests/rack_attack_shared_examples.rb index 926da827e75..95817624658 100644 --- a/spec/support/shared_examples/requests/rack_attack_shared_examples.rb +++ b/spec/support/shared_examples/requests/rack_attack_shared_examples.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true # # Requires let variables: -# * throttle_setting_prefix: "throttle_authenticated_api", "throttle_authenticated_web", "throttle_protected_paths" +# * throttle_setting_prefix: "throttle_authenticated_api", "throttle_authenticated_web", "throttle_protected_paths", "throttle_authenticated_packages_api" # * request_method # * request_args # * other_user_request_args @@ -13,7 +13,8 @@ RSpec.shared_examples 'rate-limited token-authenticated requests' do { "throttle_protected_paths" => "throttle_authenticated_protected_paths_api", "throttle_authenticated_api" => "throttle_authenticated_api", - "throttle_authenticated_web" => "throttle_authenticated_web" + "throttle_authenticated_web" => "throttle_authenticated_web", + "throttle_authenticated_packages_api" => "throttle_authenticated_packages_api" } end diff --git a/spec/support/shared_examples/row_lock_shared_examples.rb b/spec/support/shared_examples/row_lock_shared_examples.rb new file mode 100644 index 00000000000..5e003172215 --- /dev/null +++ b/spec/support/shared_examples/row_lock_shared_examples.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +# Ensure that a SQL command to lock this row(s) was requested. +# Ensure a transaction also occurred. +# Be careful! This form of spec is not foolproof, but better than nothing. + +RSpec.shared_examples 'locked row' do + it "has locked row" do + table_name = row.class.table_name + ids_regex = /SELECT.*FROM.*#{table_name}.*"#{table_name}"."id" = #{row.id}.+FOR UPDATE/m + + expect(recorded_queries.log).to include a_string_matching 'SAVEPOINT' + expect(recorded_queries.log).to include a_string_matching ids_regex + end +end + +RSpec.shared_examples 'locked rows' do + it "has locked rows" do + table_name = rows.first.class.table_name + + row_ids = rows.map(&:id).join(', ') + ids_regex = /SELECT.+FROM.+"#{table_name}".+"#{table_name}"."id" IN \(#{row_ids}\).+FOR UPDATE/m + + expect(recorded_queries.log).to include a_string_matching 'SAVEPOINT' + expect(recorded_queries.log).to include a_string_matching ids_regex + end +end diff --git a/spec/support/shared_examples/serializers/environment_serializer_shared_examples.rb b/spec/support/shared_examples/serializers/environment_serializer_shared_examples.rb index 00146335ef7..9d7ae6bcb3d 100644 --- a/spec/support/shared_examples/serializers/environment_serializer_shared_examples.rb +++ b/spec/support/shared_examples/serializers/environment_serializer_shared_examples.rb @@ -20,9 +20,27 @@ RSpec.shared_examples 'avoid N+1 on environments serialization' do expect { serialize(grouping: false) }.not_to exceed_query_limit(control.count) end - def serialize(grouping:) + it 'does not preload for environments that does not exist in the page', :request_store do + create_environment_with_associations(project) + + first_page_query = ActiveRecord::QueryRecorder.new do + serialize(grouping: false, query: { page: 1, per_page: 1 } ) + end + + second_page_query = ActiveRecord::QueryRecorder.new do + serialize(grouping: false, query: { page: 2, per_page: 1 } ) + end + + expect(second_page_query.count).to be < first_page_query.count + end + + def serialize(grouping:, query: nil) + query ||= { page: 1, per_page: 1 } + request = double(url: "#{Gitlab.config.gitlab.url}:8080/api/v4/projects?#{query.to_query}", query_parameters: query) + EnvironmentSerializer.new(current_user: user, project: project).yield_self do |serializer| serializer.within_folders if grouping + serializer.with_pagination(request, spy('response')) serializer.represent(Environment.where(project: project)) end end diff --git a/spec/support/shared_examples/serializers/pipeline_artifacts_shared_example.rb b/spec/support/shared_examples/serializers/pipeline_artifacts_shared_example.rb deleted file mode 100644 index d5ffd5e7510..00000000000 --- a/spec/support/shared_examples/serializers/pipeline_artifacts_shared_example.rb +++ /dev/null @@ -1,21 +0,0 @@ -# frozen_string_literal: true -RSpec.shared_examples 'public artifacts' do - let_it_be(:project) { create(:project, :public) } - let(:pipeline) { create(:ci_empty_pipeline, status: :success, project: project) } - - context 'that has artifacts' do - let!(:build) { create(:ci_build, :success, :artifacts, pipeline: pipeline) } - - it 'contains information about artifacts' do - expect(subject[:details][:artifacts].length).to eq(1) - end - end - - context 'that has non public artifacts' do - let!(:build) { create(:ci_build, :success, :artifacts, :non_public_artifacts, pipeline: pipeline) } - - it 'does not contain information about artifacts' do - expect(subject[:details][:artifacts].length).to eq(0) - end - end -end diff --git a/spec/support/shared_examples/services/alert_management/alert_processing/alert_firing_shared_examples.rb b/spec/support/shared_examples/services/alert_management/alert_processing/alert_firing_shared_examples.rb new file mode 100644 index 00000000000..218a3462c35 --- /dev/null +++ b/spec/support/shared_examples/services/alert_management/alert_processing/alert_firing_shared_examples.rb @@ -0,0 +1,161 @@ +# frozen_string_literal: true + +# This shared_example requires the following variables: +# - `service`, the service which includes AlertManagement::AlertProcessing +RSpec.shared_examples 'creates an alert management alert or errors' do + it { is_expected.to be_success } + + it 'creates AlertManagement::Alert' do + expect(Gitlab::AppLogger).not_to receive(:warn) + + expect { subject }.to change(AlertManagement::Alert, :count).by(1) + end + + it 'executes the alert service hooks' do + expect_next_instance_of(AlertManagement::Alert) do |alert| + expect(alert).to receive(:execute_services) + end + + subject + end + + context 'and fails to save' do + let(:errors) { double(messages: { hosts: ['hosts array is over 255 chars'] })} + + before do + allow(service).to receive(:alert).and_call_original + allow(service).to receive_message_chain(:alert, :save).and_return(false) + allow(service).to receive_message_chain(:alert, :errors).and_return(errors) + end + + it_behaves_like 'alerts service responds with an error', :bad_request + + it 'writes a warning to the log' do + expect(Gitlab::AppLogger).to receive(:warn).with( + message: "Unable to create AlertManagement::Alert from #{source}", + project_id: project.id, + alert_errors: { hosts: ['hosts array is over 255 chars'] } + ) + + subject + end + end +end + +# This shared_example requires the following variables: +# - last_alert_attributes, last created alert +# - project, project that alert created +# - payload_raw, hash representation of payload +# - environment, project's environment +# - fingerprint, fingerprint hash +RSpec.shared_examples 'properly assigns the alert properties' do + specify do + subject + + expect(last_alert_attributes).to match({ + project_id: project.id, + title: payload_raw.fetch(:title), + started_at: Time.zone.parse(payload_raw.fetch(:start_time)), + severity: payload_raw.fetch(:severity, nil), + status: AlertManagement::Alert.status_value(:triggered), + events: 1, + domain: domain, + hosts: payload_raw.fetch(:hosts, nil), + payload: payload_raw.with_indifferent_access, + issue_id: nil, + description: payload_raw.fetch(:description, nil), + monitoring_tool: payload_raw.fetch(:monitoring_tool, nil), + service: payload_raw.fetch(:service, nil), + fingerprint: Digest::SHA1.hexdigest(fingerprint), + environment_id: environment.id, + ended_at: nil, + prometheus_alert_id: nil + }.with_indifferent_access) + end +end + +RSpec.shared_examples 'does not create an alert management alert' do + specify do + expect { subject }.not_to change(AlertManagement::Alert, :count) + end +end + +# This shared_example requires the following variables: +# - `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 { subject }.to change { alert.reload.events }.by(1) + + expect(subject).to be_success + end + + it_behaves_like 'does not create an alert management alert' +end + +# This shared_example requires the following variables: +# - `alert`, the alert for which events should not be incremented +RSpec.shared_examples 'does not add an alert management alert event' do + specify do + expect { subject }.not_to change { alert.reload.events } + end +end + +RSpec.shared_examples 'processes new firing alert' do + include_examples 'processes never-before-seen alert' + + context 'for an existing alert with the same fingerprint' do + let_it_be(:gitlab_fingerprint) { Digest::SHA1.hexdigest(fingerprint) } + + context 'which is triggered' do + let_it_be(:alert) { create(:alert_management_alert, :triggered, fingerprint: gitlab_fingerprint, project: project) } + + it_behaves_like 'adds an alert management alert event' + it_behaves_like 'sends alert notification emails if enabled' + it_behaves_like 'processes incident issues if enabled', with_issue: true + + it_behaves_like 'does not create an alert management alert' + it_behaves_like 'does not create a system note for alert' + + context 'with an existing resolved alert as well' do + let_it_be(:resolved_alert) { create(:alert_management_alert, :resolved, project: project, fingerprint: gitlab_fingerprint) } + + it_behaves_like 'adds an alert management alert event' + it_behaves_like 'sends alert notification emails if enabled' + it_behaves_like 'processes incident issues if enabled', with_issue: true + + it_behaves_like 'does not create an alert management alert' + it_behaves_like 'does not create a system note for alert' + end + end + + context 'which is acknowledged' do + let_it_be(:alert) { create(:alert_management_alert, :acknowledged, fingerprint: gitlab_fingerprint, project: project) } + + it_behaves_like 'adds an alert management alert event' + it_behaves_like 'processes incident issues if enabled', with_issue: true + + it_behaves_like 'does not create an alert management alert' + it_behaves_like 'does not create a system note for alert' + it_behaves_like 'does not send alert notification emails' + end + + context 'which is ignored' do + let_it_be(:alert) { create(:alert_management_alert, :ignored, fingerprint: gitlab_fingerprint, project: project) } + + it_behaves_like 'adds an alert management alert event' + it_behaves_like 'processes incident issues if enabled', with_issue: true + + it_behaves_like 'does not create an alert management alert' + it_behaves_like 'does not create a system note for alert' + it_behaves_like 'does not send alert notification emails' + end + + context 'which is resolved' do + let_it_be(:alert) { create(:alert_management_alert, :resolved, fingerprint: gitlab_fingerprint, project: project) } + + include_examples 'processes never-before-seen alert' + end + end +end diff --git a/spec/support/shared_examples/services/alert_management/alert_processing/alert_recovery_shared_examples.rb b/spec/support/shared_examples/services/alert_management/alert_processing/alert_recovery_shared_examples.rb new file mode 100644 index 00000000000..86e7da5bcbe --- /dev/null +++ b/spec/support/shared_examples/services/alert_management/alert_processing/alert_recovery_shared_examples.rb @@ -0,0 +1,113 @@ +# frozen_string_literal: true + +# This shared_example requires the following variables: +# - `alert`, the alert to be resolved +RSpec.shared_examples 'resolves an existing alert management alert' do + it 'sets the end time and status' do + expect(Gitlab::AppLogger).not_to receive(:warn) + + expect { subject } + .to change { alert.reload.resolved? }.to(true) + .and change { alert.ended_at.present? }.to(true) + + expect(subject).to be_success + end +end + +# This shared_example requires the following variables: +# - `alert`, the alert not to be updated +RSpec.shared_examples 'does not change the alert end time' do + specify do + expect { subject }.not_to change { alert.reload.ended_at } + end +end + +# This shared_example requires the following variables: +# - `project`, expected project for an incoming alert +# - `service`, a service which includes AlertManagement::AlertProcessing +# - `alert` (optional), the alert which should fail to resolve. If not +# included, the log is expected to correspond to a new alert +RSpec.shared_examples 'writes a warning to the log for a failed alert status update' do + before do + allow(service).to receive(:alert).and_call_original + allow(service).to receive_message_chain(:alert, :resolve).and_return(false) + end + + specify do + expect(Gitlab::AppLogger).to receive(:warn).with( + message: 'Unable to update AlertManagement::Alert status to resolved', + project_id: project.id, + alert_id: alert ? alert.id : (last_alert_id + 1) + ) + + # Failure to resolve a recovery alert is not a critical failure + expect(subject).to be_success + end + + private + + def last_alert_id + AlertManagement::Alert.connection + .select_value("SELECT nextval('#{AlertManagement::Alert.sequence_name}')") + end +end + +RSpec.shared_examples 'processes recovery alert' do + context 'seen for the first time' do + let(:alert) { AlertManagement::Alert.last } + + include_examples 'processes never-before-seen recovery alert' + end + + context 'for an existing alert with the same fingerprint' do + let_it_be(:gitlab_fingerprint) { Digest::SHA1.hexdigest(fingerprint) } + + context 'which is triggered' do + let_it_be(:alert) { create(:alert_management_alert, :triggered, project: project, fingerprint: gitlab_fingerprint, monitoring_tool: source) } + + it_behaves_like 'resolves an existing alert management alert' + it_behaves_like 'creates expected system notes for alert', :recovery_alert, :resolve_alert + it_behaves_like 'sends alert notification emails if enabled' + it_behaves_like 'closes related incident if enabled' + it_behaves_like 'writes a warning to the log for a failed alert status update' + + it_behaves_like 'does not create an alert management alert' + it_behaves_like 'does not process incident issues' + it_behaves_like 'does not add an alert management alert event' + end + + context 'which is ignored' do + let_it_be(:alert) { create(:alert_management_alert, :ignored, project: project, fingerprint: gitlab_fingerprint, monitoring_tool: source) } + + it_behaves_like 'resolves an existing alert management alert' + it_behaves_like 'creates expected system notes for alert', :recovery_alert, :resolve_alert + it_behaves_like 'sends alert notification emails if enabled' + it_behaves_like 'closes related incident if enabled' + it_behaves_like 'writes a warning to the log for a failed alert status update' + + it_behaves_like 'does not create an alert management alert' + it_behaves_like 'does not process incident issues' + it_behaves_like 'does not add an alert management alert event' + end + + context 'which is acknowledged' do + let_it_be(:alert) { create(:alert_management_alert, :acknowledged, project: project, fingerprint: gitlab_fingerprint, monitoring_tool: source) } + + it_behaves_like 'resolves an existing alert management alert' + it_behaves_like 'creates expected system notes for alert', :recovery_alert, :resolve_alert + it_behaves_like 'sends alert notification emails if enabled' + it_behaves_like 'closes related incident if enabled' + it_behaves_like 'writes a warning to the log for a failed alert status update' + + it_behaves_like 'does not create an alert management alert' + it_behaves_like 'does not process incident issues' + it_behaves_like 'does not add an alert management alert event' + end + + context 'which is resolved' do + let_it_be(:alert) { create(:alert_management_alert, :resolved, project: project, fingerprint: gitlab_fingerprint, monitoring_tool: source) } + + include_examples 'processes never-before-seen recovery alert' + end + end +end 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 new file mode 100644 index 00000000000..c6ac07b6dd5 --- /dev/null +++ b/spec/support/shared_examples/services/alert_management/alert_processing/incident_creation_shared_examples.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +# Expects usage of 'incident settings enabled' context. +# +# This shared_example includes the following option: +# - with_issue: includes a test for when the defined `alert` has an associated issue +# +# This shared_example requires the following variables: +# - `alert`, required if :with_issue is true +RSpec.shared_examples 'processes incident issues if enabled' do |with_issue: false| + include_examples 'processes incident issues', with_issue + + context 'with incident setting disabled' do + let(:create_issue) { false } + + it_behaves_like 'does not process incident issues' + end +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) + end + end + + specify do + expect(IncidentManagement::ProcessAlertWorkerV2) + .to receive(:perform_async) + .with(kind_of(Integer)) + + Sidekiq::Testing.inline! do + expect(subject).to be_success + end + end + + context 'with issue', if: with_issue do + before do + alert.update!(issue: create(:issue, project: project)) + end + + it_behaves_like 'does not process incident issues' + end +end + +RSpec.shared_examples 'does not process incident issues' do + specify do + expect(IncidentManagement::ProcessAlertWorkerV2).not_to receive(:perform_async) + + subject + end +end diff --git a/spec/support/shared_examples/services/alert_management/alert_processing/incident_resolution_shared_examples.rb b/spec/support/shared_examples/services/alert_management/alert_processing/incident_resolution_shared_examples.rb new file mode 100644 index 00000000000..132f1e0422e --- /dev/null +++ b/spec/support/shared_examples/services/alert_management/alert_processing/incident_resolution_shared_examples.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +# Expects usage of 'incident settings enabled' context. +# +# This shared_example requires the following variables: +# - `alert`, alert for which related incidents should be closed +# - `project`, project of the alert +RSpec.shared_examples 'closes related incident if enabled' do + context 'with issue' do + before do + alert.update!(issue: create(:issue, project: project)) + end + + it { expect { subject }.to change { alert.issue.reload.closed? }.from(false).to(true) } + it { expect { subject }.to change(ResourceStateEvent, :count).by(1) } + end + + context 'without issue' do + it { expect { subject }.not_to change { alert.reload.issue } } + it { expect { subject }.not_to change(ResourceStateEvent, :count) } + end + + context 'with incident setting disabled' do + let(:auto_close_incident) { false } + + it_behaves_like 'does not close related incident' + end +end + +RSpec.shared_examples 'does not close related incident' do + context 'with issue' do + before do + alert.update!(issue: create(:issue, project: project)) + end + + it { expect { subject }.not_to change { alert.issue.reload.state } } + it { expect { subject }.not_to change(ResourceStateEvent, :count) } + end + + context 'without issue' do + it { expect { subject }.not_to change { alert.reload.issue } } + it { expect { subject }.not_to change(ResourceStateEvent, :count) } + end +end diff --git a/spec/support/shared_examples/services/alert_management/alert_processing/notifications_shared_examples.rb b/spec/support/shared_examples/services/alert_management/alert_processing/notifications_shared_examples.rb new file mode 100644 index 00000000000..5f30b58176b --- /dev/null +++ b/spec/support/shared_examples/services/alert_management/alert_processing/notifications_shared_examples.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +# Expects usage of 'incident settings enabled' context. +# +# This shared_example includes the following option: +# - count: number of notifications expected to be sent +RSpec.shared_examples 'sends alert notification emails if enabled' do |count: 1| + include_examples 'sends alert notification emails', count + + context 'with email setting disabled' do + let(:send_email) { false } + + it_behaves_like 'does not send alert notification emails' + end +end + +RSpec.shared_examples 'sends alert notification emails' do |count: 1| + let(:notification_async) { double(NotificationService::Async) } + + specify do + allow(NotificationService).to receive_message_chain(:new, :async).and_return(notification_async) + expect(notification_async).to receive(:prometheus_alerts_fired).exactly(count).times + + subject + end +end + +RSpec.shared_examples 'does not send alert notification emails' do + specify do + expect(NotificationService).not_to receive(:new) + + subject + end +end diff --git a/spec/support/shared_examples/services/alert_management/alert_processing/system_notes_shared_examples.rb b/spec/support/shared_examples/services/alert_management/alert_processing/system_notes_shared_examples.rb new file mode 100644 index 00000000000..57d598c0259 --- /dev/null +++ b/spec/support/shared_examples/services/alert_management/alert_processing/system_notes_shared_examples.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +# This shared_example includes the following option: +# - notes: any of [:new_alert, :recovery_alert, :resolve_alert]. +# Represents which notes are expected to be created. +# +# This shared_example requires the following variables: +# - `source` (optional), the monitoring tool or integration name +# expected in the applicable system notes +RSpec.shared_examples 'creates expected system notes for alert' do |*notes| + let(:expected_note_count) { expected_notes.length } + let(:new_notes) { Note.last(expected_note_count).pluck(:note) } + let(:expected_notes) do + { + new_alert: source, + recovery_alert: source, + resolve_alert: 'Resolved' + }.slice(*notes) + end + + it "for #{notes.join(', ')}" do + expect { subject }.to change(Note, :count).by(expected_note_count) + + expected_notes.each_value.with_index do |value, index| + expect(new_notes[index]).to include(value) + end + end +end + +RSpec.shared_examples 'does not create a system note for alert' do + specify do + expect { subject }.not_to change(Note, :count) + end +end diff --git a/spec/support/shared_examples/services/alert_management_shared_examples.rb b/spec/support/shared_examples/services/alert_management_shared_examples.rb index d9f28a97a0f..827ae42f970 100644 --- a/spec/support/shared_examples/services/alert_management_shared_examples.rb +++ b/spec/support/shared_examples/services/alert_management_shared_examples.rb @@ -1,111 +1,77 @@ # frozen_string_literal: true -RSpec.shared_examples 'creates an alert management alert' do - it { is_expected.to be_success } +RSpec.shared_examples 'alerts service responds with an error and takes no actions' do |http_status| + include_examples 'alerts service responds with an error', http_status - it 'creates AlertManagement::Alert' do - expect { subject }.to change(AlertManagement::Alert, :count).by(1) - end - - it 'executes the alert service hooks' do - expect_next_instance_of(AlertManagement::Alert) do |alert| - expect(alert).to receive(:execute_services) - end + it_behaves_like 'does not create an alert management alert' + it_behaves_like 'does not create a system note for alert' + it_behaves_like 'does not process incident issues' + it_behaves_like 'does not send alert notification emails' +end - subject +RSpec.shared_examples 'alerts service responds with an error' do |http_status| + specify do + expect(subject).to be_error + expect(subject.http_status).to eq(http_status) end end # This shared_example requires the following variables: -# - last_alert_attributes, last created alert -# - project, project that alert created -# - payload_raw, hash representation of payload -# - environment, project's environment -# - fingerprint, fingerprint hash -RSpec.shared_examples 'assigns the alert properties' do - it 'ensures that created alert has all data properly assigned' do - subject - - expect(last_alert_attributes).to match( - project_id: project.id, - title: payload_raw.fetch(:title), - started_at: Time.zone.parse(payload_raw.fetch(:start_time)), - severity: payload_raw.fetch(:severity), - status: AlertManagement::Alert.status_value(:triggered), - events: 1, - domain: domain, - hosts: payload_raw.fetch(:hosts), - payload: payload_raw.with_indifferent_access, - issue_id: nil, - description: payload_raw.fetch(:description), - monitoring_tool: payload_raw.fetch(:monitoring_tool), - service: payload_raw.fetch(:service), - fingerprint: Digest::SHA1.hexdigest(fingerprint), - environment_id: environment.id, - ended_at: nil, - prometheus_alert_id: nil +# - `service`, a service which includes ::IncidentManagement::Settings +RSpec.shared_context 'incident management settings enabled' do + let(:auto_close_incident) { true } + let(:create_issue) { true } + let(:send_email) { true } + + let(:incident_management_setting) do + double( + auto_close_incident?: auto_close_incident, + create_issue?: create_issue, + send_email?: send_email ) end -end -RSpec.shared_examples 'does not an create alert management alert' do - it 'does not create alert' do - expect { subject }.not_to change(AlertManagement::Alert, :count) + before do + allow(ProjectServiceWorker).to receive(:perform_async) + allow(service) + .to receive(:incident_management_setting) + .and_return(incident_management_setting) end end -RSpec.shared_examples 'adds an alert management alert event' do - it { is_expected.to be_success } - - it 'does not create an alert' do - expect { subject }.not_to change(AlertManagement::Alert, :count) - end - - it 'increases alert events count' do - expect { subject }.to change { alert.reload.events }.by(1) - end - - it 'does not executes the alert service hooks' do - expect(alert).not_to receive(:execute_services) - - subject - end +RSpec.shared_examples 'processes never-before-seen alert' do + it_behaves_like 'creates an alert management alert or errors' + it_behaves_like 'creates expected system notes for alert', :new_alert + it_behaves_like 'processes incident issues if enabled' + it_behaves_like 'sends alert notification emails if enabled' end -RSpec.shared_examples 'processes incident issues' do - let(:create_incident_service) { spy } - - before do - allow_any_instance_of(AlertManagement::Alert).to receive(:execute_services) +RSpec.shared_examples 'processes never-before-seen recovery alert' do + it_behaves_like 'creates an alert management alert or errors' + it_behaves_like 'creates expected system notes for alert', :new_alert, :recovery_alert, :resolve_alert + it_behaves_like 'sends alert notification emails if enabled' + it_behaves_like 'does not process incident issues' + it_behaves_like 'writes a warning to the log for a failed alert status update' do + let(:alert) { nil } # Ensure the next alert id is used end - it 'processes issues' do - expect(IncidentManagement::ProcessAlertWorker) - .to receive(:perform_async) - .with(nil, nil, kind_of(Integer)) - .once + it 'resolves the alert' do + subject - Sidekiq::Testing.inline! do - expect(subject).to be_success - end + expect(AlertManagement::Alert.last.ended_at).to be_present + expect(AlertManagement::Alert.last.resolved?).to be(true) end end -RSpec.shared_examples 'does not process incident issues' do - it 'does not process issues' do - expect(IncidentManagement::ProcessAlertWorker) - .not_to receive(:perform_async) +RSpec.shared_examples 'processes one firing and one resolved prometheus alerts' do + it 'creates AlertManagement::Alert' do + expect(Gitlab::AppLogger).not_to receive(:warn) - expect(subject).to be_success + expect { subject } + .to change(AlertManagement::Alert, :count).by(2) + .and change(Note, :count).by(4) end -end - -RSpec.shared_examples 'does not process incident issues due to error' do |http_status:| - it 'does not process issues' do - expect(IncidentManagement::ProcessAlertWorker) - .not_to receive(:perform_async) - expect(subject).to be_error - expect(subject.http_status).to eq(http_status) - end + it_behaves_like 'processes incident issues' + it_behaves_like 'sends alert notification emails', count: 2 end diff --git a/spec/support/shared_examples/services/boards/boards_recent_visit_shared_examples.rb b/spec/support/shared_examples/services/boards/boards_recent_visit_shared_examples.rb new file mode 100644 index 00000000000..68ea460dabc --- /dev/null +++ b/spec/support/shared_examples/services/boards/boards_recent_visit_shared_examples.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'boards recent visit' do + let_it_be(:user) { create(:user) } + + describe '#visited' do + it 'creates a visit if one does not exists' do + expect { described_class.visited!(user, board) }.to change(described_class, :count).by(1) + end + + shared_examples 'was visited previously' do + let_it_be(:visit) do + create(visit_relation, + board_parent_relation => board_parent, + board_relation => board, + user: user, + updated_at: 7.days.ago + ) + end + + it 'updates the timestamp' do + freeze_time do + described_class.visited!(user, board) + + expect(described_class.count).to eq 1 + expect(described_class.first.updated_at).to be_like_time(Time.zone.now) + end + end + end + + it_behaves_like 'was visited previously' + + context 'when we try to create a visit that is not unique' do + before do + expect(described_class).to receive(:find_or_create_by).and_raise(ActiveRecord::RecordNotUnique, 'record not unique') + expect(described_class).to receive(:find_or_create_by).and_return(visit) + end + + it_behaves_like 'was visited previously' + end + end + + describe '#latest' do + def create_visit(time) + create(visit_relation, board_parent_relation => board_parent, user: user, updated_at: time) + end + + it 'returns the most recent visited' do + create_visit(7.days.ago) + create_visit(5.days.ago) + recent = create_visit(1.day.ago) + + expect(described_class.latest(user, board_parent)).to eq recent + end + + it 'returns last 3 visited boards' do + create_visit(7.days.ago) + visit1 = create_visit(3.days.ago) + visit2 = create_visit(2.days.ago) + visit3 = create_visit(5.days.ago) + + expect(described_class.latest(user, board_parent, count: 3)).to eq([visit2, visit1, visit3]) + end + end +end diff --git a/spec/support/shared_examples/services/boards/create_service_shared_examples.rb b/spec/support/shared_examples/services/boards/create_service_shared_examples.rb new file mode 100644 index 00000000000..63b5e3a5a84 --- /dev/null +++ b/spec/support/shared_examples/services/boards/create_service_shared_examples.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'boards recent visit create service' do + let_it_be(:user) { create(:user) } + + subject(:service) { described_class.new(board.resource_parent, user) } + + it 'returns nil when there is no user' do + service.current_user = nil + + expect(service.execute(board)).to be_nil + end + + it 'returns nil when database is read only' do + allow(Gitlab::Database).to receive(:read_only?) { true } + + expect(service.execute(board)).to be_nil + end + + it 'records the visit' do + expect(model).to receive(:visited!).once + + service.execute(board) + end +end diff --git a/spec/support/shared_examples/services/boards/issues_move_service_shared_examples.rb b/spec/support/shared_examples/services/boards/issues_move_service_shared_examples.rb index 4aa5d7d890b..7d4fbeea0dc 100644 --- a/spec/support/shared_examples/services/boards/issues_move_service_shared_examples.rb +++ b/spec/support/shared_examples/services/boards/issues_move_service_shared_examples.rb @@ -146,7 +146,7 @@ RSpec.shared_examples 'issues move service' do |group| params.merge!(move_after_id: issue1.id, move_before_id: issue2.id) match_params = { move_between_ids: [issue1.id, issue2.id], board_group_id: parent.id } - expect(Issues::UpdateService).to receive(:new).with(issue.project, user, match_params).and_return(double(execute: build(:issue))) + expect(Issues::UpdateService).to receive(:new).with(project: issue.project, current_user: user, params: match_params).and_return(double(execute: build(:issue))) described_class.new(parent, user, params).execute(issue) end diff --git a/spec/support/shared_examples/services/boards/lists_destroy_service_shared_examples.rb b/spec/support/shared_examples/services/boards/lists_destroy_service_shared_examples.rb index 94da405e491..af88644ced7 100644 --- a/spec/support/shared_examples/services/boards/lists_destroy_service_shared_examples.rb +++ b/spec/support/shared_examples/services/boards/lists_destroy_service_shared_examples.rb @@ -3,30 +3,27 @@ RSpec.shared_examples 'lists destroy service' do context 'when list type is label' do it 'removes list from board' do - list = create(:list, board: board) service = described_class.new(parent, user) expect { service.execute(list) }.to change(board.lists, :count).by(-1) end it 'decrements position of higher lists' do - development = create(:list, board: board, position: 0) - review = create(:list, board: board, position: 1) - staging = create(:list, board: board, position: 2) - closed = board.lists.closed.first + development = create(list_type, params.merge(position: 0)) + review = create(list_type, params.merge(position: 1)) + staging = create(list_type, params.merge(position: 2)) described_class.new(parent, user).execute(development) expect(review.reload.position).to eq 0 expect(staging.reload.position).to eq 1 - expect(closed.reload.position).to be_nil + expect(closed_list.reload.position).to be_nil end end it 'does not remove list from board when list type is closed' do - list = board.lists.closed.first service = described_class.new(parent, user) - expect { service.execute(list) }.not_to change(board.lists, :count) + expect { service.execute(closed_list) }.not_to change(board.lists, :count) end end diff --git a/spec/support/shared_examples/services/common_system_notes_shared_examples.rb b/spec/support/shared_examples/services/common_system_notes_shared_examples.rb index 7b277d4bede..ce412ef55de 100644 --- a/spec/support/shared_examples/services/common_system_notes_shared_examples.rb +++ b/spec/support/shared_examples/services/common_system_notes_shared_examples.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true RSpec.shared_examples 'system note creation' do |update_params, note_text| - subject { described_class.new(project, user).execute(issuable, old_labels: []) } + subject { described_class.new(project: project, current_user: user).execute(issuable, old_labels: []) } before do issuable.assign_attributes(update_params) @@ -18,7 +18,7 @@ RSpec.shared_examples 'system note creation' do |update_params, note_text| end RSpec.shared_examples 'draft notes creation' do |action| - subject { described_class.new(project, user).execute(issuable, old_labels: []) } + subject { described_class.new(project: project, current_user: user).execute(issuable, old_labels: []) } it 'creates Draft toggle and title change notes' do expect { subject }.to change { Note.count }.from(0).to(2) diff --git a/spec/support/shared_examples/services/destroy_label_links_shared_examples.rb b/spec/support/shared_examples/services/destroy_label_links_shared_examples.rb new file mode 100644 index 00000000000..d2b52468c25 --- /dev/null +++ b/spec/support/shared_examples/services/destroy_label_links_shared_examples.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +RSpec.shared_examples_for 'service deleting label links of an issuable' do + let_it_be(:label_link) { create(:label_link, target: target) } + + def execute + described_class.new(target.id, target.class.name).execute + end + + it 'deletes label links for specified target ID and type' do + control_count = ActiveRecord::QueryRecorder.new { execute }.count + + # Create more label links for the target + create(:label_link, target: target) + create(:label_link, target: target) + + expect { execute }.not_to exceed_query_limit(control_count) + expect(target.reload.label_links.count).to eq(0) + end +end diff --git a/spec/support/shared_examples/services/issuable/destroy_service_shared_examples.rb b/spec/support/shared_examples/services/issuable/destroy_service_shared_examples.rb index ccc287c10de..e776c098fa0 100644 --- a/spec/support/shared_examples/services/issuable/destroy_service_shared_examples.rb +++ b/spec/support/shared_examples/services/issuable/destroy_service_shared_examples.rb @@ -1,10 +1,6 @@ # frozen_string_literal: true shared_examples_for 'service deleting todos' do - before do - stub_feature_flags(destroy_issuable_todos_async: group) - end - it 'destroys associated todos asynchronously' do expect(TodosDestroyer::DestroyedIssuableWorker) .to receive(:perform_async) @@ -12,20 +8,14 @@ shared_examples_for 'service deleting todos' do subject.execute(issuable) end +end - context 'when destroy_issuable_todos_async feature is disabled for group' do - before do - stub_feature_flags(destroy_issuable_todos_async: false) - end - - it 'destroy associated todos synchronously' do - expect_next_instance_of(TodosDestroyer::DestroyedIssuableWorker) do |worker| - expect(worker) - .to receive(:perform) - .with(issuable.id, issuable.class.name) - end +shared_examples_for 'service deleting label links' do + it 'destroys associated label links asynchronously' do + expect(Issuable::LabelLinksDestroyWorker) + .to receive(:perform_async) + .with(issuable.id, issuable.class.name) - subject.execute(issuable) - end + subject.execute(issuable) end end diff --git a/spec/support/shared_examples/services/issuable_shared_examples.rb b/spec/support/shared_examples/services/issuable_shared_examples.rb index 5b3e0f9e0b9..a50a386afe1 100644 --- a/spec/support/shared_examples/services/issuable_shared_examples.rb +++ b/spec/support/shared_examples/services/issuable_shared_examples.rb @@ -4,14 +4,14 @@ RSpec.shared_examples 'cache counters invalidator' do it 'invalidates counter cache for assignees' do expect_any_instance_of(User).to receive(:invalidate_merge_request_cache_counts) - described_class.new(project, user, {}).execute(merge_request) + described_class.new(project: project, current_user: user).execute(merge_request) end end RSpec.shared_examples 'updating a single task' do def update_issuable(opts) issuable = try(:issue) || try(:merge_request) - described_class.new(project, user, opts).execute(issuable) + described_class.new(project: project, current_user: user, params: opts).execute(issuable) end before do diff --git a/spec/support/shared_examples/services/merge_request_shared_examples.rb b/spec/support/shared_examples/services/merge_request_shared_examples.rb index 178b6bc47e1..d2595b92cbc 100644 --- a/spec/support/shared_examples/services/merge_request_shared_examples.rb +++ b/spec/support/shared_examples/services/merge_request_shared_examples.rb @@ -70,7 +70,7 @@ RSpec.shared_examples 'merge request reviewers cache counters invalidator' do it 'invalidates counter cache for reviewers' do expect(merge_request.reviewers).to all(receive(:invalidate_merge_request_cache_counts)) - described_class.new(project, user, {}).execute(merge_request) + described_class.new(project: project, current_user: user).execute(merge_request) end end @@ -86,7 +86,7 @@ RSpec.shared_examples_for 'a service that can create a merge request' do context 'when project has been forked', :sidekiq_might_not_need_inline do let(:forked_project) { fork_project(project, user1, repository: true) } - let(:service) { described_class.new(forked_project, user1, changes, push_options) } + let(:service) { described_class.new(project: forked_project, current_user: user1, changes: changes, push_options: push_options) } before do allow(forked_project).to receive(:empty_repo?).and_return(false) diff --git a/spec/support/shared_examples/services/metrics/dashboard_shared_examples.rb b/spec/support/shared_examples/services/metrics/dashboard_shared_examples.rb index b6c33eac7b4..4df12f7849b 100644 --- a/spec/support/shared_examples/services/metrics/dashboard_shared_examples.rb +++ b/spec/support/shared_examples/services/metrics/dashboard_shared_examples.rb @@ -12,13 +12,22 @@ RSpec.shared_examples 'misconfigured dashboard service response' do |status_code end RSpec.shared_examples 'valid dashboard service response for schema' do + file_ref_resolver = proc do |uri| + file = Rails.root.join(uri.path) + raise StandardError, "Ref file #{uri.path} must be json" unless uri.path.ends_with?('.json') + raise StandardError, "File #{file.to_path} doesn't exists" unless file.exist? + + Gitlab::Json.parse(File.read(file)) + end + it 'returns a json representation of the dashboard' do result = service_call expect(result.keys).to contain_exactly(:dashboard, :status) expect(result[:status]).to eq(:success) - expect(JSON::Validator.fully_validate(dashboard_schema, result[:dashboard])).to be_empty + validator = JSONSchemer.schema(dashboard_schema, ref_resolver: file_ref_resolver) + expect(validator.valid?(result[:dashboard].with_indifferent_access)).to be true end end diff --git a/spec/support/shared_examples/services/namespace_package_settings_shared_examples.rb b/spec/support/shared_examples/services/namespace_package_settings_shared_examples.rb index 8398dd3c453..f7a6bd3676a 100644 --- a/spec/support/shared_examples/services/namespace_package_settings_shared_examples.rb +++ b/spec/support/shared_examples/services/namespace_package_settings_shared_examples.rb @@ -7,6 +7,8 @@ RSpec.shared_examples 'updating the namespace package setting attributes' do |fr expect { subject } .to change { namespace.package_settings.reload.maven_duplicates_allowed }.from(from[:maven_duplicates_allowed]).to(to[:maven_duplicates_allowed]) .and change { namespace.package_settings.reload.maven_duplicate_exception_regex }.from(from[:maven_duplicate_exception_regex]).to(to[:maven_duplicate_exception_regex]) + .and change { namespace.package_settings.reload.generic_duplicates_allowed }.from(from[:generic_duplicates_allowed]).to(to[:generic_duplicates_allowed]) + .and change { namespace.package_settings.reload.generic_duplicate_exception_regex }.from(from[:generic_duplicate_exception_regex]).to(to[:generic_duplicate_exception_regex]) end end @@ -26,6 +28,8 @@ RSpec.shared_examples 'creating the namespace package setting' do expect(namespace.package_setting_relation.maven_duplicates_allowed).to eq(package_settings[:maven_duplicates_allowed]) expect(namespace.package_setting_relation.maven_duplicate_exception_regex).to eq(package_settings[:maven_duplicate_exception_regex]) + expect(namespace.package_setting_relation.generic_duplicates_allowed).to eq(package_settings[:generic_duplicates_allowed]) + expect(namespace.package_setting_relation.generic_duplicate_exception_regex).to eq(package_settings[:generic_duplicate_exception_regex]) end it_behaves_like 'returning a success' diff --git a/spec/support/shared_examples/services/packages_shared_examples.rb b/spec/support/shared_examples/services/packages_shared_examples.rb index 4e34c191306..72878e925dc 100644 --- a/spec/support/shared_examples/services/packages_shared_examples.rb +++ b/spec/support/shared_examples/services/packages_shared_examples.rb @@ -203,7 +203,9 @@ RSpec.shared_examples 'filters on each package_type' do |is_project: false| let_it_be(:package7) { create(:generic_package, project: project) } let_it_be(:package8) { create(:golang_package, project: project) } let_it_be(:package9) { create(:debian_package, project: project) } - let_it_be(:package9) { create(:rubygems_package, project: project) } + let_it_be(:package10) { create(:rubygems_package, project: project) } + let_it_be(:package11) { create(:helm_package, project: project) } + let_it_be(:package12) { create(:terraform_module_package, project: project) } Packages::Package.package_types.keys.each do |package_type| context "for package type #{package_type}" 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 1fb1b9f79b2..275ddebc18c 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 @@ -47,7 +47,7 @@ RSpec.shared_examples 'moves repository to another storage' do |repository_type| expect(original_repository_double).to receive(:remove) end - it "moves the project and its #{repository_type} repository to the new storage and unmarks the repository as read only" do + it "moves the project and its #{repository_type} repository to the new storage and unmarks the repository as read-only" do old_project_repository_path = Gitlab::GitalyClient::StorageSettings.allow_disk_access do project.repository.path_to_repo end diff --git a/spec/support/shared_examples/services/schedule_bulk_repository_shard_moves_shared_examples.rb b/spec/support/shared_examples/services/schedule_bulk_repository_shard_moves_shared_examples.rb index e67fc4ab04a..97304680316 100644 --- a/spec/support/shared_examples/services/schedule_bulk_repository_shard_moves_shared_examples.rb +++ b/spec/support/shared_examples/services/schedule_bulk_repository_shard_moves_shared_examples.rb @@ -27,7 +27,7 @@ RSpec.shared_examples 'moves repository shard in bulk' do container.set_repository_read_only! expect(subject).to receive(:log_info) - .with(/Container #{container.full_path} \(#{container.id}\) was skipped: #{container.class} is read only/) + .with(/Container #{container.full_path} \(#{container.id}\) was skipped: #{container.class} is read-only/) expect { subject.execute(source_storage_name, destination_storage_name) } .to change(move_service_klass, :count).by(0) end diff --git a/spec/support/shared_examples/services/security/ci_configuration/create_service_shared_examples.rb b/spec/support/shared_examples/services/security/ci_configuration/create_service_shared_examples.rb new file mode 100644 index 00000000000..538fd2bb513 --- /dev/null +++ b/spec/support/shared_examples/services/security/ci_configuration/create_service_shared_examples.rb @@ -0,0 +1,91 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.shared_examples_for 'services security ci configuration create service' do |skip_w_params| + let_it_be(:project) { create(:project, :repository) } + let_it_be(:user) { create(:user) } + + describe '#execute' do + let(:params) { {} } + + context 'user does not belong to project' do + it 'returns an error status' do + expect(result.status).to eq(:error) + expect(result.payload[:success_path]).to be_nil + end + + it 'does not track a snowplow event' do + subject + + expect_no_snowplow_event + end + end + + context 'user belongs to project' do + before do + project.add_developer(user) + end + + it 'does track the snowplow event' do + subject + + expect_snowplow_event(**snowplow_event) + end + + it 'raises exception if the user does not have permission to create a new branch' do + allow(project).to receive(:repository).and_raise(Gitlab::Git::PreReceiveError, "You are not allowed to create protected branches on this project.") + + expect { subject }.to raise_error(Gitlab::Git::PreReceiveError) + end + + context 'when exception is raised' do + let_it_be(:project) { create(:project, :repository) } + + before do + allow(project.repository).to receive(:add_branch).and_raise(StandardError, "The unexpected happened!") + end + + context 'when branch was created' do + before do + allow(project.repository).to receive(:branch_exists?).and_return(true) + end + + it 'tries to rm branch' do + expect(project.repository).to receive(:rm_branch).with(user, branch_name) + expect { subject }.to raise_error(StandardError) + end + end + + context 'when branch was not created' do + before do + allow(project.repository).to receive(:branch_exists?).and_return(false) + end + + it 'does not try to rm branch' do + expect(project.repository).not_to receive(:rm_branch) + expect { subject }.to raise_error(StandardError) + end + end + end + + context 'with no parameters' do + it 'returns the path to create a new merge request' do + expect(result.status).to eq(:success) + expect(result.payload[:success_path]).to match(/#{Gitlab::Routing.url_helpers.project_new_merge_request_url(project, {})}(.*)description(.*)source_branch/) + end + end + + unless skip_w_params + context 'with parameters' do + let(:params) { non_empty_params } + + it 'returns the path to create a new merge request' do + expect(result.status).to eq(:success) + expect(result.payload[:success_path]).to match(/#{Gitlab::Routing.url_helpers.project_new_merge_request_url(project, {})}(.*)description(.*)source_branch/) + end + end + end + end + end +end diff --git a/spec/support/shared_examples/services/updating_mentions_shared_examples.rb b/spec/support/shared_examples/services/updating_mentions_shared_examples.rb index 84f6c4d136a..13a2aa9ddac 100644 --- a/spec/support/shared_examples/services/updating_mentions_shared_examples.rb +++ b/spec/support/shared_examples/services/updating_mentions_shared_examples.rb @@ -15,7 +15,7 @@ RSpec.shared_examples 'updating mentions' do |service_class| def update_mentionable(opts) perform_enqueued_jobs do - service_class.new(project, user, opts).execute(mentionable) + service_class.new(project: project, current_user: user, params: opts).execute(mentionable) end mentionable.reload diff --git a/spec/support/stored_repositories.rb b/spec/support/stored_repositories.rb index 95f0f971787..84396c675b9 100644 --- a/spec/support/stored_repositories.rb +++ b/spec/support/stored_repositories.rb @@ -3,7 +3,7 @@ RSpec.configure do |config| config.before(:each, :broken_storage) do allow(Gitlab::GitalyClient).to receive(:call) do - raise GRPC::Unavailable.new('Gitaly broken in this spec') + raise GRPC::Unavailable, 'Gitaly broken in this spec' end end end diff --git a/spec/support/stub_languages_translation_percentage.rb b/spec/support/stub_languages_translation_percentage.rb new file mode 100644 index 00000000000..a93316288b9 --- /dev/null +++ b/spec/support/stub_languages_translation_percentage.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module StubLanguagesTranslationPercentage + # Stubs the translation percentage of the i18n languages + # - When a `blank?` list is given no stubbing is done; + # - When the list is not empty, the languages in the list + # are stubbed with the given values, any other language + # will have the translation percent set to 0; + def stub_languages_translation_percentage(list = {}) + return if list.blank? + + expect(Gitlab::I18n) + .to receive(:percentage_translated_for) + .at_least(:once) + .and_wrap_original do |_original, code| + list.with_indifferent_access[code].to_i + end + end +end |