diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-07-20 12:26:25 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-07-20 12:26:25 +0000 |
commit | a09983ae35713f5a2bbb100981116d31ce99826e (patch) | |
tree | 2ee2af7bd104d57086db360a7e6d8c9d5d43667a /spec/support | |
parent | 18c5ab32b738c0b6ecb4d0df3994000482f34bd8 (diff) | |
download | gitlab-ce-a09983ae35713f5a2bbb100981116d31ce99826e.tar.gz |
Add latest changes from gitlab-org/gitlab@13-2-stable-ee
Diffstat (limited to 'spec/support')
109 files changed, 3151 insertions, 466 deletions
diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb index 38f9ccf23f5..66fce4fddf1 100644 --- a/spec/support/capybara.rb +++ b/spec/support/capybara.rb @@ -9,6 +9,9 @@ require 'selenium-webdriver' # Give CI some extra time timeout = ENV['CI'] || ENV['CI_SERVER'] ? 60 : 30 +# Support running Capybara on a specific port to allow saving commonly used pages +Capybara.server_port = ENV['CAPYBARA_PORT'] if ENV['CAPYBARA_PORT'] + # Define an error class for JS console messages JSConsoleError = Class.new(StandardError) @@ -153,11 +156,20 @@ RSpec.configure do |config| # fixed. If we raised the `JSException` the fixed test would be marked as # failed again. if example.exception && !example.exception.is_a?(RSpec::Core::Pending::PendingExampleFixedError) - console = page.driver.browser.manage.logs.get(:browser)&.reject { |log| log.message =~ JS_CONSOLE_FILTER } - - if console.present? - message = "Unexpected browser console output:\n" + console.map(&:message).join("\n") - raise JSConsoleError, message + begin + console = page.driver.browser.manage.logs.get(:browser)&.reject { |log| log.message =~ JS_CONSOLE_FILTER } + + if console.present? + message = "Unexpected browser console output:\n" + console.map(&:message).join("\n") + raise JSConsoleError, message + end + rescue Selenium::WebDriver::Error::WebDriverError => error + if error.message =~ /unknown command: session\/[0-9a-zA-Z]+(?:\/se)?\/log/ + message = "Unable to access Chrome javascript console logs. You may be using an outdated version of ChromeDriver." + raise JSConsoleError, message + else + raise error + end end end diff --git a/spec/support/controllers/project_import_rate_limiter_shared_examples.rb b/spec/support/controllers/project_import_rate_limiter_shared_examples.rb index 9a543dd00d4..66d753a4010 100644 --- a/spec/support/controllers/project_import_rate_limiter_shared_examples.rb +++ b/spec/support/controllers/project_import_rate_limiter_shared_examples.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -shared_examples 'project import rate limiter' do +RSpec.shared_examples 'project import rate limiter' do let(:user) { create(:user) } before do diff --git a/spec/support/helpers/expect_offense.rb b/spec/support/helpers/expect_offense.rb deleted file mode 100644 index 76301fe19ff..00000000000 --- a/spec/support/helpers/expect_offense.rb +++ /dev/null @@ -1,22 +0,0 @@ -# frozen_string_literal: true - -require 'rubocop/rspec/support' - -# https://github.com/backus/rubocop-rspec/blob/master/spec/support/expect_offense.rb -# rubocop-rspec gem extension of RuboCop's ExpectOffense module. -# -# This mixin is the same as rubocop's ExpectOffense except the default -# filename ends with `_spec.rb` -module ExpectOffense - include RuboCop::RSpec::ExpectOffense - - DEFAULT_FILENAME = 'example_spec.rb'.freeze - - def expect_offense(source, filename = DEFAULT_FILENAME) - super - end - - def expect_no_offenses(source, filename = DEFAULT_FILENAME) - super - end -end diff --git a/spec/support/helpers/fast_rails_root.rb b/spec/support/helpers/fast_rails_root.rb new file mode 100644 index 00000000000..1510fe0825c --- /dev/null +++ b/spec/support/helpers/fast_rails_root.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +# For specs which don't load Rails, provide a path to Rails root +module FastRailsRoot + RAILS_ROOT = File.absolute_path("#{__dir__}/../../..") + + def rails_root_join(*args) + File.join(RAILS_ROOT, *args) + end +end diff --git a/spec/support/helpers/filtered_search_helpers.rb b/spec/support/helpers/filtered_search_helpers.rb index 99a5e043825..1847a8f8a06 100644 --- a/spec/support/helpers/filtered_search_helpers.rb +++ b/spec/support/helpers/filtered_search_helpers.rb @@ -45,9 +45,8 @@ module FilteredSearchHelpers all_count = open_count + closed_count expect(page).to have_issuable_counts(open: open_count, closed: closed_count, all: all_count) - page.within '.issues-list' do - expect(page).to have_selector('.issue', count: open_count) - end + + expect(page).to have_selector('.issue', count: open_count) end # Enables input to be added character by character diff --git a/spec/support/helpers/jira_service_helper.rb b/spec/support/helpers/jira_service_helper.rb index 198bedfe3bc..9072c41fe66 100644 --- a/spec/support/helpers/jira_service_helper.rb +++ b/spec/support/helpers/jira_service_helper.rb @@ -5,14 +5,13 @@ module JiraServiceHelper JIRA_API = JIRA_URL + "/rest/api/2" def jira_service_settings - title = "Jira tracker" url = JIRA_URL username = 'jira-user' password = 'my-secret-password' jira_issue_transition_id = '1' jira_tracker.update( - title: title, url: url, username: username, password: password, + url: url, username: username, password: password, jira_issue_transition_id: jira_issue_transition_id, active: true ) end diff --git a/spec/support/helpers/metrics_dashboard_helpers.rb b/spec/support/helpers/metrics_dashboard_helpers.rb index b8a641d5911..b2dd8ead7dd 100644 --- a/spec/support/helpers/metrics_dashboard_helpers.rb +++ b/spec/support/helpers/metrics_dashboard_helpers.rb @@ -7,6 +7,12 @@ module MetricsDashboardHelpers create(:project, :custom_repo, files: { dashboard_path => dashboard_yml }) end + def project_with_dashboard_namespace(dashboard_path, dashboard_yml = nil) + dashboard_yml ||= fixture_file('lib/gitlab/metrics/dashboard/sample_dashboard.yml') + + create(:project, :custom_repo, namespace: namespace, path: 'monitor-project', files: { dashboard_path => dashboard_yml }) + end + def delete_project_dashboard(project, user, dashboard_path) project.repository.delete_file( user, @@ -18,6 +24,14 @@ module MetricsDashboardHelpers project.repository.refresh_method_caches([:metrics_dashboard]) end + def load_sample_dashboard + load_dashboard_yaml(fixture_file('lib/gitlab/metrics/dashboard/sample_dashboard.yml')) + end + + def load_dashboard_yaml(data) + ::Gitlab::Config::Loader::Yaml.new(data).load_raw! + end + def system_dashboard_path Metrics::Dashboard::SystemDashboardService::DASHBOARD_PATH end diff --git a/spec/support/helpers/notification_helpers.rb b/spec/support/helpers/notification_helpers.rb index b3e0e7d811b..887d68de4e1 100644 --- a/spec/support/helpers/notification_helpers.rb +++ b/spec/support/helpers/notification_helpers.rb @@ -38,26 +38,26 @@ module NotificationHelpers end def expect_delivery_jobs_count(count) - expect(ActionMailer::DeliveryJob).to have_been_enqueued.exactly(count).times + expect(ActionMailer::MailDeliveryJob).to have_been_enqueued.exactly(count).times end def expect_no_delivery_jobs - expect(ActionMailer::DeliveryJob).not_to have_been_enqueued + expect(ActionMailer::MailDeliveryJob).not_to have_been_enqueued end def expect_any_delivery_jobs - expect(ActionMailer::DeliveryJob).to have_been_enqueued.at_least(:once) + expect(ActionMailer::MailDeliveryJob).to have_been_enqueued.at_least(:once) end def have_enqueued_email(*args, mailer: "Notify", mail: "", delivery: "deliver_now") - have_enqueued_job(ActionMailer::DeliveryJob).with(mailer, mail, delivery, *args) + have_enqueued_job(ActionMailer::MailDeliveryJob).with(mailer, mail, delivery, args: args) end def expect_enqueud_email(*args, mailer: "Notify", mail: "", delivery: "deliver_now") - expect(ActionMailer::DeliveryJob).to have_been_enqueued.with(mailer, mail, delivery, *args) + expect(ActionMailer::MailDeliveryJob).to have_been_enqueued.with(mailer, mail, delivery, args: args) end def expect_not_enqueud_email(*args, mailer: "Notify", mail: "") - expect(ActionMailer::DeliveryJob).not_to have_been_enqueued.with(mailer, mail, *args, any_args) + expect(ActionMailer::MailDeliveryJob).not_to have_been_enqueued.with(mailer, mail, args: any_args) end end diff --git a/spec/support/helpers/packages_manager_api_spec_helper.rb b/spec/support/helpers/packages_manager_api_spec_helper.rb new file mode 100644 index 00000000000..e5a690e1680 --- /dev/null +++ b/spec/support/helpers/packages_manager_api_spec_helper.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +module PackagesManagerApiSpecHelpers + def build_auth_headers(value) + { 'HTTP_AUTHORIZATION' => value } + end + + def build_basic_auth_header(username, password) + build_auth_headers(ActionController::HttpAuthentication::Basic.encode_credentials(username, password)) + end + + def build_token_auth_header(token) + build_auth_headers("Bearer #{token}") + end + + def build_jwt(personal_access_token, secret: jwt_secret, user_id: nil) + JSONWebToken::HMACToken.new(secret).tap do |jwt| + jwt['access_token'] = personal_access_token.id + jwt['user_id'] = user_id || personal_access_token.user_id + end + end + + def build_jwt_from_job(job, secret: jwt_secret) + JSONWebToken::HMACToken.new(secret).tap do |jwt| + jwt['access_token'] = job.token + jwt['user_id'] = job.user.id + end + end + + def build_jwt_from_deploy_token(deploy_token, secret: jwt_secret) + JSONWebToken::HMACToken.new(secret).tap do |jwt| + jwt['access_token'] = deploy_token.token + jwt['user_id'] = deploy_token.username + end + end + + def temp_file(package_tmp) + upload_path = ::Packages::PackageFileUploader.workhorse_local_upload_path + file_path = "#{upload_path}/#{package_tmp}" + + FileUtils.mkdir_p(upload_path) + File.write(file_path, 'test') + + UploadedFile.new(file_path, filename: File.basename(file_path)) + end +end diff --git a/spec/support/helpers/partitioning_helpers.rb b/spec/support/helpers/partitioning_helpers.rb index 98a13915d76..8981fea04d5 100644 --- a/spec/support/helpers/partitioning_helpers.rb +++ b/spec/support/helpers/partitioning_helpers.rb @@ -9,13 +9,36 @@ module PartitioningHelpers end def expect_range_partition_of(partition_name, table_name, min_value, max_value) - definition = find_partition_definition(partition_name) + definition = find_partition_definition(partition_name, schema: Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA) expect(definition).not_to be_nil expect(definition['base_table']).to eq(table_name.to_s) expect(definition['condition']).to eq("FOR VALUES FROM (#{min_value}) TO (#{max_value})") end + def expect_total_partitions(table_name, count, schema: Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA) + partitions = find_partitions(table_name, schema: schema) + + expect(partitions.size).to eq(count) + end + + def expect_range_partitions_for(table_name, partitions) + partitions.each do |suffix, (min_value, max_value)| + partition_name = "#{table_name}_#{suffix}" + expect_range_partition_of(partition_name, table_name, min_value, max_value) + end + + expect_total_partitions(table_name, partitions.size, schema: Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA) + end + + def expect_hash_partition_of(partition_name, table_name, modulus, remainder) + definition = find_partition_definition(partition_name, schema: Gitlab::Database::STATIC_PARTITIONS_SCHEMA) + + expect(definition).not_to be_nil + expect(definition['base_table']).to eq(table_name.to_s) + expect(definition['condition']).to eq("FOR VALUES WITH (modulus #{modulus}, remainder #{remainder})") + end + private def find_partitioned_columns(table) @@ -40,7 +63,7 @@ module PartitioningHelpers SQL end - def find_partition_definition(partition) + def find_partition_definition(partition, schema: ) connection.select_one(<<~SQL) select parent_class.relname as base_table, @@ -48,7 +71,24 @@ module PartitioningHelpers from pg_class inner join pg_inherits i on pg_class.oid = inhrelid inner join pg_class parent_class on parent_class.oid = inhparent - where pg_class.relname = '#{partition}' and pg_class.relispartition; + inner join pg_namespace ON pg_namespace.oid = pg_class.relnamespace + where pg_namespace.nspname = '#{schema}' + and pg_class.relname = '#{partition}' + and pg_class.relispartition + SQL + end + + def find_partitions(partition, schema: Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA) + connection.select_rows(<<~SQL) + select + pg_class.relname + from pg_class + inner join pg_inherits i on pg_class.oid = inhrelid + inner join pg_class parent_class on parent_class.oid = inhparent + inner join pg_namespace ON pg_namespace.oid = pg_class.relnamespace + where pg_namespace.nspname = '#{schema}' + and parent_class.relname = '#{partition}' + and pg_class.relispartition SQL end end diff --git a/spec/support/helpers/rack_attack_spec_helpers.rb b/spec/support/helpers/rack_attack_spec_helpers.rb index e0cedb5a57b..65082ec690f 100644 --- a/spec/support/helpers/rack_attack_spec_helpers.rb +++ b/spec/support/helpers/rack_attack_spec_helpers.rb @@ -30,4 +30,16 @@ module RackAttackSpecHelpers expect(response).to have_gitlab_http_status(:too_many_requests) end + + def expect_ok(&block) + yield + + expect(response).to have_gitlab_http_status(:ok) + end + + def random_next_ip + allow_next_instance_of(Rack::Attack::Request) do |instance| + allow(instance).to receive(:ip).and_return(FFaker::Internet.ip_v4_address) + end + end end diff --git a/spec/support/helpers/reference_parser_helpers.rb b/spec/support/helpers/reference_parser_helpers.rb index 9084265b587..e65cb8c96db 100644 --- a/spec/support/helpers/reference_parser_helpers.rb +++ b/spec/support/helpers/reference_parser_helpers.rb @@ -10,7 +10,7 @@ module ReferenceParserHelpers expect(result[:not_visible].count).to eq(not_visible_count) end - shared_examples 'no project N+1 queries' do + RSpec.shared_examples 'no project N+1 queries' do it 'avoids N+1 queries in #nodes_visible_to_user', :request_store do context = Banzai::RenderContext.new(project, user) @@ -28,7 +28,7 @@ module ReferenceParserHelpers end end - shared_examples 'no N+1 queries' do + RSpec.shared_examples 'no N+1 queries' do it_behaves_like 'no project N+1 queries' it 'avoids N+1 queries in #records_for_nodes', :request_store do diff --git a/spec/support/helpers/snippet_helpers.rb b/spec/support/helpers/snippet_helpers.rb new file mode 100644 index 00000000000..de64ad7d3e2 --- /dev/null +++ b/spec/support/helpers/snippet_helpers.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module SnippetHelpers + def sign_in_as(user) + sign_in(public_send(user)) if user + end + + def snippet_blob_file(blob) + { + "path" => blob.path, + "raw_url" => gitlab_raw_snippet_blob_url(blob.container, blob.path) + } + end +end diff --git a/spec/support/helpers/stub_configuration.rb b/spec/support/helpers/stub_configuration.rb index 6a832ca97d1..e19f230d8df 100644 --- a/spec/support/helpers/stub_configuration.rb +++ b/spec/support/helpers/stub_configuration.rb @@ -113,6 +113,14 @@ module StubConfiguration allow(Gitlab.config.rack_attack.git_basic_auth).to receive_messages(to_settings(messages)) end + def stub_service_desk_email_setting(messages) + allow(::Gitlab.config.service_desk_email).to receive_messages(to_settings(messages)) + end + + def stub_packages_setting(messages) + allow(::Gitlab.config.packages).to receive_messages(to_settings(messages)) + end + private # Modifies stubbed messages to also stub possible predicate versions diff --git a/spec/support/helpers/stub_object_storage.rb b/spec/support/helpers/stub_object_storage.rb index b473cdaefc1..6056359d026 100644 --- a/spec/support/helpers/stub_object_storage.rb +++ b/spec/support/helpers/stub_object_storage.rb @@ -1,6 +1,25 @@ # frozen_string_literal: true module StubObjectStorage + def stub_packages_object_storage(**params) + stub_object_storage_uploader(config: ::Gitlab.config.packages.object_store, + uploader: ::Packages::PackageFileUploader, + remote_directory: 'packages', + **params) + end + + def stub_dependency_proxy_object_storage(**params) + stub_object_storage_uploader(config: ::Gitlab.config.dependency_proxy.object_store, + uploader: ::DependencyProxy::FileUploader, + remote_directory: 'dependency_proxy', + **params) + end + + def stub_object_storage_pseudonymizer + stub_object_storage(connection_params: Pseudonymizer::Uploader.object_store_credentials, + remote_directory: Pseudonymizer::Uploader.remote_directory) + end + def stub_object_storage_uploader( config:, uploader:, @@ -73,7 +92,7 @@ module StubObjectStorage def stub_terraform_state_object_storage(uploader = described_class, **params) stub_object_storage_uploader(config: Gitlab.config.terraform_state.object_store, uploader: uploader, - remote_directory: 'terraform_state', + remote_directory: 'terraform', **params) end @@ -89,8 +108,3 @@ module StubObjectStorage EOS end end - -require_relative '../../../ee/spec/support/helpers/ee/stub_object_storage' if - Dir.exist?("#{__dir__}/../../../ee") - -StubObjectStorage.prepend_if_ee('EE::StubObjectStorage') diff --git a/spec/support/helpers/test_env.rb b/spec/support/helpers/test_env.rb index 130650b7e2e..f787aedf7aa 100644 --- a/spec/support/helpers/test_env.rb +++ b/spec/support/helpers/test_env.rb @@ -6,6 +6,8 @@ module TestEnv ComponentFailedToInstallError = Class.new(StandardError) + SHA_REGEX = /\A[0-9a-f]{5,40}\z/i.freeze + # When developing the seed repository, comment out the branch you will modify. BRANCH_SHA = { 'signed-commits' => '6101e87', @@ -29,6 +31,10 @@ module TestEnv 'gitattributes' => '5a62481', 'expand-collapse-diffs' => '4842455', 'symlink-expand-diff' => '81e6355', + 'diff-files-symlink-to-image' => '8cfca84', + 'diff-files-image-to-symlink' => '3e94fda', + 'diff-files-symlink-to-text' => '689815e', + 'diff-files-text-to-symlink' => '5e2c270', 'expand-collapse-files' => '025db92', 'expand-collapse-lines' => '238e82d', 'pages-deploy' => '7897d5b', @@ -165,8 +171,9 @@ module TestEnv task: "gitlab:gitaly:install[#{install_gitaly_args}]") do Gitlab::SetupHelper::Gitaly.create_configuration(gitaly_dir, { 'default' => repos_path }, force: true) Gitlab::SetupHelper::Praefect.create_configuration(gitaly_dir, { 'praefect' => repos_path }, force: true) - start_gitaly(gitaly_dir) end + + start_gitaly(gitaly_dir) end def gitaly_socket_path @@ -459,7 +466,6 @@ module TestEnv end def component_timed_setup(component, install_dir:, version:, task:) - puts "\n==> Setting up #{component}..." start = Time.now ensure_component_dir_name_is_correct!(component, install_dir) @@ -468,22 +474,22 @@ module TestEnv return if File.exist?(install_dir) && ci? if component_needs_update?(install_dir, version) + puts "\n==> Setting up #{component}..." # Cleanup the component entirely to ensure we start fresh FileUtils.rm_rf(install_dir) unless system('rake', task) raise ComponentFailedToInstallError end - end - yield if block_given? + yield if block_given? + puts " #{component} set up in #{Time.now - start} seconds...\n" + end rescue ComponentFailedToInstallError puts "\n#{component} failed to install, cleaning up #{install_dir}!\n" FileUtils.rm_rf(install_dir) exit 1 - ensure - puts " #{component} set up in #{Time.now - start} seconds...\n" end def ci? @@ -504,6 +510,8 @@ module TestEnv # Allow local overrides of the component for tests during development return false if Rails.env.test? && File.symlink?(component_folder) + return false if component_matches_git_sha?(component_folder, expected_version) + version = File.read(File.join(component_folder, 'VERSION')).strip # Notice that this will always yield true when using branch versions @@ -513,6 +521,16 @@ module TestEnv rescue Errno::ENOENT true end + + def component_matches_git_sha?(component_folder, expected_version) + # Not a git SHA, so return early + return false unless expected_version =~ SHA_REGEX + + sha, exit_status = Gitlab::Popen.popen(%W(#{Gitlab.config.git.bin_path} rev-parse HEAD), component_folder) + return false if exit_status != 0 + + expected_version == sha.chomp + end end require_relative('../../../ee/spec/support/helpers/ee/test_env') if Gitlab.ee? diff --git a/spec/support/helpers/trigger_helpers.rb b/spec/support/helpers/trigger_helpers.rb index fa4f499b900..67c62cf4869 100644 --- a/spec/support/helpers/trigger_helpers.rb +++ b/spec/support/helpers/trigger_helpers.rb @@ -27,7 +27,10 @@ module TriggerHelpers expected_timing, expected_events = fires_on.first expect(timing).to eq(expected_timing.to_s) expect(events).to match_array(Array.wrap(expected_events)) - expect(definition).to eq("execute procedure #{fn_name}()") + + # TODO: Update CREATE TRIGGER syntax to use EXECUTE FUNCTION + # https://gitlab.com/gitlab-org/gitlab/-/issues/227089 + expect(definition).to match(%r{execute (?:procedure|function) #{fn_name}()}) end private diff --git a/spec/support/helpers/usage_data_helpers.rb b/spec/support/helpers/usage_data_helpers.rb index f6c415a75bc..a4f40a4af0a 100644 --- a/spec/support/helpers/usage_data_helpers.rb +++ b/spec/support/helpers/usage_data_helpers.rb @@ -78,7 +78,6 @@ module UsageDataHelpers labels lfs_objects merge_requests - merge_requests_users milestone_lists milestones notes @@ -89,8 +88,6 @@ module UsageDataHelpers projects_jira_active projects_jira_server_active projects_jira_cloud_active - projects_slack_notifications_active - projects_slack_slash_active projects_slack_active projects_slack_slash_commands_active projects_custom_issue_tracker_active @@ -231,6 +228,15 @@ module UsageDataHelpers def allow_prometheus_queries allow_next_instance_of(Gitlab::PrometheusClient) do |client| allow(client).to receive(:aggregate).and_return({}) + allow(client).to receive(:query).and_return({}) + end + end + + def for_defined_days_back(days: [29, 2]) + days.each do |n| + Timecop.travel(n.days.ago) do + yield + end end end end diff --git a/spec/support/matchers/background_migrations_matchers.rb b/spec/support/matchers/background_migrations_matchers.rb index 8735dac8b2a..0144a044f6c 100644 --- a/spec/support/matchers/background_migrations_matchers.rb +++ b/spec/support/matchers/background_migrations_matchers.rb @@ -10,7 +10,8 @@ RSpec::Matchers.define :be_scheduled_delayed_migration do |delay, *expected| failure_message do |migration| "Migration `#{migration}` with args `#{expected.inspect}` " \ - 'not scheduled in expected time!' + "not scheduled in expected time! Expected any of `#{BackgroundMigrationWorker.jobs.map { |j| j['args'] }}` to be `#{[migration, expected]}` " \ + "and any of `#{BackgroundMigrationWorker.jobs.map { |j| j['at'].to_i }}` to be `#{delay.to_i + Time.now.to_i}` (`#{delay.to_i}` + `#{Time.now.to_i}`)." end end diff --git a/spec/support/matchers/jsonb_matchers.rb b/spec/support/matchers/jsonb_matchers.rb new file mode 100644 index 00000000000..823888708f3 --- /dev/null +++ b/spec/support/matchers/jsonb_matchers.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +RSpec::Matchers.define :validate_jsonb_schema do |jsonb_columns| + match do |actual| + next true if jsonb_columns.blank? + + expect(actual.validators).to include(a_kind_of(JsonSchemaValidator)) + end + + failure_message do + <<~FAILURE_MESSAGE + Expected #{actual.name} to validate the schema of #{jsonb_columns.join(', ')}. + + Use JsonSchemaValidator in your model when using a jsonb column. + See doc/development/migration_style_guide.html#storing-json-in-database for more information. + + To fix this, please add `validates :#{jsonb_columns.first}, json_schema: { filename: "filename" }` in your model file, for example: + + class #{actual.name} + validates :#{jsonb_columns.first}, json_schema: { filename: "filename" } + end + FAILURE_MESSAGE + end +end diff --git a/spec/support/rspec.rb b/spec/support/rspec.rb index 7d011c5eb95..861b57c9efa 100644 --- a/spec/support/rspec.rb +++ b/spec/support/rspec.rb @@ -4,7 +4,8 @@ require_relative "helpers/stub_configuration" require_relative "helpers/stub_metrics" require_relative "helpers/stub_object_storage" require_relative "helpers/stub_env" -require_relative "helpers/expect_offense" +require_relative "helpers/fast_rails_root" +require 'rubocop/rspec/support' RSpec.configure do |config| config.mock_with :rspec @@ -14,6 +15,7 @@ RSpec.configure do |config| config.include StubMetrics config.include StubObjectStorage config.include StubENV + config.include FastRailsRoot - config.include ExpectOffense, type: :rubocop + config.include RuboCop::RSpec::ExpectOffense, type: :rubocop end diff --git a/spec/support/services/clusters/create_service_shared.rb b/spec/support/services/clusters/create_service_shared.rb index 31aee08baec..f8a58a828ce 100644 --- a/spec/support/services/clusters/create_service_shared.rb +++ b/spec/support/services/clusters/create_service_shared.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true RSpec.shared_context 'valid cluster create params' do + let(:clusterable) { Clusters::Instance.new } let(:params) do { name: 'test-cluster', @@ -11,12 +12,14 @@ RSpec.shared_context 'valid cluster create params' do num_nodes: 1, machine_type: 'machine_type-a', legacy_abac: 'true' - } + }, + clusterable: clusterable } end end RSpec.shared_context 'invalid cluster create params' do + let(:clusterable) { Clusters::Instance.new } let(:params) do { name: 'test-cluster', @@ -26,7 +29,9 @@ RSpec.shared_context 'invalid cluster create params' do zone: 'us-central1-a', num_nodes: 1, machine_type: 'machine_type-a' - } + }, + clusterable: clusterable + } end end diff --git a/spec/support/services/issuable_create_service_slash_commands_shared_examples.rb b/spec/support/services/issuable_description_quick_actions_shared_examples.rb index 3d45fe06134..1970301e4c9 100644 --- a/spec/support/services/issuable_create_service_slash_commands_shared_examples.rb +++ b/spec/support/services/issuable_description_quick_actions_shared_examples.rb @@ -3,20 +3,25 @@ # Specifications for behavior common to all objects with executable attributes. # It can take a `default_params`. -RSpec.shared_examples 'new issuable record that supports quick actions' do - let!(:project) { create(:project, :repository) } - let(:user) { create(:user).tap { |u| project.add_maintainer(u) } } - let(:assignee) { create(:user) } - let!(:milestone) { create(:milestone, project: project) } - let!(:labels) { create_list(:label, 3, project: project) } +RSpec.shared_examples 'issuable record that supports quick actions' do + let_it_be(:project) { create(:project, :repository) } + let_it_be(:user) { create(:user) } + let_it_be(:assignee) { create(:user) } + let_it_be(:milestone) { create(:milestone, project: project) } + let_it_be(:labels) { create_list(:label, 3, project: project) } + let(:base_params) { { title: 'My issuable title' } } let(:params) { base_params.merge(defined?(default_params) ? default_params : {}).merge(example_params) } - let(:issuable) { described_class.new(project, user, params).execute } - before do + before_all do + project.add_maintainer(user) project.add_maintainer(assignee) end + before do + issuable.reload + end + context 'with labels in command only' do let(:example_params) do { @@ -25,7 +30,6 @@ RSpec.shared_examples 'new issuable record that supports quick actions' do end it 'attaches labels to issuable' do - expect(issuable).to be_persisted expect(issuable.label_ids).to match_array([labels.first.id, labels.second.id]) end end @@ -39,7 +43,6 @@ RSpec.shared_examples 'new issuable record that supports quick actions' do end it 'attaches all labels to issuable' do - expect(issuable).to be_persisted expect(issuable.label_ids).to match_array([labels.first.id, labels.second.id]) end end @@ -52,22 +55,8 @@ RSpec.shared_examples 'new issuable record that supports quick actions' do end it 'assigns and sets milestone to issuable' do - expect(issuable).to be_persisted expect(issuable.assignees).to eq([assignee]) expect(issuable.milestone).to eq(milestone) end end - - describe '/close' do - let(:example_params) do - { - description: '/close' - } - end - - it 'returns an open issue' do - expect(issuable).to be_persisted - expect(issuable).to be_open - end - end end diff --git a/spec/support/shared_contexts/cache_allowed_users_in_namespace_shared_context.rb b/spec/support/shared_contexts/cache_allowed_users_in_namespace_shared_context.rb index 04f49e94647..e3c1d0afa53 100644 --- a/spec/support/shared_contexts/cache_allowed_users_in_namespace_shared_context.rb +++ b/spec/support/shared_contexts/cache_allowed_users_in_namespace_shared_context.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -shared_examples 'allowed user IDs are cached' do +RSpec.shared_examples 'allowed user IDs are cached' do it 'caches the allowed user IDs in cache', :use_clean_rails_memory_store_caching do expect do expect(described_class.l1_cache_backend).to receive(:fetch).and_call_original diff --git a/spec/support/shared_contexts/design_management_shared_contexts.rb b/spec/support/shared_contexts/design_management_shared_contexts.rb index 2866effb3a8..3ff6a521338 100644 --- a/spec/support/shared_contexts/design_management_shared_contexts.rb +++ b/spec/support/shared_contexts/design_management_shared_contexts.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -shared_context 'four designs in three versions' do +RSpec.shared_context 'four designs in three versions' do include DesignManagementTestHelpers let_it_be(:issue) { create(:issue) } diff --git a/spec/support/shared_contexts/features/error_tracking_shared_context.rb b/spec/support/shared_contexts/features/error_tracking_shared_context.rb index 102cf7c9b11..1f4eb3a6df9 100644 --- a/spec/support/shared_contexts/features/error_tracking_shared_context.rb +++ b/spec/support/shared_contexts/features/error_tracking_shared_context.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -shared_context 'sentry error tracking context feature' do +RSpec.shared_context 'sentry error tracking context feature' do include ReactiveCachingHelpers let_it_be(:project) { create(:project) } diff --git a/spec/support/shared_contexts/finders/merge_requests_finder_shared_contexts.rb b/spec/support/shared_contexts/finders/merge_requests_finder_shared_contexts.rb index 2b8daa80ab4..07b6b98222f 100644 --- a/spec/support/shared_contexts/finders/merge_requests_finder_shared_contexts.rb +++ b/spec/support/shared_contexts/finders/merge_requests_finder_shared_contexts.rb @@ -23,12 +23,12 @@ RSpec.shared_context 'MergeRequestsFinder multiple projects with merge requests # We cannot use `let_it_be` here otherwise we get: # Failure/Error: allow(RepositoryForkWorker).to receive(:perform_async).and_return(true) # The use of doubles or partial doubles from rspec-mocks outside of the per-test lifecycle is not supported. - let(:project2) do + let!(:project2) do allow_gitaly_n_plus_1 do fork_project(project1, user) end end - let(:project3) do + let!(:project3) do allow_gitaly_n_plus_1 do fork_project(project1, user).tap do |project| project.update!(archived: true) @@ -45,6 +45,9 @@ RSpec.shared_context 'MergeRequestsFinder multiple projects with merge requests allow_gitaly_n_plus_1 { create(:project, group: subgroup) } end + let!(:label) { create(:label, project: project1) } + let!(:label2) { create(:label, project: project1) } + let!(:merge_request1) do create(:merge_request, assignees: [user], author: user, source_project: project2, target_project: project1, @@ -72,6 +75,9 @@ RSpec.shared_context 'MergeRequestsFinder multiple projects with merge requests title: '[WIP]') end + let!(:label_link) { create(:label_link, label: label, target: merge_request2) } + let!(:label_link2) { create(:label_link, label: label2, target: merge_request3) } + before do project1.add_maintainer(user) project2.add_developer(user) diff --git a/spec/support/shared_contexts/issuable/merge_request_shared_context.rb b/spec/support/shared_contexts/issuable/merge_request_shared_context.rb index 05ffb934c34..79fc42e73c7 100644 --- a/spec/support/shared_contexts/issuable/merge_request_shared_context.rb +++ b/spec/support/shared_contexts/issuable/merge_request_shared_context.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -shared_context 'merge request show action' do +RSpec.shared_context 'merge request show action' do include Devise::Test::ControllerHelpers include ProjectForksHelper diff --git a/spec/support/shared_contexts/issuable/project_shared_context.rb b/spec/support/shared_contexts/issuable/project_shared_context.rb index 6dbf3154977..5e5f6f2b7a6 100644 --- a/spec/support/shared_contexts/issuable/project_shared_context.rb +++ b/spec/support/shared_contexts/issuable/project_shared_context.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -shared_context 'project show action' do +RSpec.shared_context 'project show action' do let(:project) { create(:project, :repository) } let(:issue) { create(:issue, project: project, author: user) } let(:user) { create(:user) } diff --git a/spec/support/shared_contexts/navbar_structure_context.rb b/spec/support/shared_contexts/navbar_structure_context.rb index 79b5ff44d4f..d9a72f2b54a 100644 --- a/spec/support/shared_contexts/navbar_structure_context.rb +++ b/spec/support/shared_contexts/navbar_structure_context.rb @@ -42,6 +42,7 @@ RSpec.shared_context 'project navbar structure' do _('List'), _('Boards'), _('Labels'), + _('Service Desk'), _('Milestones') ] }, diff --git a/spec/support/shared_contexts/policies/group_policy_shared_context.rb b/spec/support/shared_contexts/policies/group_policy_shared_context.rb index a0d54666dff..4b0c7afab6d 100644 --- a/spec/support/shared_contexts/policies/group_policy_shared_context.rb +++ b/spec/support/shared_contexts/policies/group_policy_shared_context.rb @@ -18,7 +18,7 @@ RSpec.shared_context 'GroupPolicy context' do ] end let(:read_group_permissions) { %i[read_label read_list read_milestone read_board] } - let(:reporter_permissions) { %i[admin_label read_container_image read_metrics_dashboard_annotation] } + let(:reporter_permissions) { %i[admin_label read_container_image read_metrics_dashboard_annotation read_prometheus] } let(:developer_permissions) { %i[admin_milestone create_metrics_dashboard_annotation delete_metrics_dashboard_annotation update_metrics_dashboard_annotation] } let(:maintainer_permissions) do %i[ @@ -38,6 +38,7 @@ RSpec.shared_context 'GroupPolicy context' do :update_default_branch_protection ].compact end + let(:admin_permissions) { %i[read_confidential_issues] } before_all do group.add_guest(guest) diff --git a/spec/support/shared_contexts/presenters/nuget_shared_context.rb b/spec/support/shared_contexts/presenters/nuget_shared_context.rb new file mode 100644 index 00000000000..dd381db5a8b --- /dev/null +++ b/spec/support/shared_contexts/presenters/nuget_shared_context.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +RSpec.shared_context 'with expected presenters dependency groups' do + def expected_dependency_groups(project_id, package_name, package_version) + [ + { + id: "http://localhost/api/v4/projects/#{project_id}/packages/nuget/metadata/#{package_name}/#{package_version}.json#dependencyGroup/.netstandard2.0", + target_framework: '.NETStandard2.0', + type: 'PackageDependencyGroup', + dependencies: [ + { + id: "http://localhost/api/v4/projects/#{project_id}/packages/nuget/metadata/#{package_name}/#{package_version}.json#dependencyGroup/.netstandard2.0/newtonsoft.json", + range: '12.0.3', + name: 'Newtonsoft.Json', + type: 'PackageDependency' + } + ] + }, + { + id: "http://localhost/api/v4/projects/#{project_id}/packages/nuget/metadata/#{package_name}/#{package_version}.json#dependencyGroup", + type: 'PackageDependencyGroup', + dependencies: [ + { + id: "http://localhost/api/v4/projects/#{project_id}/packages/nuget/metadata/#{package_name}/#{package_version}.json#dependencyGroup/castle.core", + range: '4.4.1', + name: 'Castle.Core', + type: 'PackageDependency' + } + ] + } + ] + end + + def create_dependencies_for(package) + dependency1 = Packages::Dependency.find_by(name: 'Newtonsoft.Json', version_pattern: '12.0.3') || create(:packages_dependency, name: 'Newtonsoft.Json', version_pattern: '12.0.3') + dependency2 = Packages::Dependency.find_by(name: 'Castle.Core', version_pattern: '4.4.1') || create(:packages_dependency, name: 'Castle.Core', version_pattern: '4.4.1') + + create(:packages_dependency_link, :with_nuget_metadatum, package: package, dependency: dependency1) + create(:packages_dependency_link, package: package, dependency: dependency2) + end +end diff --git a/spec/support/shared_contexts/project_service_jira_context.rb b/spec/support/shared_contexts/project_service_jira_context.rb new file mode 100644 index 00000000000..4ca5c99323a --- /dev/null +++ b/spec/support/shared_contexts/project_service_jira_context.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +RSpec.shared_context 'project service Jira context' do + let(:url) { 'http://jira.example.com' } + let(:test_url) { 'http://jira.example.com/rest/api/2/serverInfo' } + + def fill_form(disable: false) + click_active_toggle if disable + + fill_in 'service_url', with: url + fill_in 'service_username', with: 'username' + fill_in 'service_password', with: 'password' + fill_in 'service_jira_issue_transition_id', with: '25' + end +end diff --git a/spec/support/shared_contexts/project_service_shared_context.rb b/spec/support/shared_contexts/project_service_shared_context.rb index 5b0dd26bd7b..a72d4901b72 100644 --- a/spec/support/shared_contexts/project_service_shared_context.rb +++ b/spec/support/shared_contexts/project_service_shared_context.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -shared_context 'project service activation' do +RSpec.shared_context 'project service activation' do let(:project) { create(:project) } let(:user) { create(:user) } diff --git a/spec/support/shared_contexts/prometheus/alert_shared_context.rb b/spec/support/shared_contexts/prometheus/alert_shared_context.rb new file mode 100644 index 00000000000..330d2c4515f --- /dev/null +++ b/spec/support/shared_contexts/prometheus/alert_shared_context.rb @@ -0,0 +1,76 @@ +# frozen_string_literal: true + +# These contexts expect a `project` to be defined. +# It is expected that these contexts are used to create an +# alert. +RSpec.shared_context 'self-managed prometheus alert attributes' do + let_it_be(:environment) { create(:environment, project: project, name: 'production') } + + let(:starts_at) { '2018-03-12T09:06:00Z' } + let(:title) { 'title' } + let(:y_label) { 'y_label' } + let(:query) { 'avg(metric) > 1.0' } + + let(:embed_content) do + { + panel_groups: [{ + panels: [{ + type: 'line-graph', + title: title, + y_label: y_label, + metrics: [{ query_range: query }] + }] + }] + }.to_json + end + + let(:payload) do + { + 'startsAt' => starts_at, + 'generatorURL' => "http://host?g0.expr=#{CGI.escape(query)}", + 'labels' => { + 'gitlab_environment_name' => 'production' + }, + 'annotations' => { + 'title' => title, + 'gitlab_y_label' => y_label + } + } + end + + let(:dashboard_url_for_alert) do + Gitlab::Routing.url_helpers.metrics_dashboard_project_environment_url( + project, + environment, + embed_json: embed_content, + embedded: true, + end: '2018-03-12T09:36:00Z', + start: '2018-03-12T08:36:00Z' + ) + end +end + +RSpec.shared_context 'gitlab-managed prometheus alert attributes' do + let_it_be(:prometheus_alert) { create(:prometheus_alert, project: project) } + let(:prometheus_metric_id) { prometheus_alert.prometheus_metric_id } + + let(:payload) do + { + 'startsAt' => '2018-03-12T09:06:00Z', + 'labels' => { + 'gitlab_alert_id' => prometheus_metric_id + } + } + end + + let(:dashboard_url_for_alert) do + Gitlab::Routing.url_helpers.metrics_dashboard_project_prometheus_alert_url( + project, + prometheus_metric_id, + environment_id: prometheus_alert.environment_id, + embedded: true, + end: '2018-03-12T09:36:00Z', + start: '2018-03-12T08:36:00Z' + ) + end +end diff --git a/spec/support/shared_contexts/requests/api/graphql/jira_import/jira_projects_context.rb b/spec/support/shared_contexts/requests/api/graphql/jira_import/jira_projects_context.rb index f0722beb3ed..7f150bed43d 100644 --- a/spec/support/shared_contexts/requests/api/graphql/jira_import/jira_projects_context.rb +++ b/spec/support/shared_contexts/requests/api/graphql/jira_import/jira_projects_context.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -shared_context 'jira projects request context' do +RSpec.shared_context 'jira projects request context' do let(:url) { 'https://jira.example.com' } let(:username) { 'jira-username' } let(:password) { 'jira-password' } @@ -74,6 +74,48 @@ shared_context 'jira projects request context' do }' end + let_it_be(:all_jira_projects_json) do + '[{ + "expand": "description,lead,issueTypes,url,projectKeys,permissions,insight", + "self": "https://gitlab-jira.atlassian.net/rest/api/2/project/10000", + "id": "10000", + "key": "EX", + "name": "Example", + "avatarUrls": { + "48x48": "https://gitlab-jira.atlassian.net/secure/projectavatar?pid=10000&avatarId=10425", + "24x24": "https://gitlab-jira.atlassian.net/secure/projectavatar?size=small&s=small&pid=10000&avatarId=10425", + "16x16": "https://gitlab-jira.atlassian.net/secure/projectavatar?size=xsmall&s=xsmall&pid=10000&avatarId=10425", + "32x32": "https://gitlab-jira.atlassian.net/secure/projectavatar?size=medium&s=medium&pid=10000&avatarId=10425" + }, + "projectTypeKey": "software", + "simplified": false, + "style": "classic", + "isPrivate": false, + "properties": { + } + }, + { + "expand": "description,lead,issueTypes,url,projectKeys,permissions,insight", + "self": "https://gitlab-jira.atlassian.net/rest/api/2/project/10001", + "id": "10001", + "key": "ABC", + "name": "Alphabetical", + "avatarUrls": { + "48x48": "https://gitlab-jira.atlassian.net/secure/projectavatar?pid=10001&avatarId=10405", + "24x24": "https://gitlab-jira.atlassian.net/secure/projectavatar?size=small&s=small&pid=10001&avatarId=10405", + "16x16": "https://gitlab-jira.atlassian.net/secure/projectavatar?size=xsmall&s=xsmall&pid=10001&avatarId=10405", + "32x32": "https://gitlab-jira.atlassian.net/secure/projectavatar?size=medium&s=medium&pid=10001&avatarId=10405" + }, + "projectTypeKey": "software", + "simplified": true, + "style": "next-gen", + "isPrivate": false, + "properties": { + }, + "entityId": "14935009-f8aa-481e-94bc-f7251f320b0e", + "uuid": "14935009-f8aa-481e-94bc-f7251f320b0e" + }]' + end let_it_be(:empty_jira_projects_json) do '{ "self": "https://your-domain.atlassian.net/rest/api/2/project/search?startAt=0&maxResults=2", @@ -86,10 +128,32 @@ shared_context 'jira projects request context' do }' end + let(:server_info_json) do + '{ + "baseUrl": "https://gitlab-jira.atlassian.net", + "version": "1001.0.0-SNAPSHOT", + "versionNumbers": [ + 1001, + 0, + 0 + ], + "deploymentType": "Cloud", + "buildNumber": 100128, + "buildDate": "2020-06-03T01:58:44.000-0700", + "serverTime": "2020-06-04T06:15:13.686-0700", + "scmInfo": "e736ab140ddb281c7cf5dcf9062c9ce2c08b3c1c", + "serverTitle": "Jira", + "defaultLocale": { + "locale": "en_US" + } + }' + end + let(:test_url) { "#{url}/rest/api/2/project/search?maxResults=50&query=&startAt=0" } let(:start_at_20_url) { "#{url}/rest/api/2/project/search?maxResults=50&query=&startAt=20" } let(:start_at_1_url) { "#{url}/rest/api/2/project/search?maxResults=50&query=&startAt=1" } let(:max_results_1_url) { "#{url}/rest/api/2/project/search?maxResults=1&query=&startAt=0" } + let(:all_projects_url) { "#{url}/rest/api/2/project" } before do WebMock.stub_request(:get, test_url).with(basic_auth: [username, password]) @@ -100,5 +164,9 @@ shared_context 'jira projects request context' do .to_return(body: jira_projects_json, headers: { "Content-Type": "application/json" }) WebMock.stub_request(:get, max_results_1_url).with(basic_auth: [username, password]) .to_return(body: jira_projects_json, headers: { "Content-Type": "application/json" }) + WebMock.stub_request(:get, all_projects_url).with(basic_auth: [username, password]) + .to_return(body: all_jira_projects_json, headers: { "Content-Type": "application/json" }) + WebMock.stub_request(:get, 'https://jira.example.com/rest/api/2/serverInfo') + .to_return(status: 200, body: server_info_json, headers: {}) end end diff --git a/spec/support/shared_contexts/sentry_error_tracking_shared_context.rb b/spec/support/shared_contexts/sentry_error_tracking_shared_context.rb index f06de53f0c1..3453f954c9d 100644 --- a/spec/support/shared_contexts/sentry_error_tracking_shared_context.rb +++ b/spec/support/shared_contexts/sentry_error_tracking_shared_context.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -shared_context 'sentry error tracking context' do +RSpec.shared_context 'sentry error tracking context' do let_it_be(:user) { create(:user) } let_it_be(:project) { create(:project) } diff --git a/spec/support/shared_contexts/services_shared_context.rb b/spec/support/shared_contexts/services_shared_context.rb index bf4eac8f324..899b43ade01 100644 --- a/spec/support/shared_contexts/services_shared_context.rb +++ b/spec/support/shared_contexts/services_shared_context.rb @@ -12,6 +12,8 @@ Service.available_services_names.each do |service| service_attrs_list.inject({}) do |hash, k| if k =~ /^(token*|.*_token|.*_key)/ hash.merge!(k => 'secrettoken') + elsif service == 'confluence' && k == :confluence_url + hash.merge!(k => 'https://example.atlassian.net/wiki') elsif k =~ /^(.*_url|url|webhook)/ hash.merge!(k => "http://example.com") elsif service_klass.method_defined?("#{k}?") diff --git a/spec/support/shared_contexts/spam_constants.rb b/spec/support/shared_contexts/spam_constants.rb index 32371f4b92f..813f9d00123 100644 --- a/spec/support/shared_contexts/spam_constants.rb +++ b/spec/support/shared_contexts/spam_constants.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -shared_context 'includes Spam constants' do +RSpec.shared_context 'includes Spam constants' do before do stub_const('CONDITIONAL_ALLOW', Spam::SpamConstants::CONDITIONAL_ALLOW) stub_const('DISALLOW', Spam::SpamConstants::DISALLOW) diff --git a/spec/support/shared_examples/controllers/import_controller_new_import_ui_shared_examples.rb b/spec/support/shared_examples/controllers/import_controller_new_import_ui_shared_examples.rb deleted file mode 100644 index 88ad1f6cde2..00000000000 --- a/spec/support/shared_examples/controllers/import_controller_new_import_ui_shared_examples.rb +++ /dev/null @@ -1,36 +0,0 @@ -# frozen_string_literal: true - -RSpec.shared_examples 'import controller with new_import_ui feature flag' do - include ImportSpecHelper - - context 'with new_import_ui feature flag enabled' do - let(:group) { create(:group) } - - before do - stub_feature_flags(new_import_ui: true) - group.add_owner(user) - end - - it "returns variables for json request" do - project = create(:project, import_type: provider_name, creator_id: user.id) - stub_client(client_repos_field => [repo]) - - get :status, format: :json - - expect(response).to have_gitlab_http_status(:ok) - expect(json_response.dig("imported_projects", 0, "id")).to eq(project.id) - expect(json_response.dig("provider_repos", 0, "id")).to eq(repo_id) - expect(json_response.dig("namespaces", 0, "id")).to eq(group.id) - end - - it "does not show already added project" do - project = create(:project, import_type: provider_name, namespace: user.namespace, import_status: :finished, import_source: import_source) - stub_client(client_repos_field => [repo]) - - get :status, format: :json - - expect(json_response.dig("imported_projects", 0, "id")).to eq(project.id) - expect(json_response.dig("provider_repos")).to eq([]) - end - end -end diff --git a/spec/support/shared_examples/controllers/import_controller_status_shared_examples.rb b/spec/support/shared_examples/controllers/import_controller_status_shared_examples.rb new file mode 100644 index 00000000000..ecb9abc5c46 --- /dev/null +++ b/spec/support/shared_examples/controllers/import_controller_status_shared_examples.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'import controller status' do + include ImportSpecHelper + + let(:group) { create(:group) } + + before do + group.add_owner(user) + end + + it "returns variables for json request" do + project = create(:project, import_type: provider_name, creator_id: user.id) + stub_client(client_repos_field => [repo]) + + get :status, format: :json + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response.dig("imported_projects", 0, "id")).to eq(project.id) + expect(json_response.dig("provider_repos", 0, "id")).to eq(repo_id) + expect(json_response.dig("namespaces", 0, "id")).to eq(group.id) + end + + it "does not show already added project" do + project = create(:project, import_type: provider_name, namespace: user.namespace, import_status: :finished, import_source: import_source) + stub_client(client_repos_field => [repo]) + + get :status, format: :json + + expect(json_response.dig("imported_projects", 0, "id")).to eq(project.id) + expect(json_response.dig("provider_repos")).to eq([]) + end +end diff --git a/spec/support/shared_examples/controllers/known_sign_in_shared_examples.rb b/spec/support/shared_examples/controllers/known_sign_in_shared_examples.rb index 60abb76acec..7f26155f9d6 100644 --- a/spec/support/shared_examples/controllers/known_sign_in_shared_examples.rb +++ b/spec/support/shared_examples/controllers/known_sign_in_shared_examples.rb @@ -9,13 +9,38 @@ RSpec.shared_examples 'known sign in' do user.update!(current_sign_in_ip: ip) end - context 'with a valid post' do - context 'when remote IP does not match user last sign in IP' do - before do - stub_user_ip('127.0.0.1') - stub_remote_ip('169.0.0.1') - end + def stub_cookie(value = user.id) + cookies.encrypted[KnownSignIn::KNOWN_SIGN_IN_COOKIE] = { + value: value, expires: KnownSignIn::KNOWN_SIGN_IN_COOKIE_EXPIRY + } + end + + context 'when the remote IP and the last sign in IP match' do + before do + stub_user_ip('169.0.0.1') + stub_remote_ip('169.0.0.1') + end + + it 'does not notify the user' do + expect(NotificationService).not_to receive(:new) + post_action + end + + it 'sets/updates the encrypted cookie' do + post_action + + expect(cookies.encrypted[KnownSignIn::KNOWN_SIGN_IN_COOKIE]).to eq(user.id) + end + end + + context 'when the remote IP and the last sign in IP do not match' do + before do + stub_user_ip('127.0.0.1') + stub_remote_ip('169.0.0.1') + end + + context 'when the cookie is not previously set' do it 'notifies the user' do expect_next_instance_of(NotificationService) do |instance| expect(instance).to receive(:unknown_sign_in) @@ -23,37 +48,68 @@ RSpec.shared_examples 'known sign in' do post_action end - end - - context 'when remote IP matches an active session' do - before do - existing_sessions = ActiveSession.session_ids_for_user(user.id) - existing_sessions.each { |sessions| ActiveSession.destroy(user, sessions) } - stub_user_ip('169.0.0.1') - stub_remote_ip('127.0.0.1') + it 'sets the encrypted cookie' do + post_action - ActiveSession.set(user, request) + expect(cookies.encrypted[KnownSignIn::KNOWN_SIGN_IN_COOKIE]).to eq(user.id) end + end - it 'does not notify the user' do - expect_any_instance_of(NotificationService).not_to receive(:unknown_sign_in) + it 'notifies the user when the cookie is expired' do + stub_cookie + + Timecop.freeze((KnownSignIn::KNOWN_SIGN_IN_COOKIE_EXPIRY + 1.day).from_now) do + expect_next_instance_of(NotificationService) do |instance| + expect(instance).to receive(:unknown_sign_in) + end post_action end end - context 'when remote IP address matches last sign in IP' do + context 'when notify_on_unknown_sign_in global setting is false' do before do - stub_user_ip('127.0.0.1') - stub_remote_ip('127.0.0.1') + stub_application_setting(notify_on_unknown_sign_in: false) end it 'does not notify the user' do - expect_any_instance_of(NotificationService).not_to receive(:unknown_sign_in) + expect(NotificationService).not_to receive(:new) + + post_action + end + it 'does not set a cookie' do post_action + + expect(cookies.encrypted[KnownSignIn::KNOWN_SIGN_IN_COOKIE]).to be_nil + end + end + + it 'notifies the user when the cookie is for another user' do + stub_cookie(create(:user).id) + + expect_next_instance_of(NotificationService) do |instance| + expect(instance).to receive(:unknown_sign_in) end + + post_action + end + + it 'does not notify the user when remote IP matches an active session' do + ActiveSession.set(user, request) + + expect(NotificationService).not_to receive(:new) + + post_action + end + + it 'does not notify the user when the cookie is present and not expired' do + stub_cookie + + expect(NotificationService).not_to receive(:new) + + post_action end end end diff --git a/spec/support/shared_examples/controllers/metrics/dashboard/prometheus_api_proxy_shared_examples.rb b/spec/support/shared_examples/controllers/metrics/dashboard/prometheus_api_proxy_shared_examples.rb new file mode 100644 index 00000000000..94cd6971f7c --- /dev/null +++ b/spec/support/shared_examples/controllers/metrics/dashboard/prometheus_api_proxy_shared_examples.rb @@ -0,0 +1,147 @@ +# frozen_string_literal: true + +RSpec.shared_examples_for 'metrics dashboard prometheus api proxy' do + let(:service_params) { [proxyable, 'GET', 'query', expected_params] } + let(:service_result) { { status: :success, body: prometheus_body } } + let(:prometheus_proxy_service) { instance_double(Prometheus::ProxyService) } + let(:proxyable_params) do + { + id: proxyable.id.to_s + } + end + let(:expected_params) do + ActionController::Parameters.new( + prometheus_proxy_params( + proxy_path: 'query', + controller: described_class.controller_path, + action: 'prometheus_proxy' + ) + ).permit! + end + + before do + allow_next_instance_of(Prometheus::ProxyService, *service_params) do |proxy_service| + allow(proxy_service).to receive(:execute).and_return(service_result) + end + end + + context 'with valid requests' do + context 'with success result' do + let(:prometheus_body) { '{"status":"success"}' } + let(:prometheus_json_body) { Gitlab::Json.parse(prometheus_body) } + + it 'returns prometheus response' do + get :prometheus_proxy, params: prometheus_proxy_params + + expect(Prometheus::ProxyService).to have_received(:new).with(*service_params) + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to eq(prometheus_json_body) + end + + context 'with nil query' do + let(:params_without_query) do + prometheus_proxy_params.except(:query) + end + + before do + expected_params.delete(:query) + end + + it 'does not raise error' do + get :prometheus_proxy, params: params_without_query + + expect(Prometheus::ProxyService).to have_received(:new).with(*service_params) + end + end + end + + context 'with nil result' do + let(:service_result) { nil } + + it 'returns 204 no_content' do + get :prometheus_proxy, params: prometheus_proxy_params + + expect(json_response['status']).to eq(_('processing')) + expect(json_response['message']).to eq(_('Not ready yet. Try again later.')) + expect(response).to have_gitlab_http_status(:no_content) + end + end + + context 'with 404 result' do + let(:service_result) { { http_status: 404, status: :success, body: '{"body": "value"}' } } + + it 'returns body' do + get :prometheus_proxy, params: prometheus_proxy_params + + expect(response).to have_gitlab_http_status(:not_found) + expect(json_response['body']).to eq('value') + end + end + + context 'with error result' do + context 'with http_status' do + let(:service_result) do + { http_status: :service_unavailable, status: :error, message: 'error message' } + end + + it 'sets the http response status code' do + get :prometheus_proxy, params: prometheus_proxy_params + + expect(response).to have_gitlab_http_status(:service_unavailable) + expect(json_response['status']).to eq('error') + expect(json_response['message']).to eq('error message') + end + end + + context 'without http_status' do + let(:service_result) { { status: :error, message: 'error message' } } + + it 'returns bad_request' do + get :prometheus_proxy, params: prometheus_proxy_params + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['status']).to eq('error') + expect(json_response['message']).to eq('error message') + end + end + end + end + + context 'with inappropriate requests' do + let(:prometheus_body) { nil } + + context 'without correct permissions' do + let(:user2) { create(:user) } + + before do + sign_out(user) + sign_in(user2) + end + + it 'returns 404' do + get :prometheus_proxy, params: prometheus_proxy_params + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end + + context 'with invalid proxyable id' do + let(:prometheus_body) { nil } + + it 'returns 404' do + get :prometheus_proxy, params: prometheus_proxy_params(id: proxyable.id + 1) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + private + + def prometheus_proxy_params(params = {}) + { + proxy_path: 'query', + query: '1' + }.merge(proxyable_params).merge(params) + end +end diff --git a/spec/support/shared_examples/controllers/metrics_dashboard_shared_examples.rb b/spec/support/shared_examples/controllers/metrics_dashboard_shared_examples.rb new file mode 100644 index 00000000000..cb8f6721d66 --- /dev/null +++ b/spec/support/shared_examples/controllers/metrics_dashboard_shared_examples.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +RSpec.shared_examples_for 'GET #metrics_dashboard correctly formatted response' do + it 'returns a json object with the correct keys' do + get :metrics_dashboard, params: metrics_dashboard_req_params, format: :json + + # Exclude `all_dashboards` to handle separately, at spec/controllers/projects/environments_controller_spec.rb:565 + # because `all_dashboards` key is not part of expected shared behavior + found_keys = json_response.keys - ['all_dashboards'] + + expect(response).to have_gitlab_http_status(status_code) + expect(found_keys).to contain_exactly(*expected_keys) + end +end + +RSpec.shared_examples_for 'GET #metrics_dashboard for dashboard' do |dashboard_name| + let(:expected_keys) { %w(dashboard status metrics_data) } + let(:status_code) { :ok } + + it_behaves_like 'GET #metrics_dashboard correctly formatted response' + + it 'returns correct dashboard' do + get :metrics_dashboard, params: metrics_dashboard_req_params, format: :json + + expect(json_response['dashboard']['dashboard']).to eq(dashboard_name) + end +end diff --git a/spec/support/shared_examples/controllers/namespace_storage_limit_alert_shared_examples.rb b/spec/support/shared_examples/controllers/namespace_storage_limit_alert_shared_examples.rb deleted file mode 100644 index 7885eb6c1f8..00000000000 --- a/spec/support/shared_examples/controllers/namespace_storage_limit_alert_shared_examples.rb +++ /dev/null @@ -1,53 +0,0 @@ -# frozen_string_literal: true - -RSpec.shared_examples 'namespace storage limit alert' do - let(:alert_level) { :info } - - before do - allow_next_instance_of(Namespaces::CheckStorageSizeService, namespace, user) do |check_storage_size_service| - expect(check_storage_size_service).to receive(:execute).and_return( - ServiceResponse.success( - payload: { - alert_level: alert_level, - usage_message: "Usage", - explanation_message: "Explanation", - root_namespace: namespace - } - ) - ) - end - - allow(controller).to receive(:current_user).and_return(user) - end - - render_views - - it 'does render' do - subject - - expect(response.body).to match(/Explanation/) - expect(response.body).to have_css('.js-namespace-storage-alert-dismiss') - end - - context 'when alert_level is error' do - let(:alert_level) { :error } - - it 'does not render a dismiss button' do - subject - - expect(response.body).not_to have_css('.js-namespace-storage-alert-dismiss') - end - end - - context 'when cookie is set' do - before do - cookies["hide_storage_limit_alert_#{namespace.id}_info"] = 'true' - end - - it 'does not render alert' do - subject - - expect(response.body).not_to match(/Explanation/) - end - end -end diff --git a/spec/support/shared_examples/controllers/snippet_blob_shared_examples.rb b/spec/support/shared_examples/controllers/snippet_blob_shared_examples.rb new file mode 100644 index 00000000000..c3e8f807afb --- /dev/null +++ b/spec/support/shared_examples/controllers/snippet_blob_shared_examples.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'raw snippet blob' do + context 'with valid params' do + before do + subject + end + + it 'delivers file with correct Workhorse headers' do + expect(response.header['Content-Type']).to eq('text/plain; charset=utf-8') + expect(response.header[Gitlab::Workhorse::DETECT_HEADER]).to eq 'true' + expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with('git-blob:') + end + + it 'responds with status 200' do + expect(response).to have_gitlab_http_status(:ok) + end + end + + context 'with invalid file path' do + let(:filepath) { 'doesnotexist' } + + it_behaves_like 'returning response status', :not_found + end + + context 'with invalid ref' do + let(:ref) { 'doesnotexist' } + + it_behaves_like 'returning response status', :not_found + end + + it_behaves_like 'content disposition headers' +end + +RSpec.shared_examples 'raw snippet without repository' do |unauthorized_status| + context 'when authorized' do + it 'returns a 422' do + subject + + expect(response).to have_gitlab_http_status(:unprocessable_entity) + end + end + + context 'when unauthorized' do + let(:visibility) { :private } + + it_behaves_like 'returning response status', unauthorized_status + end +end diff --git a/spec/support/shared_examples/controllers/snippets_sort_order_shared_examples.rb b/spec/support/shared_examples/controllers/snippets_sort_order_shared_examples.rb new file mode 100644 index 00000000000..aa4d78b23f4 --- /dev/null +++ b/spec/support/shared_examples/controllers/snippets_sort_order_shared_examples.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'snippets sort order' do + let(:params) { {} } + let(:sort_argument) { {} } + let(:sort_params) { params.merge(sort_argument)} + + before do + sign_in(user) + + stub_snippet_counter + end + + subject { get :index, params: sort_params } + + context 'when no sort param is provided' do + it 'calls SnippetsFinder with updated_at sort option' do + expect(SnippetsFinder).to receive(:new).with(user, + hash_including(sort: 'updated_desc')).and_call_original + + subject + end + end + + context 'when sort param is provided' do + let(:order) { 'created_desc' } + let(:sort_argument) { { sort: order } } + + it 'calls SnippetsFinder with the given sort param' do + expect(SnippetsFinder).to receive(:new).with(user, + hash_including(sort: order)).and_call_original + + subject + end + end + + def stub_snippet_counter + allow(Snippets::CountService) + .to receive(:new).and_return(double(:count_service, execute: {})) + end +end diff --git a/spec/support/shared_examples/controllers/unique_visits_shared_examples.rb b/spec/support/shared_examples/controllers/unique_visits_shared_examples.rb new file mode 100644 index 00000000000..90588756eb0 --- /dev/null +++ b/spec/support/shared_examples/controllers/unique_visits_shared_examples.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'tracking unique visits' do |method| + it 'tracks unique visit if the format is HTML' do + expect_any_instance_of(Gitlab::Analytics::UniqueVisits).to receive(:track_visit).with(instance_of(String), target_id) + + get method, params: request_params, format: :html + end + + it 'tracks unique visit if DNT is not enabled' do + expect_any_instance_of(Gitlab::Analytics::UniqueVisits).to receive(:track_visit).with(instance_of(String), target_id) + request.headers['DNT'] = '0' + + get method, params: request_params, format: :html + end + + it 'does not track unique visit if DNT is enabled' do + expect_any_instance_of(Gitlab::Analytics::UniqueVisits).not_to receive(:track_visit) + request.headers['DNT'] = '1' + + get method, params: request_params, format: :html + end + + it 'does not track unique visit if the format is JSON' do + expect_any_instance_of(Gitlab::Analytics::UniqueVisits).not_to receive(:track_visit) + + get method, params: request_params, format: :json + end +end 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 b5f2c0d07bf..4df3139d56e 100644 --- a/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb +++ b/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb @@ -104,6 +104,35 @@ RSpec.shared_examples 'wiki controller actions' do end end + describe 'GET #diff' do + context 'when commit exists' do + it 'renders the diff' do + get :diff, params: routing_params.merge(id: wiki_title, version_id: wiki.repository.commit.id) + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to render_template('shared/wikis/diff') + expect(assigns(:diffs)).to be_a(Gitlab::Diff::FileCollection::Base) + expect(assigns(:diff_notes_disabled)).to be(true) + end + end + + context 'when commit does not exist' do + it 'returns a 404 error' do + get :diff, params: routing_params.merge(id: wiki_title, version_id: 'invalid') + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'when page does not exist' do + it 'returns a 404 error' do + get :diff, params: routing_params.merge(id: 'invalid') + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end + describe 'GET #show' do render_views @@ -118,6 +147,7 @@ RSpec.shared_examples 'wiki controller actions' do subject expect(response).to have_gitlab_http_status(:ok) + expect(response).to render_template('shared/wikis/show') expect(assigns(:page).title).to eq(wiki_title) expect(assigns(:sidebar_wiki_entries)).to contain_exactly(an_instance_of(WikiPage)) expect(assigns(:sidebar_limited)).to be(false) @@ -130,6 +160,7 @@ RSpec.shared_examples 'wiki controller actions' do subject expect(response).to have_gitlab_http_status(:ok) + expect(response).to render_template('shared/wikis/show') expect(flash[:notice]).to eq(_('The content of this page is not encoded in UTF-8. Edits can only be made via the Git repository.')) end end @@ -138,19 +169,37 @@ RSpec.shared_examples 'wiki controller actions' do context 'when the page does not exist' do let(:id) { 'does not exist' } - before do - subject - end + context 'when the user can create pages' do + before do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to render_template('shared/wikis/edit') + end + + it 'builds a new wiki page with the id as the title' do + expect(assigns(:page).title).to eq(id) + end + + context 'when a random_title param is present' do + let(:random_title) { true } - it 'builds a new wiki page with the id as the title' do - expect(assigns(:page).title).to eq(id) + it 'builds a new wiki page with no title' do + expect(assigns(:page).title).to be_empty + end + end end - context 'when a random_title param is present' do - let(:random_title) { true } + context 'when the user cannot create pages' do + before do + sign_out(:user) + end - it 'builds a new wiki page with no title' do - expect(assigns(:page).title).to be_empty + it 'shows the empty state' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to render_template('shared/wikis/empty') end end end @@ -166,6 +215,7 @@ RSpec.shared_examples 'wiki controller actions' do it 'delivers the file with the correct headers' do subject + expect(response).to have_gitlab_http_status(:ok) expect(response.headers['Content-Disposition']).to match(/^inline/) expect(response.headers[Gitlab::Workhorse::DETECT_HEADER]).to eq('true') expect(response.cache_control[:public]).to be(false) @@ -179,12 +229,31 @@ RSpec.shared_examples 'wiki controller actions' do it 'renders json in a correct format' do post :preview_markdown, params: routing_params.merge(id: 'page/path', text: '*Markdown* text') + expect(response).to have_gitlab_http_status(:ok) expect(json_response.keys).to match_array(%w(body references)) end end - describe 'GET #edit' do - subject { get(:edit, params: routing_params.merge(id: wiki_title)) } + shared_examples 'edit action' do + context 'when the page does not exist' do + let(:id_param) { 'invalid' } + + it 'redirects to show' do + subject + + expect(response).to redirect_to_wiki(wiki, 'invalid') + end + end + + context 'when id param is blank' do + let(:id_param) { ' ' } + + it 'redirects to the home page' do + subject + + expect(response).to redirect_to_wiki(wiki, 'home') + end + end context 'when page content encoding is invalid' do it 'redirects to show' do @@ -208,6 +277,14 @@ RSpec.shared_examples 'wiki controller actions' do expect(response).to redirect_to_wiki(wiki, page) end end + end + + describe 'GET #edit' do + let(:id_param) { wiki_title } + + subject { get(:edit, params: routing_params.merge(id: id_param)) } + + it_behaves_like 'edit action' context 'when page content encoding is valid' do render_views @@ -224,23 +301,17 @@ RSpec.shared_examples 'wiki controller actions' do describe 'PATCH #update' do let(:new_title) { 'New title' } let(:new_content) { 'New content' } + let(:id_param) { wiki_title } subject do patch(:update, params: routing_params.merge( - id: wiki_title, + id: id_param, wiki: { title: new_title, content: new_content } )) end - context 'when page content encoding is invalid' do - it 'redirects to show' do - allow(controller).to receive(:valid_encoding?).and_return(false) - - subject - expect(response).to redirect_to_wiki(wiki, wiki.list_pages.first) - end - end + it_behaves_like 'edit action' context 'when page content encoding is valid' do render_views diff --git a/spec/support/shared_examples/create_alert_issue_shared_examples.rb b/spec/support/shared_examples/create_alert_issue_shared_examples.rb new file mode 100644 index 00000000000..9f4e1c4335a --- /dev/null +++ b/spec/support/shared_examples/create_alert_issue_shared_examples.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'create alert issue sets issue labels' do + let(:title) { IncidentManagement::CreateIncidentLabelService::LABEL_PROPERTIES[:title] } + let!(:label) { create(:label, project: project, title: title) } + let(:label_service) { instance_double(IncidentManagement::CreateIncidentLabelService, execute: label_service_response) } + + before do + allow(IncidentManagement::CreateIncidentLabelService).to receive(:new).with(project, user).and_return(label_service) + end + + context 'when create incident label responds with success' do + let(:label_service_response) { ServiceResponse.success(payload: { label: label }) } + + it 'adds label to issue' do + expect(issue.labels).to eq([label]) + end + end + + context 'when create incident label responds with error' do + let(:label_service_response) { ServiceResponse.error(payload: { label: label }, message: 'label error') } + + it 'creates an issue without labels' do + expect(issue.labels).to be_empty + end + end +end diff --git a/spec/support/shared_examples/features/discussion_comments_shared_example.rb b/spec/support/shared_examples/features/discussion_comments_shared_example.rb index 6007798c290..9fc5d8933e5 100644 --- a/spec/support/shared_examples/features/discussion_comments_shared_example.rb +++ b/spec/support/shared_examples/features/discussion_comments_shared_example.rb @@ -266,7 +266,7 @@ RSpec.shared_examples 'thread comments' do |resource_name| end end - it 'has "Comment" selected when opening the menu', quarantine: 'https://gitlab.com/gitlab-org/gitlab/issues/196825' do + it 'has "Comment" selected when opening the menu', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/196825' do find(toggle_selector).click find("#{menu_selector} li", match: :first) diff --git a/spec/support/shared_examples/features/editable_merge_request_shared_examples.rb b/spec/support/shared_examples/features/editable_merge_request_shared_examples.rb index 964c80007b0..487c38da7da 100644 --- a/spec/support/shared_examples/features/editable_merge_request_shared_examples.rb +++ b/spec/support/shared_examples/features/editable_merge_request_shared_examples.rb @@ -53,13 +53,17 @@ RSpec.shared_examples 'an editable merge request' do find('#merge_request_description').native.send_keys('') fill_in 'merge_request_description', with: user.to_reference[0..4] - wait_for_requests - page.within('.atwho-view') do expect(page).to have_content(user2.name) end end + it 'description has quick action autocomplete', :js do + find('#merge_request_description').native.send_keys('/') + + expect(page).to have_selector('.atwho-container') + end + it 'has class js-quick-submit in form' do expect(page).to have_selector('.js-quick-submit') end diff --git a/spec/support/shared_examples/features/error_tracking_shared_example.rb b/spec/support/shared_examples/features/error_tracking_shared_example.rb index 1cd05b22ae9..ae7d62f31a2 100644 --- a/spec/support/shared_examples/features/error_tracking_shared_example.rb +++ b/spec/support/shared_examples/features/error_tracking_shared_example.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -shared_examples 'error tracking index page' do +RSpec.shared_examples 'error tracking index page' do it 'renders the error index page', quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/217810' } do within('div.js-title-container') do expect(page).to have_content(project.namespace.name) @@ -33,7 +33,7 @@ shared_examples 'error tracking index page' do end end -shared_examples 'expanded stack trace context' do |selected_line: nil, expected_line: 1| +RSpec.shared_examples 'expanded stack trace context' do |selected_line: nil, expected_line: 1| it 'expands the stack trace context', quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/217810' } do within('div.stacktrace') do find("div.file-holder:nth-child(#{selected_line}) svg.ic-chevron-right").click if selected_line @@ -48,7 +48,7 @@ shared_examples 'expanded stack trace context' do |selected_line: nil, expected_ end end -shared_examples 'error tracking show page' do +RSpec.shared_examples 'error tracking show page' do it 'renders the error details', quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/217810' } do content = page.find(".content") nav = page.find("nav.breadcrumbs") diff --git a/spec/support/shared_examples/features/master_manages_access_requests_shared_example.rb b/spec/support/shared_examples/features/master_manages_access_requests_shared_example.rb index 98010150e65..00ce690d2e3 100644 --- a/spec/support/shared_examples/features/master_manages_access_requests_shared_example.rb +++ b/spec/support/shared_examples/features/master_manages_access_requests_shared_example.rb @@ -35,7 +35,12 @@ RSpec.shared_examples 'Maintainer manages access requests' do expect_visible_access_request(entity, user) - accept_confirm { click_on 'Deny access' } + # Open modal + click_on 'Deny access request' + + expect(page).not_to have_field "Also unassign this user from related issues and merge requests" + + click_on 'Deny access request' expect_no_visible_access_request(entity, user) expect(page).not_to have_content user.name diff --git a/spec/support/shared_examples/graphql/jira_import/jira_import_resolver_shared_examples.rb b/spec/support/shared_examples/graphql/jira_import/jira_import_resolver_shared_examples.rb index 2b96010477c..b2047f1d32c 100644 --- a/spec/support/shared_examples/graphql/jira_import/jira_import_resolver_shared_examples.rb +++ b/spec/support/shared_examples/graphql/jira_import/jira_import_resolver_shared_examples.rb @@ -1,12 +1,12 @@ # frozen_string_literal: true -shared_examples 'no Jira import data present' do +RSpec.shared_examples 'no Jira import data present' do it 'returns none' do expect(resolve_imports).to eq JiraImportState.none end end -shared_examples 'no Jira import access' do +RSpec.shared_examples 'no Jira import access' do it 'raises error' do expect do resolve_imports diff --git a/spec/support/shared_examples/graphql/mutation_shared_examples.rb b/spec/support/shared_examples/graphql/mutation_shared_examples.rb index 022d41c0bdd..86d2bb6c747 100644 --- a/spec/support/shared_examples/graphql/mutation_shared_examples.rb +++ b/spec/support/shared_examples/graphql/mutation_shared_examples.rb @@ -7,13 +7,23 @@ # # There must be a method or let called `mutation` defined that executes # the mutation. -RSpec.shared_examples 'a mutation that returns top-level errors' do |errors:| +RSpec.shared_examples 'a mutation that returns top-level errors' do |errors: []| + let(:match_errors) { eq(errors) } + it do post_graphql_mutation(mutation, current_user: current_user) error_messages = graphql_errors.map { |e| e['message'] } - expect(error_messages).to eq(errors) + expect(error_messages).to match_errors + end +end + +RSpec.shared_examples 'an invalid argument to the mutation' do |argument_name:| + it_behaves_like 'a mutation that returns top-level errors' do + let(:match_errors) do + contain_exactly(include("invalid value for #{GraphqlHelpers.fieldnamerize(argument_name)}")) + end end end diff --git a/spec/support/shared_examples/graphql/projects/services_resolver_shared_examples.rb b/spec/support/shared_examples/graphql/projects/services_resolver_shared_examples.rb index 4bed322564a..94b7ed1618d 100644 --- a/spec/support/shared_examples/graphql/projects/services_resolver_shared_examples.rb +++ b/spec/support/shared_examples/graphql/projects/services_resolver_shared_examples.rb @@ -1,12 +1,12 @@ # frozen_string_literal: true -shared_examples 'no project services' do +RSpec.shared_examples 'no project services' do it 'returns empty collection' do expect(resolve_services).to eq [] end end -shared_examples 'cannot access project services' do +RSpec.shared_examples 'cannot access project services' do it 'raises error' do expect do resolve_services diff --git a/spec/support/shared_examples/graphql/resolves_issuable_shared_examples.rb b/spec/support/shared_examples/graphql/resolves_issuable_shared_examples.rb index 58cd3d21f66..67d1c2a8254 100644 --- a/spec/support/shared_examples/graphql/resolves_issuable_shared_examples.rb +++ b/spec/support/shared_examples/graphql/resolves_issuable_shared_examples.rb @@ -1,7 +1,12 @@ # frozen_string_literal: true RSpec.shared_examples 'resolving an issuable in GraphQL' do |type| - subject { mutation.resolve_issuable(type: type, parent_path: parent.full_path, iid: issuable.iid) } + include GraphqlHelpers + + let(:parent_path) { parent.full_path } + let(:iid) { issuable.iid } + + subject(:result) { mutation.resolve_issuable(type: type, parent_path: parent_path, iid: iid) } context 'when user has access' do before do @@ -9,37 +14,23 @@ RSpec.shared_examples 'resolving an issuable in GraphQL' do |type| end it 'resolves issuable by iid' do - result = type == :merge_request ? subject.sync : subject expect(result).to eq(issuable) end - it 'uses the correct Resolver to resolve issuable' do - resolver_class = "Resolvers::#{type.to_s.classify.pluralize}Resolver".constantize - resolve_method = type == :epic ? :resolve_group : :resolve_project - resolved_parent = mutation.send(resolve_method, full_path: parent.full_path) - - allow(mutation).to receive(resolve_method) - .with(full_path: parent.full_path) - .and_return(resolved_parent) - - expect(resolver_class.single).to receive(:new) - .with(object: resolved_parent, context: context, field: nil) - .and_call_original - - subject - end - - it 'returns nil if issuable is not found' do - result = mutation.resolve_issuable(type: type, parent_path: parent.full_path, iid: "100") - result = result.respond_to?(:sync) ? result.sync : result + context 'the IID does not refer to a valid issuable' do + let(:iid) { '100' } - expect(result).to be_nil + it 'returns nil' do + expect(result).to be_nil + end end - it 'returns nil if parent path is not present' do - result = mutation.resolve_issuable(type: type, parent_path: "", iid: issuable.iid) + context 'the parent path is not present' do + let(:parent_path) { '' } - expect(result).to be_nil + it 'returns nil' do + expect(result).to be_nil + end end end end diff --git a/spec/support/shared_examples/helm_commands_shared_examples.rb b/spec/support/shared_examples/helm_commands_shared_examples.rb index f0624fbf29f..0a94c6648cc 100644 --- a/spec/support/shared_examples/helm_commands_shared_examples.rb +++ b/spec/support/shared_examples/helm_commands_shared_examples.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -shared_examples 'helm command generator' do +RSpec.shared_examples 'helm command generator' do describe '#generate_script' do let(:helm_setup) do <<~EOS @@ -14,7 +14,7 @@ shared_examples 'helm command generator' do end end -shared_examples 'helm command' do +RSpec.shared_examples 'helm command' do describe '#rbac?' do subject { command.rbac? } diff --git a/spec/support/shared_examples/lib/gitlab/background_migration/mentions_migration_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/background_migration/mentions_migration_shared_examples.rb index a40c38106e2..af65b61021c 100644 --- a/spec/support/shared_examples/lib/gitlab/background_migration/mentions_migration_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/background_migration/mentions_migration_shared_examples.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -shared_examples 'resource mentions migration' do |migration_class, resource_class| +RSpec.shared_examples 'resource mentions migration' do |migration_class, resource_class| it 'migrates resource mentions' do join = migration_class::JOIN conditions = migration_class::QUERY_CONDITIONS @@ -21,7 +21,7 @@ shared_examples 'resource mentions migration' do |migration_class, resource_clas end end -shared_examples 'resource notes mentions migration' do |migration_class, resource_class| +RSpec.shared_examples 'resource notes mentions migration' do |migration_class, resource_class| it 'migrates mentions from note' do join = migration_class::JOIN conditions = migration_class::QUERY_CONDITIONS @@ -56,7 +56,7 @@ shared_examples 'resource notes mentions migration' do |migration_class, resourc end end -shared_examples 'schedules resource mentions migration' do |resource_class, is_for_notes| +RSpec.shared_examples 'schedules resource mentions migration' do |resource_class, is_for_notes| before do stub_const("#{described_class.name}::BATCH_SIZE", 1) end diff --git a/spec/support/shared_examples/lib/gitlab/cycle_analytics/base_stage_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/cycle_analytics/base_stage_shared_examples.rb index 14292f70228..d76089d56dd 100644 --- a/spec/support/shared_examples/lib/gitlab/cycle_analytics/base_stage_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/cycle_analytics/base_stage_shared_examples.rb @@ -4,7 +4,7 @@ require 'spec_helper' ISSUES_MEDIAN = 30.minutes.to_i -shared_examples 'base stage' do +RSpec.shared_examples 'base stage' do let(:stage) { described_class.new(options: { project: double }) } before do @@ -35,7 +35,7 @@ shared_examples 'base stage' do end end -shared_examples 'calculate #median with date range' do +RSpec.shared_examples 'calculate #median with date range' do context 'when valid date range is given' do before do stage_options[:from] = 5.days.ago @@ -55,7 +55,7 @@ shared_examples 'calculate #median with date range' do end end -shared_examples 'Gitlab::Analytics::CycleAnalytics::DataCollector backend examples' do +RSpec.shared_examples 'Gitlab::Analytics::CycleAnalytics::DataCollector backend examples' do let(:stage_params) { Gitlab::Analytics::CycleAnalytics::DefaultStages.send("params_for_#{stage_name}_stage").merge(project: project) } let(:stage) { Analytics::CycleAnalytics::ProjectStage.new(stage_params) } let(:data_collector) { Gitlab::Analytics::CycleAnalytics::DataCollector.new(stage: stage, params: { from: stage_options[:from], current_user: project.creator }) } diff --git a/spec/support/shared_examples/lib/gitlab/cycle_analytics/default_query_config_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/cycle_analytics/default_query_config_shared_examples.rb index c053af010b3..4f648b27ea2 100644 --- a/spec/support/shared_examples/lib/gitlab/cycle_analytics/default_query_config_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/cycle_analytics/default_query_config_shared_examples.rb @@ -2,7 +2,7 @@ require 'spec_helper' -shared_examples 'default query config' do +RSpec.shared_examples 'default query config' do let(:project) { create(:project) } let(:event) { described_class.new(stage: stage_name, options: { from: 1.day.ago, project: project }) } diff --git a/spec/support/shared_examples/lib/gitlab/cycle_analytics/event_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/cycle_analytics/event_shared_examples.rb index a00359ce979..d0e41605e00 100644 --- a/spec/support/shared_examples/lib/gitlab/cycle_analytics/event_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/cycle_analytics/event_shared_examples.rb @@ -8,6 +8,7 @@ RSpec.shared_examples_for 'cycle analytics event' do it { expect(described_class.identifier).to be_a_kind_of(Symbol) } it { expect(instance.object_type.ancestors).to include(ApplicationRecord) } it { expect(instance).to respond_to(:timestamp_projection) } + it { expect(instance.column_list).to be_a_kind_of(Array) } describe '#apply_query_customization' do it 'expects an ActiveRecord::Relation object as argument and returns a modified version of it' do 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 a1cdd054f32..e43ce936b90 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 @@ -10,7 +10,7 @@ RSpec.shared_examples 'diff statistics' do |test_include_stats_flag: true| end end - context 'when should request diff stats' do + context 'when include_stats is true' do it 'Repository#diff_stats is called' do expect(diffable.project.repository) .to receive(:diff_stats) @@ -59,43 +59,87 @@ RSpec.shared_examples 'unfoldable diff' do end RSpec.shared_examples 'cacheable diff collection' do - let(:cache) { instance_double(Gitlab::Diff::HighlightCache) } + let(:highlight_cache) { instance_double(Gitlab::Diff::HighlightCache, write_if_empty: true, clear: nil, decorate: nil) } + let(:stats_cache) { instance_double(Gitlab::Diff::StatsCache, read: nil, write_if_empty: true, clear: nil) } before do - expect(Gitlab::Diff::HighlightCache).to receive(:new).with(subject) { cache } + expect(Gitlab::Diff::HighlightCache).to receive(:new).with(subject) { highlight_cache } end describe '#write_cache' do + before do + expect(Gitlab::Diff::StatsCache).to receive(:new).with(cachable_key: diffable.cache_key) { stats_cache } + end + it 'calls Gitlab::Diff::HighlightCache#write_if_empty' do - expect(cache).to receive(:write_if_empty).once + expect(highlight_cache).to receive(:write_if_empty).once + + subject.write_cache + end + + it 'calls Gitlab::Diff::StatsCache#write_if_empty with diff stats' do + diff_stats = Gitlab::Git::DiffStatsCollection.new([]) + + expect(diffable.project.repository) + .to receive(:diff_stats).and_return(diff_stats) + + expect(stats_cache).to receive(:write_if_empty).once.with(diff_stats) subject.write_cache end end describe '#clear_cache' do + before do + expect(Gitlab::Diff::StatsCache).to receive(:new).with(cachable_key: diffable.cache_key) { stats_cache } + end + it 'calls Gitlab::Diff::HighlightCache#clear' do - expect(cache).to receive(:clear).once + expect(highlight_cache).to receive(:clear).once subject.clear_cache end - end - describe '#cache_key' do - it 'calls Gitlab::Diff::HighlightCache#key' do - expect(cache).to receive(:key).once + it 'calls Gitlab::Diff::StatsCache#clear' do + expect(stats_cache).to receive(:clear).once - subject.cache_key + subject.clear_cache end end describe '#diff_files' do + before do + expect(Gitlab::Diff::StatsCache).to receive(:new).with(cachable_key: diffable.cache_key) { stats_cache } + end + it 'calls Gitlab::Diff::HighlightCache#decorate' do - expect(cache).to receive(:decorate) + expect(highlight_cache).to receive(:decorate) .with(instance_of(Gitlab::Diff::File)) .exactly(cacheable_files_count).times subject.diff_files end + + context 'when there are stats cached' do + before do + allow(stats_cache).to receive(:read).and_return(Gitlab::Git::DiffStatsCollection.new([])) + end + + it 'does not make a diff stats rpc call' do + expect(diffable.project.repository).not_to receive(:diff_stats) + + subject.diff_files + end + end + + context 'when there are no stats cached' do + it 'makes a diff stats rpc call' do + expect(diffable.project.repository) + .to receive(:diff_stats) + .with(diffable.diff_refs.base_sha, diffable.diff_refs.head_sha) + + subject.diff_files + end + end end end diff --git a/spec/support/shared_examples/lib/gitlab/gl_repository_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/gl_repository_shared_examples.rb index 97f4341340d..28137530038 100644 --- a/spec/support/shared_examples/lib/gitlab/gl_repository_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/gl_repository_shared_examples.rb @@ -1,12 +1,12 @@ # frozen_string_literal: true RSpec.shared_examples 'parsing gl_repository identifier' do - subject { described_class.new(identifier) } + subject { described_class.parse(identifier) } it 'returns correct information' do - aggregate_failures do - expect(subject.repo_type).to eq(expected_type) - expect(subject.fetch_container!).to eq(expected_container) - end + expect(subject).to have_attributes( + repo_type: expected_type, + container: expected_container + ) end end diff --git a/spec/support/shared_examples/lib/gitlab/import/stuck_import_job_workers_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/import/stuck_import_job_workers_shared_examples.rb index 06ea540706a..222390cf9cd 100644 --- a/spec/support/shared_examples/lib/gitlab/import/stuck_import_job_workers_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/import/stuck_import_job_workers_shared_examples.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -shared_examples 'stuck import job detection' do +RSpec.shared_examples 'stuck import job detection' do context 'when the job has completed' do context 'when the import status was already updated' do before do diff --git a/spec/support/shared_examples/lib/gitlab/jira_import/base_importer_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/jira_import/base_importer_shared_examples.rb index 85dcc053447..b1788bb5912 100644 --- a/spec/support/shared_examples/lib/gitlab/jira_import/base_importer_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/jira_import/base_importer_shared_examples.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true -shared_examples 'raise exception if not implemented' do +RSpec.shared_examples 'raise exception if not implemented' do it { expect { described_class.new(project).imported_items_cache_key }.not_to raise_error } end diff --git a/spec/support/shared_examples/lib/wikis_api_examples.rb b/spec/support/shared_examples/lib/wikis_api_examples.rb new file mode 100644 index 00000000000..2e4c667d37e --- /dev/null +++ b/spec/support/shared_examples/lib/wikis_api_examples.rb @@ -0,0 +1,174 @@ +# frozen_string_literal: true + +RSpec.shared_examples_for 'wikis API returns list of wiki pages' do + context 'when wiki has pages' do + let!(:pages) do + [create(:wiki_page, wiki: wiki, title: 'page1', content: 'content of page1'), + create(:wiki_page, wiki: wiki, title: 'page2.with.dot', content: 'content of page2')] + end + + it 'returns the list of wiki pages without content' do + get api(url, user) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response.size).to eq(2) + + json_response.each_with_index do |page, index| + expect(page.keys).to match_array(expected_keys_without_content) + expect(page['slug']).to eq(pages[index].slug) + expect(page['title']).to eq(pages[index].title) + end + end + + it 'returns the list of wiki pages with content' do + get api(url, user), params: { with_content: 1 } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response.size).to eq(2) + + json_response.each_with_index do |page, index| + expect(page.keys).to match_array(expected_keys_with_content) + expect(page['content']).to eq(pages[index].content) + expect(page['slug']).to eq(pages[index].slug) + expect(page['title']).to eq(pages[index].title) + end + end + end + + it 'return the empty list of wiki pages' do + get api(url, user) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response.size).to eq(0) + end +end + +RSpec.shared_examples_for 'wikis API returns wiki page' do + it 'returns the wiki page' do + expect(response).to have_gitlab_http_status(:ok) + expect(json_response.size).to eq(4) + expect(json_response.keys).to match_array(expected_keys_with_content) + expect(json_response['content']).to eq(page.content) + expect(json_response['slug']).to eq(page.slug) + expect(json_response['title']).to eq(page.title) + end +end + +RSpec.shared_examples_for 'wikis API creates wiki page' do + it 'creates the wiki page' do + post(api(url, user), params: payload) + + expect(response).to have_gitlab_http_status(:created) + expect(json_response.size).to eq(4) + expect(json_response.keys).to match_array(expected_keys_with_content) + expect(json_response['content']).to eq(payload[:content]) + expect(json_response['slug']).to eq(payload[:title].tr(' ', '-')) + expect(json_response['title']).to eq(payload[:title]) + expect(json_response['rdoc']).to eq(payload[:rdoc]) + end + + [:title, :content].each do |part| + it "responds with validation error on empty #{part}" do + payload.delete(part) + + post(api(url, user), params: payload) + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response.size).to eq(1) + expect(json_response['error']).to eq("#{part} is missing") + end + end +end + +RSpec.shared_examples_for 'wikis API updates wiki page' do + it 'updates the wiki page' do + put(api(url, user), params: payload) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response.size).to eq(4) + expect(json_response.keys).to match_array(expected_keys_with_content) + expect(json_response['content']).to eq(payload[:content]) + expect(json_response['slug']).to eq(payload[:title].tr(' ', '-')) + expect(json_response['title']).to eq(payload[:title]) + end + + [:title, :content, :format].each do |part| + it "updates with wiki with missing #{part}" do + payload.delete(part) + + put(api(url, user), params: payload) + + expect(response).to have_gitlab_http_status(:ok) + end + end +end + +RSpec.shared_examples_for 'wiki API 403 Forbidden' do + it 'returns 403 Forbidden' do + expect(response).to have_gitlab_http_status(:forbidden) + expect(json_response.size).to eq(1) + expect(json_response['message']).to eq('403 Forbidden') + end +end + +RSpec.shared_examples_for 'wiki API 404 Wiki Page Not Found' do + it 'returns 404 Wiki Page Not Found' do + expect(response).to have_gitlab_http_status(:not_found) + expect(json_response.size).to eq(1) + expect(json_response['message']).to eq('404 Wiki Page Not Found') + end +end + +RSpec.shared_examples_for 'wiki API 404 Not Found' do |what| + it "returns 404 #{what} Not Found" do + expect(response).to have_gitlab_http_status(:not_found) + expect(json_response.size).to eq(1) + expect(json_response['message']).to eq("404 #{what} Not Found") + end +end + +RSpec.shared_examples_for 'wiki API 204 No Content' do + it 'returns 204 No Content' do + expect(response).to have_gitlab_http_status(:no_content) + end +end + +RSpec.shared_examples_for 'wiki API uploads wiki attachment' do + it 'pushes attachment to the wiki repository' do + allow(SecureRandom).to receive(:hex).and_return('fixed_hex') + + workhorse_post_with_file(api(url, user), file_key: :file, params: payload) + + expect(response).to have_gitlab_http_status(:created) + expect(json_response).to eq result_hash.deep_stringify_keys + end + + it 'responds with validation error on empty file' do + payload.delete(:file) + + post(api(url, user), params: payload) + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response.size).to eq(1) + expect(json_response['error']).to eq('file is missing') + end + + it 'responds with validation error on invalid temp file' do + payload[:file] = { tempfile: '/etc/hosts' } + + post(api(url, user), params: payload) + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response.size).to eq(1) + expect(json_response['error']).to eq('file is invalid') + end + + it 'is backward compatible with regular multipart uploads' do + allow(SecureRandom).to receive(:hex).and_return('fixed_hex') + + post(api(url, user), params: payload) + + expect(response).to have_gitlab_http_status(:created) + expect(json_response).to eq result_hash.deep_stringify_keys + end +end diff --git a/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb b/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb index 0efa5e56199..f80ca235220 100644 --- a/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb +++ b/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb @@ -88,16 +88,6 @@ RSpec.shared_examples 'cluster application status specs' do |application_name| end end - it 'sets the correct version of the application' do - subject.update!(version: '0.0.0') - - subject.make_installed! - - subject.reload - - expect(subject.version).to eq(subject.class.const_get(:VERSION, false)) - end - context 'application is updating' do subject { create(application_name, :updating) } @@ -146,16 +136,6 @@ RSpec.shared_examples 'cluster application status specs' do |application_name| end end end - - it 'updates the version of the application' do - subject.update!(version: '0.0.0') - - subject.make_installed! - - subject.reload - - expect(subject.version).to eq(subject.class.const_get(:VERSION, false)) - end end end diff --git a/spec/support/shared_examples/models/cluster_application_version_shared_examples.rb b/spec/support/shared_examples/models/cluster_application_version_shared_examples.rb index cf7010c48c2..ed2e4fee2de 100644 --- a/spec/support/shared_examples/models/cluster_application_version_shared_examples.rb +++ b/spec/support/shared_examples/models/cluster_application_version_shared_examples.rb @@ -19,4 +19,32 @@ RSpec.shared_examples 'cluster application version specs' do |application_name| it { is_expected.to be_falsey } end end + + describe '#make_installed' do + subject { create(application_name, :installing) } + + it 'sets the correct version of the application' do + subject.update!(version: '0.0.0') + + subject.make_installed! + + subject.reload + + expect(subject.version).to eq(subject.class.const_get(:VERSION, false)) + end + + context 'application is updating' do + subject { create(application_name, :updating) } + + it 'updates the version of the application' do + subject.update!(version: '0.0.0') + + subject.make_installed! + + subject.reload + + expect(subject.version).to eq(subject.class.const_get(:VERSION, false)) + 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 7bcd6191f1d..3db5d7a8d7d 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 @@ -7,17 +7,17 @@ RSpec.shared_examples 'a BulkInsertSafe model' do |klass| let(:target_class) { klass.dup } # We consider all callbacks unsafe for bulk insertions unless we have explicitly - # whitelisted them (esp. anything related to :save, :create, :commit etc.) - let(:callback_method_blacklist) do + # allowed them (especially anything related to :save, :create, :commit, etc.) + let(:unsafe_callbacks) do ActiveRecord::Callbacks::CALLBACKS.reject do |callback| cb_name = callback.to_s.gsub(/(before_|after_|around_)/, '').to_sym - BulkInsertSafe::CALLBACK_NAME_WHITELIST.include?(cb_name) + BulkInsertSafe::ALLOWED_CALLBACKS.include?(cb_name) end.to_set end context 'when calling class methods directly' do it 'raises an error when method is not bulk-insert safe' do - callback_method_blacklist.each do |m| + unsafe_callbacks.each do |m| expect { target_class.send(m, nil) }.to( raise_error(BulkInsertSafe::MethodNotAllowedError), "Expected call to #{m} to raise an error, but it didn't" @@ -26,7 +26,7 @@ RSpec.shared_examples 'a BulkInsertSafe model' do |klass| end it 'does not raise an error when method is bulk-insert safe' do - BulkInsertSafe::CALLBACK_NAME_WHITELIST.each do |name| + BulkInsertSafe::ALLOWED_CALLBACKS.each do |name| expect { target_class.set_callback(name) {} }.not_to raise_error end end diff --git a/spec/support/shared_examples/models/jira_import_state_shared_examples.rb b/spec/support/shared_examples/models/jira_import_state_shared_examples.rb index f4643375c8e..1999f0f3c49 100644 --- a/spec/support/shared_examples/models/jira_import_state_shared_examples.rb +++ b/spec/support/shared_examples/models/jira_import_state_shared_examples.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -shared_examples 'multiple running imports not allowed' do +RSpec.shared_examples 'multiple running imports not allowed' do it 'returns not valid' do new_import = build(:jira_import_state, project: project) @@ -9,21 +9,21 @@ shared_examples 'multiple running imports not allowed' do end end -shared_examples 'in progress' do |status| +RSpec.shared_examples 'in progress' do |status| it 'returns true' do jira_import_state = build(:jira_import_state, status: status) expect(jira_import_state).to be_in_progress end end -shared_examples 'not in progress' do |status| +RSpec.shared_examples 'not in progress' do |status| it 'returns false' do jira_import_state = build(:jira_import_state, status: status) expect(jira_import_state).not_to be_in_progress end end -shared_examples 'can transition' do |states| +RSpec.shared_examples 'can transition' do |states| states.each do |state| it 'returns true' do expect(jira_import.send(state)).to be true @@ -31,7 +31,7 @@ shared_examples 'can transition' do |states| end end -shared_examples 'cannot transition' do |states| +RSpec.shared_examples 'cannot transition' do |states| states.each do |state| it 'returns false' do expect(jira_import.send(state)).to be false diff --git a/spec/support/shared_examples/models/note_access_check_shared_examples.rb b/spec/support/shared_examples/models/note_access_check_shared_examples.rb index 3bafad202f6..44edafe9091 100644 --- a/spec/support/shared_examples/models/note_access_check_shared_examples.rb +++ b/spec/support/shared_examples/models/note_access_check_shared_examples.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -shared_examples 'users with note access' do +RSpec.shared_examples 'users with note access' do it 'returns true' do users.each do |user| expect(note.system_note_with_references_visible_for?(user)).to be_truthy @@ -9,7 +9,7 @@ shared_examples 'users with note access' do end end -shared_examples 'users without note access' do +RSpec.shared_examples 'users without note access' do it 'returns false' do users.each do |user| expect(note.system_note_with_references_visible_for?(user)).to be_falsy diff --git a/spec/support/shared_examples/models/services_fields_shared_examples.rb b/spec/support/shared_examples/models/services_fields_shared_examples.rb deleted file mode 100644 index cb36f74460d..00000000000 --- a/spec/support/shared_examples/models/services_fields_shared_examples.rb +++ /dev/null @@ -1,31 +0,0 @@ -# frozen_string_literal: true - -RSpec.shared_examples 'issue tracker fields' do - let(:title) { 'custom title' } - let(:description) { 'custom description' } - let(:url) { 'http://issue_tracker.example.com' } - - context 'when data are stored in the properties' do - describe '#update' do - before do - service.update(title: 'new_title', description: 'new description') - end - - it 'removes title and description from properties' do - expect(service.reload.properties).not_to include('title', 'description') - end - - it 'stores title & description in services table' do - expect(service.read_attribute(:title)).to eq('new_title') - expect(service.read_attribute(:description)).to eq('new description') - end - end - - describe 'reading fields' do - it 'returns correct values' do - expect(service.title).to eq(title) - expect(service.description).to eq(description) - end - end - end -end diff --git a/spec/support/shared_examples/models/synthetic_note_shared_examples.rb b/spec/support/shared_examples/models/synthetic_note_shared_examples.rb new file mode 100644 index 00000000000..a41ade2950a --- /dev/null +++ b/spec/support/shared_examples/models/synthetic_note_shared_examples.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'a synthetic note' do |action| + it_behaves_like 'a system note', exclude_project: true do + let(:action) { action } + end + + describe '#discussion_id' do + before do + allow(event).to receive(:discussion_id).and_return('foobar42') + end + + it 'returns the expected discussion id' do + expect(subject.discussion_id(nil)).to eq('foobar42') + end + end +end diff --git a/spec/support/shared_examples/namespaces/hierarchy_examples.rb b/spec/support/shared_examples/namespaces/hierarchy_examples.rb new file mode 100644 index 00000000000..d5754f47be2 --- /dev/null +++ b/spec/support/shared_examples/namespaces/hierarchy_examples.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'hierarchy with traversal_ids' do + # A convenient null node to represent the parent of root. + let(:null_node) { double(traversal_ids: []) } + + # Walk the tree to assert that the current_node's traversal_id is always + # present and equal to it's parent's traversal_ids plus it's own ID. + def validate_traversal_ids(current_node, parent = null_node) + expect(current_node.traversal_ids).to be_present + expect(current_node.traversal_ids).to eq parent.traversal_ids + [current_node.id] + + current_node.children.each do |child| + validate_traversal_ids(child, current_node) + end + end + + it 'will be valid' do + validate_traversal_ids(root) + end +end diff --git a/spec/support/shared_examples/policies/namespace_policy_shared_examples.rb b/spec/support/shared_examples/policies/namespace_policy_shared_examples.rb new file mode 100644 index 00000000000..ddec1ba5e3d --- /dev/null +++ b/spec/support/shared_examples/policies/namespace_policy_shared_examples.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'update namespace limit policy' do + describe 'update_subscription_limit' do + using RSpec::Parameterized::TableSyntax + + let(:policy) { :update_subscription_limit } + + where(:role, :is_com, :allowed) do + :user | true | false + :owner | true | false + :admin | true | true + :user | false | false + :owner | false | false + :admin | false | false + end + + with_them do + let(:current_user) { build_stubbed(role) } + + before do + allow(Gitlab).to receive(:com?).and_return(is_com) + end + + context 'when admin mode enabled', :enable_admin_mode do + it { is_expected.to(allowed ? be_allowed(policy) : be_disallowed(policy)) } + end + + context 'when admin mode disabled' do + it { is_expected.to be_disallowed(policy) } + end + end + end +end 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 4dd0152e3d1..f8526ec68dc 100644 --- a/spec/support/shared_examples/policies/project_policy_shared_examples.rb +++ b/spec/support/shared_examples/policies/project_policy_shared_examples.rb @@ -41,6 +41,28 @@ RSpec.shared_examples 'archived project policies' do end end +RSpec.shared_examples 'project private features with read_all_resources ability' do + subject { described_class.new(user, project) } + + before do + project.project_feature.update!( + repository_access_level: ProjectFeature::PRIVATE, + merge_requests_access_level: ProjectFeature::PRIVATE, + builds_access_level: ProjectFeature::PRIVATE + ) + end + + [:public, :internal, :private].each do |visibility| + context "for #{visibility} projects" do + let(:project) { create(:project, visibility, namespace: owner.namespace) } + + it 'allows the download_code ability' do + expect_allowed(:download_code) + end + end + end +end + RSpec.shared_examples 'project policies as anonymous' do context 'abilities for public projects' do context 'when a project has pending invites' do @@ -231,6 +253,12 @@ RSpec.shared_examples 'project policies as admin with admin mode' do let(:regular_abilities) { owner_permissions } end end + + context 'abilities for all project visibility', :enable_admin_mode do + it_behaves_like 'project private features with read_all_resources ability' do + let(:user) { admin } + end + end end RSpec.shared_examples 'project policies as admin without admin mode' do diff --git a/spec/support/shared_examples/quick_actions/issue/create_merge_request_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issue/create_merge_request_quick_action_shared_examples.rb index 159660e7d1d..910805dbdea 100644 --- a/spec/support/shared_examples/quick_actions/issue/create_merge_request_quick_action_shared_examples.rb +++ b/spec/support/shared_examples/quick_actions/issue/create_merge_request_quick_action_shared_examples.rb @@ -47,7 +47,7 @@ RSpec.shared_examples 'create_merge_request quick action' do expect(created_mr.source_branch).to eq(issue.to_branch_name) visit project_merge_request_path(project, created_mr) - expect(page).to have_content %{WIP: Resolve "#{issue.title}"} + expect(page).to have_content %{Draft: Resolve "#{issue.title}"} end it 'creates a merge request using the given branch name' do @@ -60,7 +60,7 @@ RSpec.shared_examples 'create_merge_request quick action' do expect(created_mr.source_branch).to eq(branch_name) visit project_merge_request_path(project, created_mr) - expect(page).to have_content %{WIP: Resolve "#{issue.title}"} + expect(page).to have_content %{Draft: Resolve "#{issue.title}"} end end end diff --git a/spec/support/shared_examples/quick_actions/merge_request/merge_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/merge_request/merge_quick_action_shared_examples.rb index e0edbc5637a..258d9ab85e4 100644 --- a/spec/support/shared_examples/quick_actions/merge_request/merge_quick_action_shared_examples.rb +++ b/spec/support/shared_examples/quick_actions/merge_request/merge_quick_action_shared_examples.rb @@ -1,68 +1,85 @@ # frozen_string_literal: true RSpec.shared_examples 'merge quick action' do - context 'when the current user can merge the MR' do + context 'when updating the description' do before do sign_in(user) - visit project_merge_request_path(project, merge_request) + visit edit_project_merge_request_path(project, merge_request) end - it 'merges the MR', :sidekiq_might_not_need_inline do - add_note("/merge") - - expect(page).to have_content 'Merged this merge request.' + it 'merges the MR', :sidekiq_inline do + fill_in('Description', with: '/merge') + click_button('Save changes') + expect(page).to have_content('Merged') expect(merge_request.reload).to be_merged end + end - context 'when auto merge is avialable' do + context 'when creating a new note' do + context 'when the current user can merge the MR' do before do - create(:ci_pipeline, :detached_merge_request_pipeline, - project: project, merge_request: merge_request) - merge_request.update_head_pipeline + sign_in(user) + visit project_merge_request_path(project, merge_request) end - it 'schedules to merge the MR' do + it 'merges the MR', :sidekiq_inline do add_note("/merge") - expect(page).to have_content "Scheduled to merge this merge request (Merge when pipeline succeeds)." + expect(page).to have_content 'Merged this merge request.' - expect(merge_request.reload).to be_auto_merge_enabled - expect(merge_request.reload).not_to be_merged + expect(merge_request.reload).to be_merged end - end - end - context 'when the head diff changes in the meanwhile' do - before do - merge_request.source_branch = 'another_branch' - merge_request.save - sign_in(user) - visit project_merge_request_path(project, merge_request) - end + context 'when auto merge is available' do + before do + create(:ci_pipeline, :detached_merge_request_pipeline, + project: project, merge_request: merge_request) + merge_request.update_head_pipeline + end - it 'does not merge the MR' do - add_note("/merge") + it 'schedules to merge the MR' do + add_note("/merge") - expect(page).not_to have_content 'Your commands have been executed!' + expect(page).to have_content "Scheduled to merge this merge request (Merge when pipeline succeeds)." - expect(merge_request.reload).not_to be_merged + expect(merge_request.reload).to be_auto_merge_enabled + expect(merge_request.reload).not_to be_merged + end + end end - end - context 'when the current user cannot merge the MR' do - before do - project.add_guest(guest) - sign_in(guest) - visit project_merge_request_path(project, merge_request) + context 'when the head diff changes in the meanwhile' do + before do + merge_request.source_branch = 'another_branch' + merge_request.save + sign_in(user) + visit project_merge_request_path(project, merge_request) + end + + it 'does not merge the MR' do + add_note("/merge") + + expect(page).not_to have_content 'Your commands have been executed!' + + expect(merge_request.reload).not_to be_merged + end end - it 'does not merge the MR' do - add_note("/merge") + context 'when the current user cannot merge the MR' do + before do + project.add_guest(guest) + sign_in(guest) + visit project_merge_request_path(project, merge_request) + end + + it 'does not merge the MR' do + add_note("/merge") - expect(page).not_to have_content 'Your commands have been executed!' + expect(page).not_to have_content 'Your commands have been executed!' - expect(merge_request.reload).not_to be_merged + expect(merge_request.reload).not_to be_merged + end end end end diff --git a/spec/support/shared_examples/requests/api/composer_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/composer_packages_shared_examples.rb new file mode 100644 index 00000000000..5257980d7df --- /dev/null +++ b/spec/support/shared_examples/requests/api/composer_packages_shared_examples.rb @@ -0,0 +1,138 @@ +# frozen_string_literal: true + +RSpec.shared_context 'Composer user type' do |user_type, add_member| + before do + group.send("add_#{user_type}", user) if add_member && user_type != :anonymous + project.send("add_#{user_type}", user) if add_member && user_type != :anonymous + end +end + +RSpec.shared_examples 'Composer package index' do |user_type, status, add_member = true| + include_context 'Composer user type', user_type, add_member do + it 'returns the package index' do + subject + + expect(response).to have_gitlab_http_status(status) + expect(response).to match_response_schema('public_api/v4/packages/composer/index') + end + end +end + +RSpec.shared_examples 'Composer empty provider index' do |user_type, status, add_member = true| + include_context 'Composer user type', user_type, add_member do + it 'returns the package index' do + subject + + expect(response).to have_gitlab_http_status(status) + expect(response).to match_response_schema('public_api/v4/packages/composer/provider') + expect(json_response['providers']).to eq({}) + end + end +end + +RSpec.shared_examples 'Composer provider index' do |user_type, status, add_member = true| + include_context 'Composer user type', user_type, add_member do + it 'returns the package index' do + subject + + expect(response).to have_gitlab_http_status(status) + expect(response).to match_response_schema('public_api/v4/packages/composer/provider') + expect(json_response['providers']).to include(package.name) + end + end +end + +RSpec.shared_examples 'Composer package api request' do |user_type, status, add_member = true| + include_context 'Composer user type', user_type, add_member do + it 'returns the package index' do + subject + + expect(response).to have_gitlab_http_status(status) + expect(response).to match_response_schema('public_api/v4/packages/composer/package') + expect(json_response['packages']).to include(package.name) + expect(json_response['packages'][package.name]).to include(package.version) + end + end +end + +RSpec.shared_examples 'Composer package creation' 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 + project.send("add_#{user_type}", user) if add_member && user_type != :anonymous + end + + it 'creates package files' do + expect { subject } + .to change { project.packages.composer.count }.by(1) + + expect(response).to have_gitlab_http_status(status) + end + it_behaves_like 'a gitlab tracking event', described_class.name, 'register_package' + end +end + +RSpec.shared_examples 'process Composer api request' 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 + project.send("add_#{user_type}", user) if add_member && user_type != :anonymous + end + + it_behaves_like 'returning response status', status + end +end + +RSpec.shared_context 'Composer auth headers' do |user_role, user_token| + let(:token) { user_token ? personal_access_token.token : 'wrong' } + let(:headers) { user_role == :anonymous ? {} : build_basic_auth_header(user.username, token) } +end + +RSpec.shared_context 'Composer api project access' do |project_visibility_level, user_role, user_token| + include_context 'Composer auth headers', user_role, user_token do + before do + project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility_level, false)) + end + end +end + +RSpec.shared_context 'Composer api group access' do |project_visibility_level, user_role, user_token| + include_context 'Composer auth headers', user_role, user_token do + before do + project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility_level, false)) + group.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility_level, false)) + end + end +end + +RSpec.shared_examples 'rejects Composer 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 'process Composer api request', :anonymous, :not_found + end + + context 'as authenticated user' do + subject { get api(url), headers: build_basic_auth_header(user.username, personal_access_token.token) } + + it_behaves_like 'process Composer api request', :anonymous, :not_found + end + end +end + +RSpec.shared_examples 'rejects Composer access with unknown project id' do + context 'with an unknown project' do + let(:project) { double(id: non_existing_record_id) } + + context 'as anonymous' do + it_behaves_like 'process Composer api request', :anonymous, :not_found + end + + context 'as authenticated user' do + subject { get api(url), headers: build_basic_auth_header(user.username, personal_access_token.token) } + + it_behaves_like 'process Composer api request', :anonymous, :not_found + end + end +end diff --git a/spec/support/shared_examples/requests/api/graphql/projects/services_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/projects/services_shared_examples.rb index 246f1850c3c..da1caef63ba 100644 --- a/spec/support/shared_examples/requests/api/graphql/projects/services_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/graphql/projects/services_shared_examples.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -shared_examples 'unauthorized users cannot read services' do +RSpec.shared_examples 'unauthorized users cannot read services' do before do post_graphql(query, current_user: current_user) end diff --git a/spec/support/shared_examples/requests/api/notes_shared_examples.rb b/spec/support/shared_examples/requests/api/notes_shared_examples.rb index 60ed61269df..a34c48a5ba4 100644 --- a/spec/support/shared_examples/requests/api/notes_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/notes_shared_examples.rb @@ -132,6 +132,16 @@ RSpec.shared_examples 'noteable API' do |parent_type, noteable_type, id_name| expect(response).to have_gitlab_http_status(:created) expect(json_response['body']).to eq('hi!') + expect(json_response['confidential']).to be_falsey + expect(json_response['author']['username']).to eq(user.username) + end + + it "creates a confidential note if confidential is set to true" do + post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", user), params: { body: 'hi!', confidential: true } + + expect(response).to have_gitlab_http_status(:created) + expect(json_response['body']).to eq('hi!') + expect(json_response['confidential']).to be_truthy expect(json_response['author']['username']).to eq(user.username) end diff --git a/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb new file mode 100644 index 00000000000..8d8483cae72 --- /dev/null +++ b/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb @@ -0,0 +1,408 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'rejects nuget 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 + + if status == :unauthorized + it 'has the correct response header' do + subject + + expect(response.headers['Www-Authenticate: Basic realm']).to eq 'GitLab Packages Registry' + end + end + end +end + +RSpec.shared_examples 'process nuget service index request' 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 gitlab tracking event', described_class.name, 'nuget_service_index' + + it 'returns a valid json response' do + subject + + expect(response.media_type).to eq('application/json') + expect(json_response).to match_schema('public_api/v4/packages/nuget/service_index') + expect(json_response).to be_a(Hash) + end + + context 'with invalid format' do + let(:url) { "/projects/#{project.id}/packages/nuget/index.xls" } + + it_behaves_like 'rejects nuget packages access', :anonymous, :not_found + end + end +end + +RSpec.shared_examples 'returning nuget metadata json response with json schema' do |json_schema| + it 'returns a valid json response' do + subject + + expect(response.media_type).to eq('application/json') + expect(json_response).to match_schema(json_schema) + expect(json_response).to be_a(Hash) + end +end + +RSpec.shared_examples 'process nuget metadata request at package name level' 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 'returning nuget metadata json response with json schema', 'public_api/v4/packages/nuget/packages_metadata' + + context 'with invalid format' do + let(:url) { "/projects/#{project.id}/packages/nuget/metadata/#{package_name}/index.xls" } + + it_behaves_like 'rejects nuget packages access', :anonymous, :not_found + end + + context 'with lower case package name' do + let_it_be(:package_name) { 'dummy.package' } + + it_behaves_like 'returning response status', status + + it_behaves_like 'returning nuget metadata json response with json schema', 'public_api/v4/packages/nuget/packages_metadata' + end + end +end + +RSpec.shared_examples 'process nuget metadata request at package name and package version level' 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 'returning nuget metadata json response with json schema', 'public_api/v4/packages/nuget/package_metadata' + + context 'with invalid format' do + let(:url) { "/projects/#{project.id}/packages/nuget/metadata/#{package_name}/#{package.version}.xls" } + + it_behaves_like 'rejects nuget packages access', :anonymous, :not_found + end + + context 'with lower case package name' do + let_it_be(:package_name) { 'dummy.package' } + + it_behaves_like 'returning response status', status + + it_behaves_like 'returning nuget metadata json response with json schema', 'public_api/v4/packages/nuget/package_metadata' + end + end +end + +RSpec.shared_examples 'process nuget 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 + build_basic_auth_header(user.username, personal_access_token.token) + .merge(workhorse_header) + .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 nuget upload' do |user_type, status, add_member = true| + RSpec.shared_examples 'creates nuget package files' do + it 'creates package files' do + expect(::Packages::Nuget::ExtractionWorker).to receive(:perform_async).once + expect { subject } + .to change { project.packages.count }.by(1) + .and change { Packages::PackageFile.count }.by(1) + expect(response).to have_gitlab_http_status(status) + + package_file = project.packages.last.package_files.reload.last + expect(package_file.file_name).to eq('package.nupkg') + 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 nuget package files' + it_behaves_like 'a gitlab 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( + key: "tmp/uploads/#{file_name}", + body: 'content' + ) + end + let(:fog_file) { fog_to_uploaded_file(tmp_object) } + let(:params) { { package: fog_file, 'package.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 nuget package files' + + ['123123', '../../123123'].each do |remote_id| + context "with invalid remote_id: #{remote_id}" do + let(:params) do + { + package: fog_file, + 'package.remote_id' => remote_id + } + end + + it_behaves_like 'returning response status', :forbidden + end + end + + context 'with crafted package.path param' do + let(:crafted_file) { Tempfile.new('nuget.crafted.package.path') } + let(:url) { "/projects/#{project.id}/packages/nuget?package.path=#{crafted_file.path}" } + let(:params) { { file: temp_file(file_name) } } + let(:file_key) { :file } + + it 'does not create a package file' do + expect { subject }.to change { ::Packages::PackageFile.count }.by(0) + end + + it_behaves_like 'returning response status', :bad_request + 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 nuget 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 nuget package files' + end + end + end + + it_behaves_like 'background upload schedules a file migration' + end +end + +RSpec.shared_examples 'process nuget download versions request' do |user_type, status, add_member = true| + RSpec.shared_examples 'returns a valid nuget download versions json response' do + it 'returns a valid json response' do + subject + + expect(response.media_type).to eq('application/json') + expect(json_response).to match_schema('public_api/v4/packages/nuget/download_versions') + expect(json_response).to be_a(Hash) + expect(json_response['versions']).to match_array(packages.map(&:version).sort) + end + end + + 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 'returns a valid nuget download versions json response' + + context 'with invalid format' do + let(:url) { "/projects/#{project.id}/packages/nuget/download/#{package_name}/index.xls" } + + it_behaves_like 'rejects nuget packages access', :anonymous, :not_found + end + + context 'with lower case package name' do + let_it_be(:package_name) { 'dummy.package' } + + it_behaves_like 'returning response status', status + + it_behaves_like 'returns a valid nuget download versions json response' + end + end +end + +RSpec.shared_examples 'process nuget download content request' 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 gitlab tracking event', described_class.name, 'pull_package' + + it 'returns a valid package archive' do + subject + + expect(response.media_type).to eq('application/octet-stream') + end + + context 'with invalid format' do + let(:url) { "/projects/#{project.id}/packages/nuget/download/#{package.name}/#{package.version}/#{package.name}.#{package.version}.xls" } + + it_behaves_like 'rejects nuget packages access', :anonymous, :not_found + end + + context 'with lower case package name' do + let_it_be(:package_name) { 'dummy.package' } + + it_behaves_like 'returning response status', status + + it 'returns a valid package archive' do + subject + + expect(response.media_type).to eq('application/octet-stream') + end + end + end +end + +RSpec.shared_examples 'process nuget search request' do |user_type, status, add_member = true| + RSpec.shared_examples 'returns a valid json search response' do |status, total_hits, versions| + it_behaves_like 'returning response status', status + + it 'returns a valid json response' do + subject + + expect(response.media_type).to eq('application/json') + expect(json_response).to be_a(Hash) + expect(json_response).to match_schema('public_api/v4/packages/nuget/search') + expect(json_response['totalHits']).to eq total_hits + expect(json_response['data'].map { |e| e['versions'].size }).to match_array(versions) + end + end + + 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 'returns a valid json search response', status, 4, [1, 5, 5, 1] + + it_behaves_like 'a gitlab tracking event', described_class.name, 'search_package' + + context 'with skip set to 2' do + let(:skip) { 2 } + + it_behaves_like 'returns a valid json search response', status, 4, [5, 1] + end + + context 'with take set to 2' do + let(:take) { 2 } + + it_behaves_like 'returns a valid json search response', status, 4, [1, 5] + end + + context 'without prereleases' do + let(:include_prereleases) { false } + + it_behaves_like 'returns a valid json search response', status, 3, [1, 5, 5] + end + + context 'with empty search term' do + let(:search_term) { '' } + + it_behaves_like 'returns a valid json search response', status, 5, [1, 5, 5, 1, 1] + end + + context 'with nil search term' do + let(:search_term) { nil } + + it_behaves_like 'returns a valid json search response', status, 5, [1, 5, 5, 1, 1] + end + end +end + +RSpec.shared_examples 'rejects nuget access with invalid project id' do + context 'with a project id with invalid integers' do + using RSpec::Parameterized::TableSyntax + + let(:project) { OpenStruct.new(id: id) } + + where(:id, :status) do + '/../' | :unauthorized + '' | :not_found + '%20' | :unauthorized + '%2e%2e%2f' | :unauthorized + 'NaN' | :unauthorized + 00002345 | :unauthorized + 'anything25' | :unauthorized + end + + with_them do + it_behaves_like 'rejects nuget packages access', :anonymous, params[:status] + end + end +end + +RSpec.shared_examples 'rejects nuget access with unknown project id' do + context 'with an unknown project' do + let(:project) { OpenStruct.new(id: 1234567890) } + + context 'as anonymous' do + it_behaves_like 'rejects nuget packages access', :anonymous, :unauthorized + end + + context 'as authenticated user' do + subject { get api(url), headers: build_basic_auth_header(user.username, personal_access_token.token) } + + it_behaves_like 'rejects nuget packages access', :anonymous, :not_found + end + end +end diff --git a/spec/support/shared_examples/requests/api/packages_shared_examples.rb b/spec/support/shared_examples/requests/api/packages_shared_examples.rb new file mode 100644 index 00000000000..ec15d7a4d2e --- /dev/null +++ b/spec/support/shared_examples/requests/api/packages_shared_examples.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'deploy token for package GET requests' do + context 'with deploy token headers' do + let(:headers) { build_basic_auth_header(deploy_token.username, deploy_token.token) } + + subject { get api(url), headers: headers } + + before do + project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE) + end + + context 'valid token' do + it_behaves_like 'returning response status', :success + end + + context 'invalid token' do + let(:headers) { build_basic_auth_header(deploy_token.username, 'bar') } + + it_behaves_like 'returning response status', :unauthorized + end + end +end + +RSpec.shared_examples 'deploy token for package uploads' do + context 'with deploy token headers' do + let(:headers) { build_basic_auth_header(deploy_token.username, deploy_token.token).merge(workhorse_header) } + + before do + project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE) + end + + context 'valid token' do + it_behaves_like 'returning response status', :success + end + + context 'invalid token' do + let(:headers) { build_basic_auth_header(deploy_token.username, 'bar').merge(workhorse_header) } + + it_behaves_like 'returning response status', :unauthorized + end + end +end diff --git a/spec/support/shared_examples/requests/api/packages_tags_shared_examples.rb b/spec/support/shared_examples/requests/api/packages_tags_shared_examples.rb new file mode 100644 index 00000000000..a371d380f47 --- /dev/null +++ b/spec/support/shared_examples/requests/api/packages_tags_shared_examples.rb @@ -0,0 +1,185 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'rejects package tags access' do |user_type, status| + context "for user type #{user_type}" do + before do + project.send("add_#{user_type}", user) unless user_type == :no_type + end + + it_behaves_like 'returning response status', status + end +end + +RSpec.shared_examples 'returns package tags' do |user_type| + using RSpec::Parameterized::TableSyntax + + before do + stub_application_setting(npm_package_requests_forwarding: false) + project.send("add_#{user_type}", user) unless user_type == :no_type + end + + it_behaves_like 'returning response status', :success + + it 'returns a valid json response' do + subject + + expect(response.media_type).to eq('application/json') + expect(json_response).to be_a(Hash) + end + + it 'returns two package tags' do + subject + + expect(json_response).to match_schema('public_api/v4/packages/npm_package_tags') + expect(json_response.length).to eq(3) # two tags + latest (auto added) + expect(json_response[package_tag1.name]).to eq(package.version) + expect(json_response[package_tag2.name]).to eq(package.version) + expect(json_response['latest']).to eq(package.version) + end + + context 'with invalid package name' do + where(:package_name, :status) do + '%20' | :bad_request + nil | :forbidden + end + + with_them do + it_behaves_like 'returning response status', params[:status] + end + end +end + +RSpec.shared_examples 'create package tag' do |user_type| + using RSpec::Parameterized::TableSyntax + + before do + project.send("add_#{user_type}", user) unless user_type == :no_type + end + + it_behaves_like 'returning response status', :no_content + + it 'creates the package tag' do + expect { subject }.to change { Packages::Tag.count }.by(1) + + last_tag = Packages::Tag.last + expect(last_tag.name).to eq(tag_name) + expect(last_tag.package).to eq(package) + end + + it 'returns a valid response' do + subject + + expect(response.body).to be_empty + end + + context 'with already existing tag' do + let_it_be(:package2) { create(:npm_package, project: project, name: package.name, version: '5.5.55') } + let_it_be(:tag) { create(:packages_tag, package: package2, name: tag_name) } + + it_behaves_like 'returning response status', :no_content + + it 'reuses existing tag' do + expect(package.tags).to be_empty + expect(package2.tags).to eq([tag]) + expect { subject }.to not_change { Packages::Tag.count } + expect(package.reload.tags).to eq([tag]) + expect(package2.reload.tags).to be_empty + end + + it 'returns a valid response' do + subject + + expect(response.body).to be_empty + end + end + + context 'with invalid package name' do + where(:package_name, :status) do + 'unknown' | :forbidden + '' | :not_found + '%20' | :bad_request + end + + with_them do + it_behaves_like 'returning response status', params[:status] + end + end + + context 'with invalid tag name' do + where(:tag_name, :status) do + '' | :not_found + '%20' | :bad_request + end + + with_them do + it_behaves_like 'returning response status', params[:status] + end + end + + context 'with invalid version' do + where(:version, :status) do + ' ' | :bad_request + '' | :bad_request + nil | :bad_request + end + + with_them do + it_behaves_like 'returning response status', params[:status] + end + end +end + +RSpec.shared_examples 'delete package tag' do |user_type| + using RSpec::Parameterized::TableSyntax + + before do + project.send("add_#{user_type}", user) unless user_type == :no_type + end + + context "for #{user_type} user" do + it_behaves_like 'returning response status', :no_content + + it 'returns a valid response' do + subject + + expect(response.body).to be_empty + end + + it 'destroy the package tag' do + expect(package.tags).to eq([package_tag]) + expect { subject }.to change { Packages::Tag.count }.by(-1) + expect(package.reload.tags).to be_empty + end + + context 'with tag from other package' do + let(:package2) { create(:npm_package, project: project) } + let(:package_tag) { create(:packages_tag, package: package2) } + + it_behaves_like 'returning response status', :not_found + end + + context 'with invalid package name' do + where(:package_name, :status) do + 'unknown' | :forbidden + '' | :not_found + '%20' | :bad_request + end + + with_them do + it_behaves_like 'returning response status', params[:status] + end + end + + context 'with invalid tag name' do + where(:tag_name, :status) do + 'unknown' | :not_found + '' | :not_found + '%20' | :bad_request + end + + with_them do + it_behaves_like 'returning response status', params[:status] + end + end + end +end diff --git a/spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb new file mode 100644 index 00000000000..fcc166ac87d --- /dev/null +++ b/spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb @@ -0,0 +1,152 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'PyPi package creation' do |user_type, status, add_member = true| + RSpec.shared_examples 'creating pypi package files' do + it 'creates package files' do + expect { subject } + .to change { project.packages.pypi.count }.by(1) + .and change { Packages::PackageFile.count }.by(1) + .and change { Packages::Pypi::Metadatum.count }.by(1) + expect(response).to have_gitlab_http_status(status) + + package = project.reload.packages.pypi.last + + expect(package.name).to eq params[:name] + expect(package.version).to eq params[:version] + expect(package.pypi_metadatum.required_python).to eq params[:requires_python] + end + end + + 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 'creating pypi package files' + + 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 'creating pypi package files' + it_behaves_like 'a gitlab 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( + key: "tmp/uploads/#{file_name}", + body: 'content' + ) + end + let(:fog_file) { fog_to_uploaded_file(tmp_object) } + let(:params) { base_params.merge(content: fog_file, 'content.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 'creating pypi package files' + + ['123123', '../../123123'].each do |remote_id| + context "with invalid remote_id: #{remote_id}" do + let(:params) { base_params.merge(content: fog_file, 'content.remote_id' => remote_id) } + + 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 'creating pypi 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 'creating pypi package files' + end + end + end + + it_behaves_like 'background upload schedules a file migration' + end +end + +RSpec.shared_examples 'PyPi package versions' 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 'returns the package listing' do + subject + + expect(response.body).to match(package.package_files.first.file_name) + end + + it_behaves_like 'returning response status', status + it_behaves_like 'a gitlab tracking event', described_class.name, 'list_package' + end +end + +RSpec.shared_examples 'PyPi package download' 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 'returns the package listing' do + subject + + expect(response.body).to eq(File.open(package.package_files.first.file.path, "rb").read) + end + + it_behaves_like 'returning response status', status + it_behaves_like 'a gitlab tracking event', described_class.name, 'pull_package' + end +end + +RSpec.shared_examples 'process PyPi api request' 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 'rejects PyPI access with unknown project id' do + context 'with an unknown project' do + let(:project) { OpenStruct.new(id: 1234567890) } + + context 'as anonymous' do + it_behaves_like 'process PyPi api request', :anonymous, :not_found + end + + context 'as authenticated user' do + subject { get api(url), headers: build_basic_auth_header(user.username, personal_access_token.token) } + + it_behaves_like 'process PyPi api request', :anonymous, :not_found + end + end +end diff --git a/spec/support/shared_examples/requests/api/resource_milestone_events_api_shared_examples.rb b/spec/support/shared_examples/requests/api/resource_milestone_events_api_shared_examples.rb index bca51dab353..d21a9f419fd 100644 --- a/spec/support/shared_examples/requests/api/resource_milestone_events_api_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/resource_milestone_events_api_shared_examples.rb @@ -16,6 +16,29 @@ RSpec.shared_examples 'resource_milestone_events API' do |parent_type, eventable expect(json_response.first['action']).to eq(event.action) end + context 'when there is an event with a milestone which is not visible for requesting user' do + let!(:private_project) { create(:project, :private) } + let!(:private_milestone) { create(:milestone, project: private_project) } + + let!(:other_user) { create(:user) } + + it 'returns the expected events' do + create_event(private_milestone) + + get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_milestone_events", other_user) + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to include_pagination_headers + expect(response.headers['X-Total']).to eq('1') + expect(json_response).to be_an Array + expect(json_response.size).to eq(1) + + expect(json_response.first['id']).to eq(event.id) + expect(json_response.first['milestone']['id']).to eq(event.milestone.id) + expect(json_response.first['action']).to eq(event.action) + end + end + it "returns a 404 error when eventable id not found" do get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{non_existing_record_id}/resource_milestone_events", user) @@ -60,6 +83,20 @@ RSpec.shared_examples 'resource_milestone_events API' do |parent_type, eventable end end + describe 'pagination' do + let!(:event1) { create_event(milestone) } + let!(:event2) { create_event(milestone) } + + # https://gitlab.com/gitlab-org/gitlab/-/issues/220192 + it 'returns the second page' do + get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_milestone_events?page=2&per_page=1", user) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response.count).to eq(1) + expect(json_response.first['id']).to eq(event2.id) + end + end + def create_event(milestone, action: :add) create(:resource_milestone_event, eventable.class.name.underscore => eventable, milestone: milestone, action: action) end diff --git a/spec/support/shared_examples/requests/api/snippets_shared_examples.rb b/spec/support/shared_examples/requests/api/snippets_shared_examples.rb new file mode 100644 index 00000000000..cfbb84dd099 --- /dev/null +++ b/spec/support/shared_examples/requests/api/snippets_shared_examples.rb @@ -0,0 +1,79 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'raw snippet files' do + let_it_be(:unauthorized_user) { create(:user) } + let(:snippet_id) { snippet.id } + let(:user) { snippet.author } + let(:file_path) { '%2Egitattributes' } + let(:ref) { 'master' } + + context 'with no user' do + it 'requires authentication' do + get api(api_path) + + expect(response).to have_gitlab_http_status(:unauthorized) + end + end + + shared_examples 'not found' do + it 'returns 404' do + get api(api_path, user) + + expect(response).to have_gitlab_http_status(:not_found) + expect(json_response['message']).to eq('404 Snippet Not Found') + end + end + + context 'when not authorized' do + let(:user) { unauthorized_user } + + it_behaves_like 'not found' + end + + context 'with an invalid snippet ID' do + let(:snippet_id) { 'invalid' } + + it_behaves_like 'not found' + end + + context 'with valid params' do + it 'returns the raw file info' do + expect(Gitlab::Workhorse).to receive(:send_git_blob).and_call_original + + get api(api_path, user) + + aggregate_failures do + expect(response).to have_gitlab_http_status(:ok) + expect(response.media_type).to eq 'text/plain' + expect(response.header[Gitlab::Workhorse::DETECT_HEADER]).to eq 'true' + expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with('git-blob:') + expect(response.header['Content-Disposition']).to match 'filename=".gitattributes"' + end + end + end + + context 'with invalid params' do + using RSpec::Parameterized::TableSyntax + + where(:file_path, :ref, :status, :key, :message) do + '%2Egitattributes' | 'invalid-ref' | :not_found | 'message' | '404 Reference Not Found' + '%2Egitattributes' | nil | :not_found | 'error' | '404 Not Found' + '%2Egitattributes' | '' | :not_found | 'error' | '404 Not Found' + + 'doesnotexist.rb' | 'master' | :not_found | 'message' | '404 File Not Found' + '/does/not/exist.rb' | 'master' | :not_found | 'error' | '404 Not Found' + '%2E%2E%2Fetc%2Fpasswd' | 'master' | :bad_request | 'error' | 'file_path should be a valid file path' + '%2Fetc%2Fpasswd' | 'master' | :bad_request | 'error' | 'file_path should be a valid file path' + '../../etc/passwd' | 'master' | :not_found | 'error' | '404 Not Found' + end + + with_them do + before do + get api(api_path, user) + end + + it { expect(response).to have_gitlab_http_status(status) } + it { expect(json_response[key]).to eq(message) } + end + end +end diff --git a/spec/support/shared_examples/requests/snippet_shared_examples.rb b/spec/support/shared_examples/requests/snippet_shared_examples.rb index f830f957174..644abb191a6 100644 --- a/spec/support/shared_examples/requests/snippet_shared_examples.rb +++ b/spec/support/shared_examples/requests/snippet_shared_examples.rb @@ -74,18 +74,14 @@ RSpec.shared_examples 'update with repository actions' do end end -RSpec.shared_examples 'snippet response without repository URLs' do - it 'skip inclusion of repository URLs' do - expect(json_response).not_to have_key('ssh_url_to_repo') - expect(json_response).not_to have_key('http_url_to_repo') - end -end - RSpec.shared_examples 'snippet blob content' do it 'returns content from repository' do + expect(Gitlab::Workhorse).to receive(:send_git_blob).and_call_original + subject - expect(response.body).to eq(snippet.blobs.first.data) + expect(response.header[Gitlab::Workhorse::DETECT_HEADER]).to eq 'true' + expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with('git-blob:') end context 'when snippet repository is empty' do @@ -98,3 +94,15 @@ RSpec.shared_examples 'snippet blob content' do end end end + +RSpec.shared_examples 'snippet_multiple_files feature disabled' do + before do + stub_feature_flags(snippet_multiple_files: false) + + subject + end + + it 'does not return files attributes' do + expect(json_response).not_to have_key('files') + end +end diff --git a/spec/support/shared_examples/resource_events.rb b/spec/support/shared_examples/resource_events.rb index 66f5e760c37..c0158f9b24b 100644 --- a/spec/support/shared_examples/resource_events.rb +++ b/spec/support/shared_examples/resource_events.rb @@ -2,7 +2,7 @@ require 'spec_helper' -shared_examples 'a resource event' do +RSpec.shared_examples 'a resource event' do let_it_be(:user1) { create(:user) } let_it_be(:user2) { create(:user) } @@ -54,7 +54,7 @@ shared_examples 'a resource event' do end end -shared_examples 'a resource event for issues' do +RSpec.shared_examples 'a resource event for issues' do let_it_be(:user1) { create(:user) } let_it_be(:user2) { create(:user) } @@ -101,9 +101,19 @@ shared_examples 'a resource event for issues' do expect(events).to be_empty end end + + if described_class.method_defined?(:issuable) + describe '#issuable' do + let_it_be(:event1) { create(described_class.name.underscore.to_sym, issue: issue2) } + + it 'returns the expected issuable' do + expect(event1.issuable).to eq(issue2) + end + end + end end -shared_examples 'a resource event for merge requests' do +RSpec.shared_examples 'a resource event for merge requests' do let_it_be(:user1) { create(:user) } let_it_be(:user2) { create(:user) } @@ -132,4 +142,14 @@ shared_examples 'a resource event for merge requests' do expect(events).to be_empty end end + + if described_class.method_defined?(:issuable) + describe '#issuable' do + let_it_be(:event1) { create(described_class.name.underscore.to_sym, merge_request: merge_request2) } + + it 'returns the expected issuable' do + expect(event1.issuable).to eq(merge_request2) + end + end + end end diff --git a/spec/support/shared_examples/routing/resource_routing_shared_examples.rb b/spec/support/shared_examples/routing/resource_routing_shared_examples.rb new file mode 100644 index 00000000000..b98901a57ea --- /dev/null +++ b/spec/support/shared_examples/routing/resource_routing_shared_examples.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true + +# Shared examples for resource routes. +# +# By default it tests all the default REST actions: index, create, new, edit, +# show, update, and destroy. You can remove actions by customizing the +# `actions` variable. +# +# The subject is expected to be an instance of the controller under test. +# +# It also expects a `base_path` variable to be available which defines the +# base path of the controller, and a `base_params` variable which +# defines the route params the base path maps to. +# +# Examples +# +# # Default behavior +# describe Projects::CommitsController, 'routing' do +# it_behaves_like 'resource routing' do +# let(:base_path) { '/gitlab/gitlabhq/-/commits' } +# let(:base_params) { { namespace_id: 'gitlab', project_id: 'gitlabhq' } } +# end +# end +# +# # Customizing actions +# it_behaves_like 'resource routing' do +# let(:base_path) { '/gitlab/gitlabhq/-/commits' } +# +# # Specify default actions +# let(:actions) { [:index] } +# +# # Add custom actions by passing a hash with action names +# # as keys, and the HTTP method and path as values. +# let(:additional_actions) do +# { +# preview_markdown: [:post, '/:id/preview_markdown'], +# } +# end +# end +RSpec.shared_examples 'resource routing' do + let(:controller) { described_class.controller_path } + let(:id) { '123' } + + let(:default_actions) do + { + index: [:get, ''], + show: [:get, '/:id'], + new: [:get, '/new'], + create: [:post, ''], + edit: [:get, '/:id/edit'], + update: [:put, '/:id'], + destroy: [:delete, '/:id'] + } + end + + let(:actions) { default_actions.keys } + let(:additional_actions) { {} } + + it 'routes resource actions', :aggregate_failures do + selected_actions = default_actions.slice(*actions).merge(additional_actions) + + selected_actions.each do |action, (method, action_path)| + expected_params = base_params.merge(controller: controller.to_s, action: action.to_s) + + if action_path.include?(':id') + action_path = action_path.sub(':id', id) + expected_params[:id] = id + end + + expect(public_send(method, "#{base_path}#{action_path}")).to route_to(expected_params) + end + end +end diff --git a/spec/support/shared_examples/routing/wiki_routing_shared_examples.rb b/spec/support/shared_examples/routing/wiki_routing_shared_examples.rb new file mode 100644 index 00000000000..9289934677e --- /dev/null +++ b/spec/support/shared_examples/routing/wiki_routing_shared_examples.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'wiki routing' do + it_behaves_like 'resource routing' do + let(:id) { 'directory/page' } + let(:actions) { %i[show new create edit update destroy] } + let(:additional_actions) do + { + pages: [:get, '/pages'], + history: [:get, '/:id/history'], + git_access: [:get, '/git_access'], + preview_markdown: [:post, '/:id/preview_markdown'] + } + end + end + + it 'redirects the base path to the home page', type: :request do + expect(get(base_path)).to redirect_to("#{base_path}/home") + 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 new file mode 100644 index 00000000000..a1354a8099b --- /dev/null +++ b/spec/support/shared_examples/services/alert_management_shared_examples.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'creates an alert management alert' do + it { is_expected.to be_success } + + it 'creates AlertManagement::Alert' do + expect { subject }.to change(AlertManagement::Alert, :count).by(1) + end + + it 'executes the alert service hooks' do + slack_service = create(:service, type: 'SlackService', project: project, alert_events: true, active: true) + + subject + + expect(ProjectServiceWorker).to have_received(:perform_async).with(slack_service.id, an_instance_of(Hash)) + 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) + 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 +end diff --git a/spec/support/shared_examples/services/clusters/parse_cluster_applications_artifact_shared_examples.rb b/spec/support/shared_examples/services/clusters/parse_cluster_applications_artifact_shared_examples.rb new file mode 100644 index 00000000000..cbe20928f98 --- /dev/null +++ b/spec/support/shared_examples/services/clusters/parse_cluster_applications_artifact_shared_examples.rb @@ -0,0 +1,89 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'parse cluster applications artifact' do |release_name| + let(:application_class) { Clusters::Cluster::APPLICATIONS[release_name] } + let(:cluster_application) { cluster.public_send("application_#{release_name}") } + let(:file) { fixture_file_upload(Rails.root.join(fixture)) } + let(:artifact) { create(:ci_job_artifact, :cluster_applications, job: job, file: file) } + + context 'release is missing' do + let(:fixture) { "spec/fixtures/helm/helm_list_v2_#{release_name}_missing.json.gz" } + + context 'application does not exist' do + it 'does not create or destroy an application' do + expect do + described_class.new(job, user).execute(artifact) + end.not_to change(application_class, :count) + end + end + + context 'application exists' do + before do + create("clusters_applications_#{release_name}".to_sym, :installed, cluster: cluster) + end + + it 'marks the application as uninstalled' do + described_class.new(job, user).execute(artifact) + + cluster_application.reload + expect(cluster_application).to be_uninstalled + end + end + end + + context 'release is deployed' do + let(:fixture) { "spec/fixtures/helm/helm_list_v2_#{release_name}_deployed.json.gz" } + + context 'application does not exist' do + it 'creates an application and marks it as installed' do + expect do + described_class.new(job, user).execute(artifact) + end.to change(application_class, :count) + + expect(cluster_application).to be_persisted + expect(cluster_application).to be_installed + end + end + + context 'application exists' do + before do + create("clusters_applications_#{release_name}".to_sym, :errored, cluster: cluster) + end + + it 'marks the application as installed' do + described_class.new(job, user).execute(artifact) + + expect(cluster_application).to be_installed + end + end + end + + context 'release is failed' do + let(:fixture) { "spec/fixtures/helm/helm_list_v2_#{release_name}_failed.json.gz" } + + context 'application does not exist' do + it 'creates an application and marks it as errored' do + expect do + described_class.new(job, user).execute(artifact) + end.to change(application_class, :count) + + expect(cluster_application).to be_persisted + expect(cluster_application).to be_errored + expect(cluster_application.status_reason).to eq('Helm release failed to install') + end + end + + context 'application exists' do + before do + create("clusters_applications_#{release_name}".to_sym, :installed, cluster: cluster) + end + + it 'marks the application as errored' do + described_class.new(job, user).execute(artifact) + + expect(cluster_application).to be_errored + expect(cluster_application.status_reason).to eq('Helm release failed to install') + end + end + 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 4ce3e32d774..20856b05de6 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 @@ -17,10 +17,10 @@ RSpec.shared_examples 'system note creation' do |update_params, note_text| end end -RSpec.shared_examples 'WIP notes creation' do |wip_action| +RSpec.shared_examples 'draft notes creation' do |wip_action| subject { described_class.new(project, user).execute(issuable, old_labels: []) } - it 'creates WIP toggle and title change notes' do + it 'creates Draft toggle and title change notes' do expect { subject }.to change { Note.count }.from(0).to(2) expect(Note.first.note).to match("#{wip_action} as a **Work In Progress**") diff --git a/spec/support/shared_examples/services/jira_import/start_import_service_shared_examples.rb b/spec/support/shared_examples/services/jira_import/start_import_service_shared_examples.rb index c5e56ed3539..8fd76f7cb1f 100644 --- a/spec/support/shared_examples/services/jira_import/start_import_service_shared_examples.rb +++ b/spec/support/shared_examples/services/jira_import/start_import_service_shared_examples.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -shared_examples 'responds with error' do |message| +RSpec.shared_examples 'responds with error' do |message| it 'returns error' do expect(subject).to be_a(ServiceResponse) expect(subject).to be_error 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 5dd1badbefc..c8fabfe30b9 100644 --- a/spec/support/shared_examples/services/metrics/dashboard_shared_examples.rb +++ b/spec/support/shared_examples/services/metrics/dashboard_shared_examples.rb @@ -29,14 +29,43 @@ RSpec.shared_examples 'valid dashboard service response' do end RSpec.shared_examples 'caches the unprocessed dashboard for subsequent calls' do - it do - expect(YAML).to receive(:safe_load).once.and_call_original + specify do + expect_next_instance_of(::Gitlab::Config::Loader::Yaml) do |loader| + expect(loader).to receive(:load_raw!).once.and_call_original + end described_class.new(*service_params).get_dashboard described_class.new(*service_params).get_dashboard end end +# This spec is applicable for predefined/out-of-the-box dashboard services. +RSpec.shared_examples 'refreshes cache when dashboard_version is changed' do + specify do + allow_next_instance_of(described_class) do |service| + allow(service).to receive(:dashboard_version).and_return('1', '2') + end + + expect(File).to receive(:read).twice.and_call_original + + service = described_class.new(*service_params) + + service.get_dashboard + service.get_dashboard + end +end + +# This spec is applicable for predefined/out-of-the-box dashboard services. +# This shared_example requires the following variables to be defined: +# dashboard_path: Relative path to the dashboard, ex: 'config/prometheus/common_metrics.yml' +# dashboard_version: The version string used in the cache_key. +RSpec.shared_examples 'dashboard_version contains SHA256 hash of dashboard file content' do + specify do + dashboard = File.read(Rails.root.join(dashboard_path)) + expect(Digest::SHA256.hexdigest(dashboard)).to eq(dashboard_version) + end +end + RSpec.shared_examples 'valid embedded dashboard service response' do let(:dashboard_schema) { Gitlab::Json.parse(fixture_file('lib/gitlab/metrics/dashboard/schemas/embedded_dashboard.json')) } @@ -128,3 +157,50 @@ RSpec.shared_examples 'updates gitlab_metrics_dashboard_processing_time_ms metri expect(metric.get(labels)).to be > 0 end end + +RSpec.shared_examples '#raw_dashboard raises error if dashboard loading fails' do + context 'when yaml is too large' do + before do + allow_next_instance_of(::Gitlab::Config::Loader::Yaml) do |loader| + allow(loader).to receive(:load_raw!) + .and_raise(Gitlab::Config::Loader::Yaml::DataTooLargeError, 'The parsed YAML is too big') + end + end + + it 'raises error' do + expect { subject.raw_dashboard }.to raise_error( + Gitlab::Metrics::Dashboard::Errors::LayoutError, + 'The parsed YAML is too big' + ) + end + end + + context 'when yaml loader returns error' do + before do + allow_next_instance_of(::Gitlab::Config::Loader::Yaml) do |loader| + allow(loader).to receive(:load_raw!) + .and_raise(Gitlab::Config::Loader::FormatError, 'Invalid configuration format') + end + end + + it 'raises error' do + expect { subject.raw_dashboard }.to raise_error( + Gitlab::Metrics::Dashboard::Errors::LayoutError, + 'Invalid yaml' + ) + end + end + + context 'when yaml is not a hash' do + before do + allow_next_instance_of(::Gitlab::Config::Loader::Yaml) do |loader| + allow(loader).to receive(:load_raw!) + .and_raise(Gitlab::Config::Loader::Yaml::NotHashError, 'Invalid configuration format') + end + end + + it 'returns nil' do + expect(subject.raw_dashboard).to eq({}) + end + end +end diff --git a/spec/support/shared_examples/services/packages_shared_examples.rb b/spec/support/shared_examples/services/packages_shared_examples.rb new file mode 100644 index 00000000000..45a4c2bb151 --- /dev/null +++ b/spec/support/shared_examples/services/packages_shared_examples.rb @@ -0,0 +1,193 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'assigns build to package' do + context 'with build info' do + let(:job) { create(:ci_build, user: user) } + let(:params) { super().merge(build: job) } + + it 'assigns the pipeline to the package' do + package = subject + + expect(package.build_info).to be_present + expect(package.build_info.pipeline).to eq job.pipeline + end + end +end + +RSpec.shared_examples 'returns packages' do |container_type, user_type| + context "for #{user_type}" do + before do + send(container_type)&.send("add_#{user_type}", user) unless user_type == :no_type + end + + it 'returns success response' do + subject + + expect(response).to have_gitlab_http_status(:success) + end + + it 'returns a valid response schema' do + subject + + expect(response).to match_response_schema(package_schema) + end + + it 'returns two packages' do + subject + + expect(json_response.length).to eq(2) + expect(json_response.map { |package| package['id'] }).to contain_exactly(package1.id, package2.id) + end + end +end + +RSpec.shared_examples 'returns packages with subgroups' do |container_type, user_type| + context "with subgroups for #{user_type}" do + before do + send(container_type)&.send("add_#{user_type}", user) unless user_type == :no_type + end + + it 'returns success response' do + subject + + expect(response).to have_gitlab_http_status(:success) + end + + it 'returns a valid response schema' do + subject + + expect(response).to match_response_schema(package_schema) + end + + it 'returns three packages' do + subject + + expect(json_response.length).to eq(3) + expect(json_response.map { |package| package['id'] }).to contain_exactly(package1.id, package2.id, package3.id) + end + end +end + +RSpec.shared_examples 'package sorting' do |order_by| + subject { get api(url), params: { sort: sort, order_by: order_by } } + + context "sorting by #{order_by}" do + context 'ascending order' do + let(:sort) { 'asc' } + + it 'returns the sorted packages' do + subject + + expect(json_response.map { |package| package['id'] }).to eq(packages.map(&:id)) + end + end + + context 'descending order' do + let(:sort) { 'desc' } + + it 'returns the sorted packages' do + subject + + expect(json_response.map { |package| package['id'] }).to eq(packages.reverse.map(&:id)) + end + end + end +end + +RSpec.shared_examples 'rejects packages access' do |container_type, user_type, status| + context "for #{user_type}" do + before do + send(container_type)&.send("add_#{user_type}", user) unless user_type == :no_type + end + + it_behaves_like 'returning response status', status + end +end + +RSpec.shared_examples 'returns paginated packages' do + let(:per_page) { 2 } + + context 'when viewing the first page' do + let(:page) { 1 } + + it 'returns first 2 packages' do + get api(url, user), params: { page: page, per_page: per_page } + + expect_paginated_array_response([package1.id, package2.id]) + end + end + + context 'when viewing the second page' do + let(:page) { 2 } + + it 'returns first 2 packages' do + get api(url, user), params: { page: page, per_page: per_page } + + expect_paginated_array_response([package3.id, package4.id]) + end + end +end + +RSpec.shared_examples 'background upload schedules a file migration' do + context 'background upload enabled' do + before do + stub_package_file_object_storage(background_upload: true) + end + + it 'schedules migration of file to object storage' do + expect(ObjectStorage::BackgroundMoveWorker).to receive(:perform_async).with('Packages::PackageFileUploader', 'Packages::PackageFile', :file, kind_of(Numeric)) + + subject + end + end +end + +RSpec.shared_context 'package filter context' do + def package_filter_url(filter, param) + "/projects/#{project.id}/packages?package_#{filter}=#{param}" + end + + def group_filter_url(filter, param) + "/groups/#{group.id}/packages?package_#{filter}=#{param}" + end +end + +RSpec.shared_examples 'filters on each package_type' do |is_project: false| + include_context 'package filter context' + + let_it_be(:package1) { create(:conan_package, project: project) } + let_it_be(:package2) { create(:maven_package, project: project) } + let_it_be(:package3) { create(:npm_package, project: project) } + let_it_be(:package4) { create(:nuget_package, project: project) } + let_it_be(:package5) { create(:pypi_package, project: project) } + let_it_be(:package6) { create(:composer_package, project: project) } + + Packages::Package.package_types.keys.each do |package_type| + context "for package type #{package_type}" do + let(:url) { is_project ? package_filter_url(:type, package_type) : group_filter_url(:type, package_type) } + + subject { get api(url, user) } + + it "returns #{package_type} packages" do + subject + + expect(json_response.length).to eq(1) + expect(json_response.map { |package| package['package_type'] }).to contain_exactly(package_type) + end + end + end +end + +RSpec.shared_examples 'package workhorse uploads' do + context 'without a workhorse header' do + let(:workhorse_token) { JWT.encode({ 'iss' => 'invalid header' }, Gitlab::Workhorse.secret, 'HS256') } + + it_behaves_like 'returning response status', :forbidden + + it 'logs an error' do + expect(Gitlab::ErrorTracking).to receive(:track_exception).once + + subject + end + end +end 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 0e6ecf49cd0..2ddbdebdb97 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 @@ -25,19 +25,18 @@ RSpec.shared_examples 'moves repository to another storage' do |repository_type| allow(Gitlab::GitalyClient).to receive(:filesystem_id).with('default').and_call_original allow(Gitlab::GitalyClient).to receive(:filesystem_id).with('test_second_storage').and_return(SecureRandom.uuid) - allow(project_repository_double).to receive(:create_repository) - .and_return(true) allow(project_repository_double).to receive(:replicate) .with(project.repository.raw) allow(project_repository_double).to receive(:checksum) .and_return(project_repository_checksum) - allow(repository_double).to receive(:create_repository) - .and_return(true) allow(repository_double).to receive(:replicate) .with(repository.raw) allow(repository_double).to receive(:checksum) .and_return(repository_checksum) + + expect(GitlabShellWorker).to receive(:perform_async).with(:mv_repository, 'default', anything, anything) + .twice.and_call_original end it "moves the project and its #{repository_type} repository to the new storage and unmarks the repository as read only" do @@ -48,6 +47,7 @@ RSpec.shared_examples 'moves repository to another storage' do |repository_type| old_repository_path = repository.full_path result = subject.execute + project.reload expect(result).to be_success expect(project).not_to be_repository_read_only @@ -101,15 +101,11 @@ RSpec.shared_examples 'moves repository to another storage' do |repository_type| it 'unmarks the repository as read-only without updating the repository storage' do allow(Gitlab::GitalyClient).to receive(:filesystem_id).with('default').and_call_original allow(Gitlab::GitalyClient).to receive(:filesystem_id).with('test_second_storage').and_return(SecureRandom.uuid) - allow(project_repository_double).to receive(:create_repository) - .and_return(true) allow(project_repository_double).to receive(:replicate) .with(project.repository.raw) allow(project_repository_double).to receive(:checksum) .and_return(project_repository_checksum) - allow(repository_double).to receive(:create_repository) - .and_return(true) allow(repository_double).to receive(:replicate) .with(repository.raw) .and_raise(Gitlab::Git::CommandError) @@ -128,15 +124,11 @@ RSpec.shared_examples 'moves repository to another storage' do |repository_type| it 'unmarks the repository as read-only without updating the repository storage' do allow(Gitlab::GitalyClient).to receive(:filesystem_id).with('default').and_call_original allow(Gitlab::GitalyClient).to receive(:filesystem_id).with('test_second_storage').and_return(SecureRandom.uuid) - allow(project_repository_double).to receive(:create_repository) - .and_return(true) allow(project_repository_double).to receive(:replicate) .with(project.repository.raw) allow(project_repository_double).to receive(:checksum) .and_return(project_repository_checksum) - allow(repository_double).to receive(:create_repository) - .and_return(true) allow(repository_double).to receive(:replicate) .with(repository.raw) allow(repository_double).to receive(:checksum) diff --git a/spec/support/shared_examples/services/resource_events/change_milestone_service_shared_examples.rb b/spec/support/shared_examples/services/resource_events/change_milestone_service_shared_examples.rb index c5f84e205cf..ef41c2fcc13 100644 --- a/spec/support/shared_examples/services/resource_events/change_milestone_service_shared_examples.rb +++ b/spec/support/shared_examples/services/resource_events/change_milestone_service_shared_examples.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -shared_examples 'a milestone events creator' do +RSpec.shared_examples 'a milestone events creator' do let_it_be(:user) { create(:user) } let(:created_at_time) { Time.utc(2019, 12, 30) } diff --git a/spec/support/shared_examples/services/wiki_pages/create_service_shared_examples.rb b/spec/support/shared_examples/services/wiki_pages/create_service_shared_examples.rb index efcb83a34af..ebe78c299a5 100644 --- a/spec/support/shared_examples/services/wiki_pages/create_service_shared_examples.rb +++ b/spec/support/shared_examples/services/wiki_pages/create_service_shared_examples.rb @@ -63,16 +63,6 @@ RSpec.shared_examples 'WikiPages::CreateService#execute' do |container_type| include_examples 'correct event created' end - context 'the feature is disabled' do - before do - stub_feature_flags(wiki_events: false) - end - - it 'does not record the activity' do - expect { service.execute }.not_to change(Event, :count) - end - end - context 'when the options are bad' do let(:page_title) { '' } diff --git a/spec/support/shared_examples/services/wiki_pages/destroy_service_shared_examples.rb b/spec/support/shared_examples/services/wiki_pages/destroy_service_shared_examples.rb index 1231c012c31..db1b50fdf3c 100644 --- a/spec/support/shared_examples/services/wiki_pages/destroy_service_shared_examples.rb +++ b/spec/support/shared_examples/services/wiki_pages/destroy_service_shared_examples.rb @@ -37,14 +37,4 @@ RSpec.shared_examples 'WikiPages::DestroyService#execute' do |container_type| expect { service.execute(nil) }.not_to change { counter.read(:delete) } end - - context 'the feature is disabled' do - before do - stub_feature_flags(wiki_events: false) - end - - it 'does not record the activity' do - expect { service.execute(page) }.not_to change(Event, :count) - end - end end diff --git a/spec/support/shared_examples/services/wiki_pages/update_service_shared_examples.rb b/spec/support/shared_examples/services/wiki_pages/update_service_shared_examples.rb index 77354fec069..0191a6dfbc9 100644 --- a/spec/support/shared_examples/services/wiki_pages/update_service_shared_examples.rb +++ b/spec/support/shared_examples/services/wiki_pages/update_service_shared_examples.rb @@ -67,16 +67,6 @@ RSpec.shared_examples 'WikiPages::UpdateService#execute' do |container_type| include_examples 'adds activity event' end - context 'the feature is disabled' do - before do - stub_feature_flags(wiki_events: false) - end - - it 'does not record the activity' do - expect { service.execute(page) }.not_to change(Event, :count) - end - end - context 'when the options are bad' do let(:page_title) { '' } diff --git a/spec/support/shared_examples/snippet_blob_shared_examples.rb b/spec/support/shared_examples/snippet_blob_shared_examples.rb new file mode 100644 index 00000000000..ba97688d017 --- /dev/null +++ b/spec/support/shared_examples/snippet_blob_shared_examples.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.shared_examples 'snippet blob raw path' do + let(:blob) { snippet.blobs.first } + let(:ref) { blob.repository.root_ref } + + context 'for PersonalSnippets' do + let(:snippet) { personal_snippet } + + it 'returns the raw personal snippet blob path' do + expect(subject).to eq("/-/snippets/#{snippet.id}/raw/#{ref}/#{blob.path}") + end + end + + context 'for ProjectSnippets' do + let(:snippet) { project_snippet } + + it 'returns the raw project snippet blob path' do + expect(subject).to eq("/#{snippet.project.full_path}/-/snippets/#{snippet.id}/raw/#{ref}/#{blob.path}") + end + end +end diff --git a/spec/support/shared_examples/uploaders/upload_type_shared_examples.rb b/spec/support/shared_examples/uploaders/upload_type_shared_examples.rb index e58723324d3..9e0104e1410 100644 --- a/spec/support/shared_examples/uploaders/upload_type_shared_examples.rb +++ b/spec/support/shared_examples/uploaders/upload_type_shared_examples.rb @@ -2,7 +2,7 @@ # @param path [String] the path to file to upload. E.g. File.join('spec', 'fixtures', 'sanitized.svg') # @param uploader [CarrierWave::Uploader::Base] uploader to handle the upload. -shared_examples 'denied carrierwave upload' do +RSpec.shared_examples 'denied carrierwave upload' do it 'will deny upload' do fixture_file = fixture_file_upload(path) expect { uploader.cache!(fixture_file) }.to raise_exception(CarrierWave::IntegrityError) @@ -11,7 +11,7 @@ end # @param path [String] the path to file to upload. E.g. File.join('spec', 'fixtures', 'sanitized.svg') # @param uploader [CarrierWave::Uploader::Base] uploader to handle the upload. -shared_examples 'accepted carrierwave upload' do +RSpec.shared_examples 'accepted carrierwave upload' do let(:fixture_file) { fixture_file_upload(path) } before do @@ -30,7 +30,7 @@ end # @param path [String] the path to file to upload. E.g. File.join('spec', 'fixtures', 'sanitized.svg') # @param uploader [CarrierWave::Uploader::Base] uploader to handle the upload. # @param content_type [String] the upload file content type after cache -shared_examples 'upload with content type' do |content_type| +RSpec.shared_examples 'upload with content type' do |content_type| let(:fixture_file) { fixture_file_upload(path, content_type) } it 'will not change upload file content type' do diff --git a/spec/support/shared_examples/views/pipeline_status_changes_email.rb b/spec/support/shared_examples/views/pipeline_status_changes_email.rb index 15b4ce9c44e..698f11c2216 100644 --- a/spec/support/shared_examples/views/pipeline_status_changes_email.rb +++ b/spec/support/shared_examples/views/pipeline_status_changes_email.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -shared_examples 'pipeline status changes email' do +RSpec.shared_examples 'pipeline status changes email' do include Devise::Test::ControllerHelpers let(:user) { create(:user, developer_projects: [project]) } diff --git a/spec/support/shared_examples/views/plain_text_email.rb b/spec/support/shared_examples/views/plain_text_email.rb new file mode 100644 index 00000000000..23f9262b446 --- /dev/null +++ b/spec/support/shared_examples/views/plain_text_email.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'renders plain text email correctly' do + it 'renders the email without HTML links' do + render + + expect(rendered).to have_no_selector('a') + end +end diff --git a/spec/support/shared_examples/workers/gitlab/jira_import/jira_import_workers_shared_examples.rb b/spec/support/shared_examples/workers/gitlab/jira_import/jira_import_workers_shared_examples.rb index ae8c82cb67c..2cca76c8fe3 100644 --- a/spec/support/shared_examples/workers/gitlab/jira_import/jira_import_workers_shared_examples.rb +++ b/spec/support/shared_examples/workers/gitlab/jira_import/jira_import_workers_shared_examples.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -shared_examples 'include import workers modules' do +RSpec.shared_examples 'include import workers modules' do it { expect(described_class).to include_module(ApplicationWorker) } it { expect(described_class).to include_module(Gitlab::JiraImport::QueueOptions) } @@ -12,7 +12,7 @@ shared_examples 'include import workers modules' do end end -shared_examples 'does not advance to next stage' do +RSpec.shared_examples 'does not advance to next stage' do it 'does not advance to next stage' do expect(Gitlab::JiraImport::AdvanceStageWorker).not_to receive(:perform_async) @@ -20,7 +20,7 @@ shared_examples 'does not advance to next stage' do end end -shared_examples 'cannot do Jira import' do +RSpec.shared_examples 'cannot do Jira import' do it 'does not advance to next stage' do worker = described_class.new expect(worker).not_to receive(:import) @@ -29,7 +29,7 @@ shared_examples 'cannot do Jira import' do end end -shared_examples 'advance to next stage' do |next_stage| +RSpec.shared_examples 'advance to next stage' do |next_stage| let(:job_waiter) { Gitlab::JobWaiter.new(2, 'some-job-key') } it "advances to #{next_stage} stage" do |