summaryrefslogtreecommitdiff
path: root/spec/support
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-05-19 07:33:21 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2022-05-19 07:33:21 +0000
commit36a59d088eca61b834191dacea009677a96c052f (patch)
treee4f33972dab5d8ef79e3944a9f403035fceea43f /spec/support
parenta1761f15ec2cae7c7f7bbda39a75494add0dfd6f (diff)
downloadgitlab-ce-36a59d088eca61b834191dacea009677a96c052f.tar.gz
Add latest changes from gitlab-org/gitlab@15-0-stable-eev15.0.0-rc42
Diffstat (limited to 'spec/support')
-rw-r--r--spec/support/database/query_analyzer.rb8
-rw-r--r--spec/support/gitlab/usage/metrics_instrumentation_shared_examples.rb4
-rw-r--r--spec/support/graphql/arguments.rb4
-rw-r--r--spec/support/helpers/database/migration_testing_helpers.rb43
-rw-r--r--spec/support/helpers/features/snippet_helpers.rb9
-rw-r--r--spec/support/helpers/features/sorting_helpers.rb8
-rw-r--r--spec/support/helpers/filtered_search_helpers.rb112
-rw-r--r--spec/support/helpers/gitaly_setup.rb66
-rw-r--r--spec/support/helpers/graphql_helpers.rb119
-rw-r--r--spec/support/helpers/migrations_helpers.rb21
-rw-r--r--spec/support/helpers/namespaces_test_helper.rb13
-rw-r--r--spec/support/helpers/navbar_structure_helper.rb8
-rw-r--r--spec/support/helpers/next_instance_of.rb8
-rw-r--r--spec/support/helpers/project_helpers.rb16
-rw-r--r--spec/support/helpers/query_recorder.rb10
-rw-r--r--spec/support/helpers/rendered_helpers.rb12
-rw-r--r--spec/support/helpers/saas_test_helper.rb9
-rw-r--r--spec/support/helpers/stub_feature_flags.rb14
-rw-r--r--spec/support/helpers/stub_object_storage.rb5
-rw-r--r--spec/support/helpers/test_env.rb2
-rw-r--r--spec/support/helpers/trial_status_widget_test_helper.rb9
-rw-r--r--spec/support/helpers/workhorse_helpers.rb6
-rw-r--r--spec/support/helpers/workhorse_lfs_helpers.rb45
-rw-r--r--spec/support/import_export/common_util.rb2
-rw-r--r--spec/support/matchers/background_migrations_matchers.rb19
-rw-r--r--spec/support/matchers/graphql_matchers.rb9
-rw-r--r--spec/support/matchers/make_queries.rb31
-rw-r--r--spec/support/rspec.rb2
-rw-r--r--spec/support/shared_contexts/email_shared_context.rb2
-rw-r--r--spec/support/shared_contexts/graphql/requests/packages_shared_context.rb2
-rw-r--r--spec/support/shared_contexts/lib/gitlab/sidekiq_logging/structured_logger_shared_context.rb3
-rw-r--r--spec/support/shared_contexts/models/concerns/integrations/enable_ssl_verification_shared_context.rb4
-rw-r--r--spec/support/shared_contexts/navbar_structure_context.rb24
-rw-r--r--spec/support/shared_contexts/policies/group_policy_shared_context.rb5
-rw-r--r--spec/support/shared_contexts/policies/project_policy_shared_context.rb4
-rw-r--r--spec/support/shared_contexts/sentry_error_tracking_shared_context.rb2
-rw-r--r--spec/support/shared_contexts/services/service_ping/stubbed_service_ping_metrics_definitions_shared_context.rb4
-rw-r--r--spec/support/shared_examples/ci/log_downstream_pipeline_shared_examples.rb34
-rw-r--r--spec/support/shared_examples/controllers/environments_controller_shared_examples.rb17
-rw-r--r--spec/support/shared_examples/controllers/repository_lfs_file_load_shared_examples.rb6
-rw-r--r--spec/support/shared_examples/features/content_editor_shared_examples.rb6
-rw-r--r--spec/support/shared_examples/features/dependency_proxy_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/features/editable_merge_request_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/features/inviting_groups_shared_examples.rb144
-rw-r--r--spec/support/shared_examples/features/manage_applications_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/features/multiple_assignees_mr_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/features/multiple_assignees_widget_mr_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/features/multiple_reviewers_mr_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/features/packages_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/features/sidebar_shared_examples.rb6
-rw-r--r--spec/support/shared_examples/features/wiki/user_creates_wiki_page_shared_examples.rb17
-rw-r--r--spec/support/shared_examples/graphql/mutations/incident_management_timeline_events_shared_examples.rb48
-rw-r--r--spec/support/shared_examples/graphql/resolvers/packages_resolvers_shared_examples.rb6
-rw-r--r--spec/support/shared_examples/graphql/sorted_paginated_query_shared_examples.rb10
-rw-r--r--spec/support/shared_examples/graphql/types/gitlab_style_deprecations_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/lib/gitlab/ci/ci_trace_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/lib/gitlab/cycle_analytics/deployment_metrics.rb4
-rw-r--r--spec/support/shared_examples/lib/gitlab/database/reestablished_connection_stack_shared_examples.rb32
-rw-r--r--spec/support/shared_examples/lib/gitlab/kubernetes/network_policy_common_shared_examples.rb162
-rw-r--r--spec/support/shared_examples/merge_request_author_auto_assign_shared_examples.rb8
-rw-r--r--spec/support/shared_examples/models/chat_integration_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/models/concerns/integrations/reset_secret_fields_shared_examples.rb110
-rw-r--r--spec/support/shared_examples/models/concerns/integrations/slack_mattermost_notifier_shared_examples.rb27
-rw-r--r--spec/support/shared_examples/models/member_shared_examples.rb403
-rw-r--r--spec/support/shared_examples/models/reviewer_state_shared_examples.rb15
-rw-r--r--spec/support/shared_examples/models/wiki_shared_examples.rb233
-rw-r--r--spec/support/shared_examples/nav_sidebar_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/requests/api/container_repositories_shared_examples.rb27
-rw-r--r--spec/support/shared_examples/requests/api/graphql/group_and_project_boards_query_shared_examples.rb8
-rw-r--r--spec/support/shared_examples/requests/api/graphql/noteable_shared_examples.rb18
-rw-r--r--spec/support/shared_examples/requests/api/graphql/packages/group_and_project_packages_list_shared_examples.rb89
-rw-r--r--spec/support/shared_examples/requests/api/graphql/packages/package_details_shared_examples.rb12
-rw-r--r--spec/support/shared_examples/requests/api/graphql/projects/alert_management/integrations_shared_examples.rb10
-rw-r--r--spec/support/shared_examples/requests/api/milestones_shared_examples.rb10
-rw-r--r--spec/support/shared_examples/requests/rack_attack_shared_examples.rb19
-rw-r--r--spec/support/shared_examples/serializers/environment_serializer_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/services/issuable/destroy_service_shared_examples.rb26
-rw-r--r--spec/support/shared_examples/services/jira/requests/base_shared_examples.rb16
-rw-r--r--spec/support/shared_examples/work_item_base_types_importer.rb42
-rw-r--r--spec/support/shared_examples/workers/batched_background_migration_worker_shared_examples.rb119
-rw-r--r--spec/support/shared_examples/workers/concerns/git_garbage_collect_methods_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/workers/concerns/reenqueuer_shared_examples.rb8
82 files changed, 1757 insertions, 631 deletions
diff --git a/spec/support/database/query_analyzer.rb b/spec/support/database/query_analyzer.rb
index 6d6627d54b9..aaa1b3516a3 100644
--- a/spec/support/database/query_analyzer.rb
+++ b/spec/support/database/query_analyzer.rb
@@ -6,13 +6,17 @@
RSpec.configure do |config|
config.before do |example|
if example.metadata.fetch(:query_analyzers, true)
- ::Gitlab::Database::QueryAnalyzer.instance.begin!
+ ::Gitlab::Database::QueryAnalyzer.instance.begin!(
+ ::Gitlab::Database::QueryAnalyzer.instance.all_analyzers
+ )
end
end
config.after do |example|
if example.metadata.fetch(:query_analyzers, true)
- ::Gitlab::Database::QueryAnalyzer.instance.end!
+ ::Gitlab::Database::QueryAnalyzer.instance.end!(
+ ::Gitlab::Database::QueryAnalyzer.instance.all_analyzers
+ )
end
end
end
diff --git a/spec/support/gitlab/usage/metrics_instrumentation_shared_examples.rb b/spec/support/gitlab/usage/metrics_instrumentation_shared_examples.rb
index 4624a8ac82a..e02bf66507a 100644
--- a/spec/support/gitlab/usage/metrics_instrumentation_shared_examples.rb
+++ b/spec/support/gitlab/usage/metrics_instrumentation_shared_examples.rb
@@ -5,6 +5,10 @@ RSpec.shared_examples 'a correct instrumented metric value' do |params|
let(:options) { params[:options] }
let(:metric) { described_class.new(time_frame: time_frame, options: options) }
+ around do |example|
+ freeze_time { example.run }
+ end
+
before do
if described_class.respond_to?(:relation) && described_class.relation.respond_to?(:connection)
allow(described_class.relation.connection).to receive(:transaction_open?).and_return(false)
diff --git a/spec/support/graphql/arguments.rb b/spec/support/graphql/arguments.rb
index 20e940030f8..a5bb01c31a3 100644
--- a/spec/support/graphql/arguments.rb
+++ b/spec/support/graphql/arguments.rb
@@ -40,7 +40,7 @@ module Graphql
when Array then "[#{value.map { |v| as_graphql_literal(v) }.join(',')}]"
when Hash then "{#{new(value)}}"
when Integer, Float, Symbol then value.to_s
- when String then "\"#{value.gsub(/"/, '\\"')}\""
+ when String, GlobalID then "\"#{value.to_s.gsub(/"/, '\\"')}\""
when Time, Date then "\"#{value.iso8601}\""
when nil then 'null'
when true then 'true'
@@ -49,7 +49,7 @@ module Graphql
value.to_graphql_value
end
rescue NoMethodError
- raise ArgumentError, "Cannot represent #{value} as GraphQL literal"
+ raise ArgumentError, "Cannot represent #{value} (instance of #{value.class}) as GraphQL literal"
end
def merge(other)
diff --git a/spec/support/helpers/database/migration_testing_helpers.rb b/spec/support/helpers/database/migration_testing_helpers.rb
new file mode 100644
index 00000000000..916446e66b7
--- /dev/null
+++ b/spec/support/helpers/database/migration_testing_helpers.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+module Database
+ module MigrationTestingHelpers
+ def define_background_migration(name)
+ klass = Class.new do
+ # Can't simply def perform here as we won't have access to the block,
+ # similarly can't define_method(:perform, &block) here as it would change the block receiver
+ define_method(:perform) { |*args| yield(*args) }
+ end
+ stub_const("Gitlab::BackgroundMigration::#{name}", klass)
+ klass
+ end
+
+ def expect_migration_call_counts(migrations_to_calls)
+ migrations_to_calls.each do |migration, calls|
+ expect_next_instances_of(migration, calls) do |m|
+ expect(m).to receive(:perform).and_call_original
+ end
+ end
+ end
+
+ def expect_recorded_migration_runs(migrations_to_runs)
+ migrations_to_runs.each do |migration, runs|
+ path = File.join(result_dir, migration.name.demodulize)
+ if runs.zero?
+ expect(Pathname(path)).not_to be_exist
+ else
+ num_subdirs = Pathname(path).children.count(&:directory?)
+ expect(num_subdirs).to eq(runs)
+ end
+ end
+ end
+
+ def expect_migration_runs(migrations_to_run_counts)
+ expect_migration_call_counts(migrations_to_run_counts)
+
+ yield
+
+ expect_recorded_migration_runs(migrations_to_run_counts)
+ end
+ end
+end
diff --git a/spec/support/helpers/features/snippet_helpers.rb b/spec/support/helpers/features/snippet_helpers.rb
index dc718b1b212..3e32b0e4c67 100644
--- a/spec/support/helpers/features/snippet_helpers.rb
+++ b/spec/support/helpers/features/snippet_helpers.rb
@@ -67,14 +67,19 @@ module Spec
end
def snippet_fill_in_form(title: nil, content: nil, file_name: nil, description: nil, visibility: nil)
+ if content
+ snippet_fill_in_content(content)
+ # It takes some time after sending keys for the vue component to
+ # update so let Capybara wait for the content before proceeding
+ expect(page).to have_content(content)
+ end
+
snippet_fill_in_title(title) if title
snippet_fill_in_description(description) if description
snippet_fill_in_file_name(file_name) if file_name
- snippet_fill_in_content(content) if content
-
snippet_fill_in_visibility(visibility) if visibility
end
end
diff --git a/spec/support/helpers/features/sorting_helpers.rb b/spec/support/helpers/features/sorting_helpers.rb
index a6428bf8573..50b8083ebb3 100644
--- a/spec/support/helpers/features/sorting_helpers.rb
+++ b/spec/support/helpers/features/sorting_helpers.rb
@@ -21,6 +21,14 @@ module Spec
click_link(value)
end
end
+
+ # pajamas_sort_by is used to sort new pajamas dropdowns. When
+ # all of the dropdowns are converted, pajamas_sort_by can be renamed to sort_by
+ # https://gitlab.com/groups/gitlab-org/-/epics/7551
+ def pajamas_sort_by(value)
+ find('.filter-dropdown-container .dropdown').click
+ find('.dropdown-item', text: value).click
+ end
end
end
end
diff --git a/spec/support/helpers/filtered_search_helpers.rb b/spec/support/helpers/filtered_search_helpers.rb
index b6cf78b9046..93122ca3d0c 100644
--- a/spec/support/helpers/filtered_search_helpers.rb
+++ b/spec/support/helpers/filtered_search_helpers.rb
@@ -187,4 +187,116 @@ module FilteredSearchHelpers
toggle.click if toggle.visible?
end
end
+
+ ##
+ # For use with gl-filtered-search
+ def select_tokens(*args, submit: false)
+ within '[data-testid="filtered-search-input"]' do
+ find_field('Search').click
+
+ args.each do |token|
+ # Move mouse away to prevent invoking tooltips on usernames, which blocks the search input
+ find_button('Search').hover
+
+ if token == '='
+ click_on '= is'
+ else
+ click_on token
+ end
+
+ wait_for_requests
+ end
+ end
+
+ if submit
+ send_keys :enter
+ end
+ end
+
+ def get_suggestion_count
+ all('.gl-filtered-search-suggestion').size
+ end
+
+ def submit_search_term(value)
+ click_filtered_search_bar
+ send_keys(value, :enter)
+ end
+
+ def click_filtered_search_bar
+ find('.gl-filtered-search-last-item').click
+ end
+
+ def click_token_segment(value)
+ find('.gl-filtered-search-token-segment', text: value).click
+ end
+
+ def expect_visible_suggestions_list
+ expect(page).to have_css('.gl-filtered-search-suggestion-list')
+ end
+
+ def expect_hidden_suggestions_list
+ expect(page).not_to have_css('.gl-filtered-search-suggestion-list')
+ end
+
+ def expect_suggestion(value)
+ expect(page).to have_css('.gl-filtered-search-suggestion', text: value)
+ end
+
+ def expect_no_suggestion(value)
+ expect(page).not_to have_css('.gl-filtered-search-suggestion', text: value)
+ end
+
+ def expect_suggestion_count(count)
+ expect(page).to have_css('.gl-filtered-search-suggestion', count: count)
+ end
+
+ def expect_assignee_token(value)
+ expect(page).to have_css '.gl-filtered-search-token', text: "Assignee = #{value}"
+ end
+
+ def expect_author_token(value)
+ expect(page).to have_css '.gl-filtered-search-token', text: "Author = #{value}"
+ end
+
+ def expect_label_token(value)
+ expect(page).to have_css '.gl-filtered-search-token', text: "Label = ~#{value}"
+ end
+
+ def expect_negated_label_token(value)
+ expect(page).to have_css '.gl-filtered-search-token', text: "Label != ~#{value}"
+ end
+
+ def expect_milestone_token(value)
+ expect(page).to have_css '.gl-filtered-search-token', text: "Milestone = %#{value}"
+ end
+
+ def expect_negated_milestone_token(value)
+ expect(page).to have_css '.gl-filtered-search-token', text: "Milestone != %#{value}"
+ end
+
+ def expect_epic_token(value)
+ expect(page).to have_css '.gl-filtered-search-token', text: "Epic = #{value}"
+ end
+
+ def expect_search_term(value)
+ value.split(' ').each do |term|
+ expect(page).to have_css '.gl-filtered-search-term', text: term
+ end
+ end
+
+ def expect_empty_search_term
+ expect(page).to have_css '.gl-filtered-search-term', text: ''
+ end
+
+ def expect_token_segment(value)
+ expect(page).to have_css '.gl-filtered-search-token-segment', text: value
+ end
+
+ def expect_recent_searches_history_item(value)
+ expect(page).to have_css '.gl-search-box-by-click-history-item', text: value
+ end
+
+ def expect_recent_searches_history_item_count(count)
+ expect(page).to have_css '.gl-search-box-by-click-history-item', count: count
+ end
end
diff --git a/spec/support/helpers/gitaly_setup.rb b/spec/support/helpers/gitaly_setup.rb
index 0ad83bdeeb2..264281ef94a 100644
--- a/spec/support/helpers/gitaly_setup.rb
+++ b/spec/support/helpers/gitaly_setup.rb
@@ -31,6 +31,10 @@ module GitalySetup
expand_path('tmp/tests/gitaly')
end
+ def runtime_dir
+ expand_path('tmp/run')
+ end
+
def tmp_tests_gitaly_bin_dir
File.join(tmp_tests_gitaly_dir, '_build', 'bin')
end
@@ -102,12 +106,14 @@ module GitalySetup
Gitlab.config.repositories.storages[REPOS_STORAGE].legacy_disk_path
end
- def service_binary(service)
+ def service_cmd(service, toml = nil)
+ toml ||= config_path(service)
+
case service
when :gitaly, :gitaly2
- 'gitaly'
+ [File.join(tmp_tests_gitaly_bin_dir, 'gitaly'), toml]
when :praefect
- 'praefect'
+ [File.join(tmp_tests_gitaly_bin_dir, 'praefect'), '-config', toml]
end
end
@@ -132,14 +138,18 @@ module GitalySetup
end
def start_praefect
- start(:praefect)
+ if ENV['GITALY_PRAEFECT_WITH_DB']
+ LOGGER.debug 'Starting Praefect with database election strategy'
+ start(:praefect, File.join(tmp_tests_gitaly_dir, 'praefect-db.config.toml'))
+ else
+ LOGGER.debug 'Starting Praefect with in-memory election strategy'
+ start(:praefect)
+ end
end
def start(service, toml = nil)
toml ||= config_path(service)
- args = ["#{tmp_tests_gitaly_bin_dir}/#{service_binary(service)}"]
- args.push("-config") if service == :praefect
- args.push(toml)
+ args = service_cmd(service, toml)
# Ensure user configuration does not affect Git
# Context: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/58776#note_547613780
@@ -259,6 +269,7 @@ module GitalySetup
{ 'default' => repos_path },
force: true,
options: {
+ runtime_dir: runtime_dir,
prometheus_listen_addr: 'localhost:9236'
}
)
@@ -267,12 +278,47 @@ module GitalySetup
{ 'default' => repos_path },
force: true,
options: {
- runtime_dir: File.join(gitaly_dir, "run2"),
+ runtime_dir: runtime_dir,
gitaly_socket: "gitaly2.socket",
config_filename: "gitaly2.config.toml"
}
)
- Gitlab::SetupHelper::Praefect.create_configuration(gitaly_dir, { 'praefect' => repos_path }, force: true)
+
+ # In CI we need to pre-generate both config files.
+ # For local testing we'll create the correct file on-demand.
+ if ENV['CI'] || ENV['GITALY_PRAEFECT_WITH_DB'].nil?
+ Gitlab::SetupHelper::Praefect.create_configuration(
+ gitaly_dir,
+ { 'praefect' => repos_path },
+ force: true
+ )
+ end
+
+ if ENV['CI'] || ENV['GITALY_PRAEFECT_WITH_DB']
+ Gitlab::SetupHelper::Praefect.create_configuration(
+ gitaly_dir,
+ { 'praefect' => repos_path },
+ force: true,
+ options: {
+ per_repository: true,
+ config_filename: 'praefect-db.config.toml',
+ pghost: ENV['CI'] ? 'postgres' : ENV.fetch('PGHOST'),
+ pgport: ENV['CI'] ? 5432 : ENV.fetch('PGPORT').to_i,
+ pguser: ENV['CI'] ? 'postgres' : ENV.fetch('USER')
+ }
+ )
+ end
+
+ # In CI no database is running when Gitaly is set up
+ # so scripts/gitaly-test-spawn will take care of it instead.
+ setup_praefect unless ENV['CI']
+ end
+
+ def setup_praefect
+ return unless ENV['GITALY_PRAEFECT_WITH_DB']
+
+ migrate_cmd = service_cmd(:praefect, File.join(tmp_tests_gitaly_dir, 'praefect-db.config.toml')) + ['sql-migrate']
+ system(env, *migrate_cmd, [:out, :err] => 'log/praefect-test.log')
end
def socket_path(service)
@@ -325,7 +371,7 @@ module GitalySetup
message += "- The `praefect` binary does not exist: #{praefect_binary}\n" unless File.exist?(praefect_binary)
message += "- The `git` binary does not exist: #{git_binary}\n" unless File.exist?(git_binary)
- message += "\nCheck log/gitaly-test.log for errors.\n"
+ message += "\nCheck log/gitaly-test.log & log/praefect-test.log for errors.\n"
unless ENV['CI']
message += "\nIf binaries are missing, try running `make -C tmp/tests/gitaly all WITH_BUNDLED_GIT=YesPlease`.\n"
diff --git a/spec/support/helpers/graphql_helpers.rb b/spec/support/helpers/graphql_helpers.rb
index ff8908e531a..db8d45f61ea 100644
--- a/spec/support/helpers/graphql_helpers.rb
+++ b/spec/support/helpers/graphql_helpers.rb
@@ -244,15 +244,16 @@ module GraphqlHelpers
def graphql_mutation(name, input, fields = nil, &block)
raise ArgumentError, 'Please pass either `fields` parameter or a block to `#graphql_mutation`, but not both.' if fields.present? && block_given?
+ name = name.graphql_name if name.respond_to?(:graphql_name)
mutation_name = GraphqlHelpers.fieldnamerize(name)
input_variable_name = "$#{input_variable_name_for_mutation(name)}"
mutation_field = GitlabSchema.mutation.fields[mutation_name]
fields = yield if block_given?
- fields ||= all_graphql_fields_for(mutation_field.type.to_graphql)
+ fields ||= all_graphql_fields_for(mutation_field.type.to_type_signature)
query = <<~MUTATION
- mutation(#{input_variable_name}: #{mutation_field.arguments['input'].type.to_graphql}) {
+ mutation(#{input_variable_name}: #{mutation_field.arguments['input'].type.to_type_signature}) {
#{mutation_name}(input: #{input_variable_name}) {
#{fields}
}
@@ -264,7 +265,7 @@ module GraphqlHelpers
end
def variables_for_mutation(name, input)
- graphql_input = prepare_input_for_mutation(input)
+ graphql_input = prepare_variables(input)
{ input_variable_name_for_mutation(name) => graphql_input }
end
@@ -273,25 +274,35 @@ module GraphqlHelpers
return unless variables
return variables if variables.is_a?(String)
- ::Gitlab::Utils::MergeHash.merge(Array.wrap(variables).map(&:to_h)).to_json
+ # Combine variables into a single hash.
+ hash = ::Gitlab::Utils::MergeHash.merge(Array.wrap(variables).map(&:to_h))
+
+ prepare_variables(hash).to_json
end
- # Recursively convert a Hash with Ruby-style keys to GraphQL fieldname-style keys
+ # Recursively convert any ruby object we can pass as a variable value
+ # to an object we can serialize with JSON, using fieldname-style keys
#
- # prepare_input_for_mutation({ 'my_key' => 1 })
- # => { 'myKey' => 1}
- def prepare_input_for_mutation(input)
- input.to_h do |name, value|
- value = prepare_input_for_mutation(value) if value.is_a?(Hash)
+ # prepare_variables({ 'my_key' => 1 })
+ # => { 'myKey' => 1 }
+ # prepare_variables({ enums: [:FOO, :BAR], user_id: global_id_of(user) })
+ # => { 'enums' => ['FOO', 'BAR'], 'userId' => "gid://User/123" }
+ # prepare_variables({ nested: { hash_values: { are_supported: true } } })
+ # => { 'nested' => { 'hashValues' => { 'areSupported' => true } } }
+ def prepare_variables(input)
+ return input.map { prepare_variables(_1) } if input.is_a?(Array)
+ return input.to_s if input.is_a?(GlobalID) || input.is_a?(Symbol)
+ return input unless input.is_a?(Hash)
- [GraphqlHelpers.fieldnamerize(name), value]
+ input.to_h do |name, value|
+ [GraphqlHelpers.fieldnamerize(name), prepare_variables(value)]
end
end
def input_variable_name_for_mutation(mutation_name)
mutation_name = GraphqlHelpers.fieldnamerize(mutation_name)
mutation_field = GitlabSchema.mutation.fields[mutation_name]
- input_type = field_type(mutation_field.arguments['input'])
+ input_type = mutation_field.arguments['input'].type.unwrap.to_type_signature
GraphqlHelpers.fieldnamerize(input_type)
end
@@ -346,6 +357,10 @@ module GraphqlHelpers
end
end
+ def query_double(schema:)
+ double('query', schema: schema)
+ end
+
def wrap_fields(fields)
fields = Array.wrap(fields).map do |field|
case field
@@ -646,11 +661,11 @@ module GraphqlHelpers
end
end
- def global_id_of(model, id: nil, model_name: nil)
+ def global_id_of(model = nil, id: nil, model_name: nil)
if id || model_name
- ::Gitlab::GlobalId.build(model, id: id, model_name: model_name).to_s
+ ::Gitlab::GlobalId.as_global_id(id || model.id, model_name: model_name || model.class.name)
else
- model.to_global_id.to_s
+ model.to_global_id
end
end
@@ -683,26 +698,94 @@ module GraphqlHelpers
end
end
- # assumes query_string to be let-bound in the current context
- def execute_query(query_type, schema: empty_schema, graphql: query_string)
+ # assumes query_string and user to be let-bound in the current context
+ def execute_query(query_type, schema: empty_schema, graphql: query_string, raise_on_error: false)
schema.query(query_type)
- schema.execute(
+ r = schema.execute(
graphql,
context: { current_user: user },
variables: {}
)
+
+ if raise_on_error && r.to_h['errors'].present?
+ raise NoData, r.to_h['errors']
+ end
+
+ r
end
def empty_schema
Class.new(GraphQL::Schema) do
use GraphQL::Pagination::Connections
use Gitlab::Graphql::Pagination::Connections
+ use BatchLoader::GraphQL
lazy_resolve ::Gitlab::Graphql::Lazy, :force
end
end
+ # Wrapper around a_hash_including that supports unpacking with **
+ class UnpackableMatcher < SimpleDelegator
+ include RSpec::Matchers
+
+ attr_reader :to_hash
+
+ def initialize(hash)
+ @to_hash = hash
+ super(a_hash_including(hash))
+ end
+
+ def to_json(_opts = {})
+ to_hash.to_json
+ end
+
+ def as_json(opts = {})
+ to_hash.as_json(opts)
+ end
+ end
+
+ # Construct a matcher for GraphQL entity response objects, of the form
+ # `{ "id" => "some-gid" }`.
+ #
+ # Usage:
+ #
+ # ```ruby
+ # expect(graphql_data_at(:path, :to, :entity)).to match a_graphql_entity_for(user)
+ # ```
+ #
+ # This can be called as:
+ #
+ # ```ruby
+ # a_graphql_entity_for(project, :full_path) # also checks that `entity['fullPath'] == project.full_path
+ # a_graphql_entity_for(project, full_path: 'some/path') # same as above, with explicit values
+ # a_graphql_entity_for(user, :username, foo: 'bar') # combinations of the above
+ # a_graphql_entity_for(foo: 'bar') # if properties are defined, the model is not necessary
+ # ```
+ #
+ # Note that the model instance must not be nil, unless some properties are
+ # explicitly passed in. The following are rejected with `ArgumentError`:
+ #
+ # ```
+ # a_graphql_entity_for(nil, :username)
+ # a_graphql_entity_for(:username)
+ # a_graphql_entity_for
+ # ```
+ #
+ def a_graphql_entity_for(model = nil, *fields, **attrs)
+ raise ArgumentError, 'model is nil' if model.nil? && fields.any?
+
+ attrs.transform_keys! { GraphqlHelpers.fieldnamerize(_1) }
+ attrs['id'] = global_id_of(model).to_s if model
+ fields.each do |name|
+ attrs[GraphqlHelpers.fieldnamerize(name)] = model.public_send(name)
+ end
+
+ raise ArgumentError, 'no attributes' if attrs.empty?
+
+ UnpackableMatcher.new(attrs)
+ end
+
# A lookahead that selects everything
def positive_lookahead
double(selects?: true).tap do |selection|
diff --git a/spec/support/helpers/migrations_helpers.rb b/spec/support/helpers/migrations_helpers.rb
index afa7ee84bda..60097e301c4 100644
--- a/spec/support/helpers/migrations_helpers.rb
+++ b/spec/support/helpers/migrations_helpers.rb
@@ -1,12 +1,18 @@
# frozen_string_literal: true
module MigrationsHelpers
- def active_record_base
- Gitlab::Database.database_base_models.fetch(self.class.metadata[:database] || :main)
+ def active_record_base(database: nil)
+ database_name = database || self.class.metadata[:database] || :main
+
+ unless Gitlab::Database::DATABASE_NAMES.include?(database_name.to_s)
+ raise ArgumentError, "#{database_name} is not a valid argument"
+ end
+
+ Gitlab::Database.database_base_models[database_name] || Gitlab::Database.database_base_models[:main]
end
- def table(name)
- Class.new(active_record_base) do
+ def table(name, database: nil)
+ Class.new(active_record_base(database: database)) do
self.table_name = name
self.inheritance_column = :_type_disabled
@@ -150,6 +156,13 @@ module MigrationsHelpers
end
def migrate!
+ open_transactions = ActiveRecord::Base.connection.open_transactions
+ allow_next_instance_of(described_class) do |migration|
+ allow(migration).to receive(:transaction_open?) do
+ ActiveRecord::Base.connection.open_transactions > open_transactions
+ end
+ end
+
migration_context.up do |migration|
migration.name == described_class.name
end
diff --git a/spec/support/helpers/namespaces_test_helper.rb b/spec/support/helpers/namespaces_test_helper.rb
new file mode 100644
index 00000000000..9762c38a9bb
--- /dev/null
+++ b/spec/support/helpers/namespaces_test_helper.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module NamespacesTestHelper
+ def get_buy_minutes_path(namespace)
+ buy_minutes_subscriptions_path(selected_group: namespace.id)
+ end
+
+ def get_buy_storage_path(namespace)
+ buy_storage_subscriptions_path(selected_group: namespace.id)
+ end
+end
+
+NamespacesTestHelper.prepend_mod
diff --git a/spec/support/helpers/navbar_structure_helper.rb b/spec/support/helpers/navbar_structure_helper.rb
index 315303401cc..e11548d0b75 100644
--- a/spec/support/helpers/navbar_structure_helper.rb
+++ b/spec/support/helpers/navbar_structure_helper.rb
@@ -93,7 +93,7 @@ module NavbarStructureHelper
)
end
- def analytics_sub_nav_item
+ def project_analytics_sub_nav_item
[
_('Value stream'),
_('CI/CD'),
@@ -102,6 +102,12 @@ module NavbarStructureHelper
_('Repository')
]
end
+
+ def group_analytics_sub_nav_item
+ [
+ _('Contribution')
+ ]
+ end
end
NavbarStructureHelper.prepend_mod
diff --git a/spec/support/helpers/next_instance_of.rb b/spec/support/helpers/next_instance_of.rb
index 95d8936588c..461d411a5ce 100644
--- a/spec/support/helpers/next_instance_of.rb
+++ b/spec/support/helpers/next_instance_of.rb
@@ -22,9 +22,15 @@ module NextInstanceOf
def stub_new(target, number, ordered = false, *new_args, &blk)
receive_new = receive(:new)
receive_new.ordered if ordered
- receive_new.exactly(number).times if number
receive_new.with(*new_args) if new_args.any?
+ if number.is_a?(Range)
+ receive_new.at_least(number.begin).times if number.begin
+ receive_new.at_most(number.end).times if number.end
+ elsif number
+ receive_new.exactly(number).times
+ end
+
target.to receive_new.and_wrap_original do |method, *original_args|
method.call(*original_args).tap(&blk)
end
diff --git a/spec/support/helpers/project_helpers.rb b/spec/support/helpers/project_helpers.rb
index 89f0163b4b6..2ea6405e48c 100644
--- a/spec/support/helpers/project_helpers.rb
+++ b/spec/support/helpers/project_helpers.rb
@@ -24,4 +24,20 @@ module ProjectHelpers
project.update!(params)
end
+
+ def create_project_with_statistics(namespace = nil, with_data: false, size_multiplier: 1)
+ project = namespace.present? ? create(:project, namespace: namespace) : create(:project)
+ project.tap do |p|
+ create(:project_statistics, project: p, with_data: with_data, size_multiplier: size_multiplier)
+ end
+ end
+
+ def grace_months_after_deletion_notification
+ (::Gitlab::CurrentSettings.inactive_projects_delete_after_months -
+ ::Gitlab::CurrentSettings.inactive_projects_send_warning_email_after_months).months
+ end
+
+ def deletion_date
+ Date.parse(grace_months_after_deletion_notification.from_now.to_s).to_s
+ end
end
diff --git a/spec/support/helpers/query_recorder.rb b/spec/support/helpers/query_recorder.rb
index d18a1d23584..01839a74e65 100644
--- a/spec/support/helpers/query_recorder.rb
+++ b/spec/support/helpers/query_recorder.rb
@@ -80,7 +80,8 @@ module ActiveRecord
if values[:cached] && skip_cached
@cached << values[:sql]
- elsif !skip_schema_queries || !values[:name]&.include?("SCHEMA")
+ elsif !ignorable?(values)
+
backtrace = @query_recorder_debug ? show_backtrace(values, duration) : nil
@log << values[:sql]
store_sql_by_source(values: values, duration: duration, backtrace: backtrace)
@@ -102,5 +103,12 @@ module ActiveRecord
def occurrences
@occurrences ||= @log.group_by(&:to_s).transform_values(&:count)
end
+
+ def ignorable?(values)
+ return true if skip_schema_queries && values[:name]&.include?("SCHEMA")
+ return true if values[:name]&.match(/License Load/)
+
+ false
+ end
end
end
diff --git a/spec/support/helpers/rendered_helpers.rb b/spec/support/helpers/rendered_helpers.rb
new file mode 100644
index 00000000000..137b7d5f708
--- /dev/null
+++ b/spec/support/helpers/rendered_helpers.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+module RenderedHelpers
+ # Wraps the `rendered` in `expect` to make it the target of an expectation.
+ # Designed to read nicely for one-liners.
+ # rubocop:disable RSpec/VoidExpect
+ def expect_rendered
+ render
+ expect(rendered)
+ end
+ # rubocop:enable RSpec/VoidExpect
+end
diff --git a/spec/support/helpers/saas_test_helper.rb b/spec/support/helpers/saas_test_helper.rb
new file mode 100644
index 00000000000..a8162603cd9
--- /dev/null
+++ b/spec/support/helpers/saas_test_helper.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module SaasTestHelper
+ def get_next_url
+ "https://next.gitlab.com"
+ end
+end
+
+SaasTestHelper.prepend_mod
diff --git a/spec/support/helpers/stub_feature_flags.rb b/spec/support/helpers/stub_feature_flags.rb
index 77f31169ecb..f1654e55b7e 100644
--- a/spec/support/helpers/stub_feature_flags.rb
+++ b/spec/support/helpers/stub_feature_flags.rb
@@ -70,4 +70,18 @@ module StubFeatureFlags
def skip_default_enabled_yaml_check
allow(Feature::Definition).to receive(:default_enabled?).and_return(false)
end
+
+ def stub_feature_flag_definition(name, opts = {})
+ opts = opts.with_defaults(
+ name: name,
+ type: 'development',
+ default_enabled: false
+ )
+
+ Feature::Definition.new("#{opts[:type]}/#{name}.yml", opts).tap do |definition|
+ all_definitions = Feature::Definition.definitions
+ all_definitions[definition.key] = definition
+ allow(Feature::Definition).to receive(:definitions).and_return(all_definitions)
+ end
+ end
end
diff --git a/spec/support/helpers/stub_object_storage.rb b/spec/support/helpers/stub_object_storage.rb
index d49a14f7f5b..024f06cae1b 100644
--- a/spec/support/helpers/stub_object_storage.rb
+++ b/spec/support/helpers/stub_object_storage.rb
@@ -7,11 +7,6 @@ module StubObjectStorage
**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:,
diff --git a/spec/support/helpers/test_env.rb b/spec/support/helpers/test_env.rb
index d81d0d436a1..11f469c1d27 100644
--- a/spec/support/helpers/test_env.rb
+++ b/spec/support/helpers/test_env.rb
@@ -374,7 +374,7 @@ module TestEnv
end
def seed_db
- Gitlab::DatabaseImporters::WorkItems::BaseTypeImporter.import
+ Gitlab::DatabaseImporters::WorkItems::BaseTypeImporter.upsert_types
end
private
diff --git a/spec/support/helpers/trial_status_widget_test_helper.rb b/spec/support/helpers/trial_status_widget_test_helper.rb
new file mode 100644
index 00000000000..d75620d17ee
--- /dev/null
+++ b/spec/support/helpers/trial_status_widget_test_helper.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module TrialStatusWidgetTestHelper
+ def purchase_href(group)
+ new_subscriptions_path(namespace_id: group.id, plan_id: 'ultimate-plan-id')
+ end
+end
+
+TrialStatusWidgetTestHelper.prepend_mod
diff --git a/spec/support/helpers/workhorse_helpers.rb b/spec/support/helpers/workhorse_helpers.rb
index 83bda6e03b1..6f22df9ae0f 100644
--- a/spec/support/helpers/workhorse_helpers.rb
+++ b/spec/support/helpers/workhorse_helpers.rb
@@ -114,16 +114,18 @@ module WorkhorseHelpers
end
params["#{key}.remote_id"] = file.remote_id if file.respond_to?(:remote_id) && file.remote_id.present?
+ params["#{key}.sha256"] = file.sha256 if file.respond_to?(:sha256) && file.sha256.present?
end
end
- def fog_to_uploaded_file(file)
+ def fog_to_uploaded_file(file, sha256: nil)
filename = File.basename(file.key)
UploadedFile.new(nil,
filename: filename,
remote_id: filename,
- size: file.content_length
+ size: file.content_length,
+ sha256: sha256
)
end
end
diff --git a/spec/support/helpers/workhorse_lfs_helpers.rb b/spec/support/helpers/workhorse_lfs_helpers.rb
new file mode 100644
index 00000000000..c9644826317
--- /dev/null
+++ b/spec/support/helpers/workhorse_lfs_helpers.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+module WorkhorseLfsHelpers
+ extend self
+
+ def put_finalize(
+ lfs_tmp = nil, with_tempfile: false, verified: true, remote_object: nil,
+ args: {}, to_project: nil, size: nil, sha256: nil)
+
+ lfs_tmp ||= "#{sample_oid}012345678"
+ to_project ||= project
+ uploaded_file =
+ if with_tempfile
+ upload_path = LfsObjectUploader.workhorse_local_upload_path
+ file_path = upload_path + '/' + lfs_tmp
+
+ FileUtils.mkdir_p(upload_path)
+ FileUtils.touch(file_path)
+ File.truncate(file_path, sample_size)
+
+ UploadedFile.new(file_path, filename: File.basename(file_path), sha256: sample_oid)
+ elsif remote_object
+ fog_to_uploaded_file(remote_object, sha256: sample_oid)
+ else
+ UploadedFile.new(
+ nil,
+ size: size || sample_size,
+ sha256: sha256 || sample_oid,
+ remote_id: 'remote id'
+ )
+ end
+
+ finalize_headers = headers
+ finalize_headers.merge!(workhorse_internal_api_request_header) if verified
+
+ workhorse_finalize(
+ objects_url(to_project, sample_oid, sample_size),
+ method: :put,
+ file_key: :file,
+ params: args.merge(file: uploaded_file),
+ headers: finalize_headers,
+ send_rewritten_field: include_workhorse_jwt_header
+ )
+ end
+end
diff --git a/spec/support/import_export/common_util.rb b/spec/support/import_export/common_util.rb
index 1aa20dab6f8..9da151895a7 100644
--- a/spec/support/import_export/common_util.rb
+++ b/spec/support/import_export/common_util.rb
@@ -19,7 +19,7 @@ module ImportExport
end
def setup_reader(reader)
- if reader == :ndjson_reader && Feature.enabled?(:project_import_ndjson, default_enabled: true)
+ if reader == :ndjson_reader && Feature.enabled?(:project_import_ndjson)
allow_any_instance_of(Gitlab::ImportExport::Json::LegacyReader::File).to receive(:exist?).and_return(false)
allow_any_instance_of(Gitlab::ImportExport::Json::NdjsonReader).to receive(:exist?).and_return(true)
else
diff --git a/spec/support/matchers/background_migrations_matchers.rb b/spec/support/matchers/background_migrations_matchers.rb
index 1057639beec..b471323dd72 100644
--- a/spec/support/matchers/background_migrations_matchers.rb
+++ b/spec/support/matchers/background_migrations_matchers.rb
@@ -67,17 +67,6 @@ end
RSpec::Matchers.define :have_scheduled_batched_migration do |table_name: nil, column_name: nil, job_arguments: [], **attributes|
define_method :matches? do |migration|
- # Default arguments passed by BatchedMigrationWrapper (values don't matter here)
- expect(migration).to be_background_migration_with_arguments([
- _start_id = 1,
- _stop_id = 2,
- table_name,
- column_name,
- _sub_batch_size = 10,
- _pause_ms = 100,
- *job_arguments
- ])
-
batched_migrations =
Gitlab::Database::BackgroundMigration::BatchedMigration
.for_configuration(migration, table_name, column_name, job_arguments)
@@ -94,3 +83,11 @@ RSpec::Matchers.define :have_scheduled_batched_migration do |table_name: nil, co
expect(batched_migrations.count).to be(0)
end
end
+
+RSpec::Matchers.define :be_finalize_background_migration_of do |migration|
+ define_method :matches? do |klass|
+ expect_next_instance_of(klass) do |instance|
+ expect(instance).to receive(:finalize_background_migration).with(migration)
+ end
+ end
+end
diff --git a/spec/support/matchers/graphql_matchers.rb b/spec/support/matchers/graphql_matchers.rb
index 3ba88c3ae71..e6d820104be 100644
--- a/spec/support/matchers/graphql_matchers.rb
+++ b/spec/support/matchers/graphql_matchers.rb
@@ -211,18 +211,13 @@ end
RSpec::Matchers.define :have_graphql_resolver do |expected|
match do |field|
- case expected
- when Method
- expect(field.type_class.resolve_proc).to eq(expected)
- else
- expect(field.type_class.resolver).to eq(expected)
- end
+ expect(field.resolver).to eq(expected)
end
end
RSpec::Matchers.define :have_graphql_extension do |expected|
match do |field|
- expect(field.type_class.extensions).to include(expected)
+ expect(field.extensions).to include(expected)
end
end
diff --git a/spec/support/matchers/make_queries.rb b/spec/support/matchers/make_queries.rb
new file mode 100644
index 00000000000..19c69240a40
--- /dev/null
+++ b/spec/support/matchers/make_queries.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+RSpec::Matchers.define :make_queries do |expected_count = nil|
+ supports_block_expectations
+
+ match do |block|
+ @recorder = ActiveRecord::QueryRecorder.new(&block)
+ @counter = @recorder.count
+ if expected_count
+ @counter == expected_count
+ else
+ @counter > 0
+ end
+ end
+
+ failure_message do |_|
+ if expected_count
+ "expected to make #{expected_count} queries but made #{@counter} queries"
+ else
+ "expected to make queries but did not make any"
+ end
+ end
+
+ failure_message_when_negated do |_|
+ if expected_count
+ "expected not to make #{expected_count} queries but received #{@counter} queries"
+ else
+ "expected not to make queries but received #{@counter} queries"
+ end
+ end
+end
diff --git a/spec/support/rspec.rb b/spec/support/rspec.rb
index b4a25fd121d..30e48b3baf1 100644
--- a/spec/support/rspec.rb
+++ b/spec/support/rspec.rb
@@ -14,6 +14,8 @@ require_relative "helpers/fast_rails_root"
require 'rubocop'
require 'rubocop/rspec/support'
+RSpec::Expectations.configuration.on_potential_false_positives = :raise
+
RSpec.configure do |config|
config.mock_with :rspec do |mocks|
mocks.verify_doubled_constant_names = true
diff --git a/spec/support/shared_contexts/email_shared_context.rb b/spec/support/shared_contexts/email_shared_context.rb
index 0dc66eeb2ee..086cdf50e9d 100644
--- a/spec/support/shared_contexts/email_shared_context.rb
+++ b/spec/support/shared_contexts/email_shared_context.rb
@@ -148,7 +148,7 @@ RSpec.shared_examples :note_handler_shared_examples do |forwardable|
end
it 'allows email to only have quoted text', if: forwardable do
- expect { receiver.execute }.not_to raise_error(Gitlab::Email::EmptyEmailError)
+ expect { receiver.execute }.not_to raise_error
end
end
diff --git a/spec/support/shared_contexts/graphql/requests/packages_shared_context.rb b/spec/support/shared_contexts/graphql/requests/packages_shared_context.rb
index 13e7ecf2669..b29a231f3a6 100644
--- a/spec/support/shared_contexts/graphql/requests/packages_shared_context.rb
+++ b/spec/support/shared_contexts/graphql/requests/packages_shared_context.rb
@@ -14,7 +14,7 @@ RSpec.shared_context 'package details setup' do
let(:user) { project.first_owner }
let(:package_details) { graphql_data_at(:package) }
let(:metadata_response) { graphql_data_at(:package, :metadata) }
- let(:first_file) { package.package_files.find { |f| global_id_of(f) == first_file_response['id'] } }
+ let(:first_file) { package.package_files.find { |f| a_graphql_entity_for(f).matches?(first_file_response) } }
let(:package_files_response) { graphql_data_at(:package, :package_files, :nodes) }
let(:first_file_response) { graphql_data_at(:package, :package_files, :nodes, 0)}
let(:first_file_response_metadata) { graphql_data_at(:package, :package_files, :nodes, 0, :file_metadata)}
diff --git a/spec/support/shared_contexts/lib/gitlab/sidekiq_logging/structured_logger_shared_context.rb b/spec/support/shared_contexts/lib/gitlab/sidekiq_logging/structured_logger_shared_context.rb
index b7966e25b38..7d51c90522a 100644
--- a/spec/support/shared_contexts/lib/gitlab/sidekiq_logging/structured_logger_shared_context.rb
+++ b/spec/support/shared_contexts/lib/gitlab/sidekiq_logging/structured_logger_shared_context.rb
@@ -57,7 +57,8 @@ RSpec.shared_context 'structured_logger' do
'job_status' => 'done',
'duration_s' => 0.0,
'completed_at' => timestamp.to_f,
- 'cpu_s' => 1.111112
+ 'cpu_s' => 1.111112,
+ 'rate_limiting_gates' => []
)
end
diff --git a/spec/support/shared_contexts/models/concerns/integrations/enable_ssl_verification_shared_context.rb b/spec/support/shared_contexts/models/concerns/integrations/enable_ssl_verification_shared_context.rb
index c698e06c2a2..fbec6f98e76 100644
--- a/spec/support/shared_contexts/models/concerns/integrations/enable_ssl_verification_shared_context.rb
+++ b/spec/support/shared_contexts/models/concerns/integrations/enable_ssl_verification_shared_context.rb
@@ -43,5 +43,9 @@ RSpec.shared_context Integrations::EnableSslVerification do
expect(names.index('enable_ssl_verification')).to eq insert_index
end
+
+ it 'does not insert the field repeatedly' do
+ expect(integration.fields.pluck(:name)).to eq(integration.fields.pluck(:name))
+ end
end
end
diff --git a/spec/support/shared_contexts/navbar_structure_context.rb b/spec/support/shared_contexts/navbar_structure_context.rb
index 65c7f63cf6e..ef6ff7be840 100644
--- a/spec/support/shared_contexts/navbar_structure_context.rb
+++ b/spec/support/shared_contexts/navbar_structure_context.rb
@@ -46,8 +46,7 @@ RSpec.shared_context 'project navbar structure' do
_('List'),
_('Boards'),
_('Service Desk'),
- _('Milestones'),
- (_('Iterations') if Gitlab.ee?)
+ _('Milestones')
]
},
{
@@ -74,6 +73,13 @@ RSpec.shared_context 'project navbar structure' do
]
},
{
+ nav_item: _('Infrastructure'),
+ nav_sub_items: [
+ _('Kubernetes clusters'),
+ _('Terraform')
+ ]
+ },
+ {
nav_item: _('Monitor'),
nav_sub_items: [
_('Metrics'),
@@ -86,16 +92,8 @@ RSpec.shared_context 'project navbar structure' do
]
},
{
- nav_item: _('Infrastructure'),
- nav_sub_items: [
- _('Kubernetes clusters'),
- _('Serverless platform'),
- _('Terraform')
- ]
- },
- {
nav_item: _('Analytics'),
- nav_sub_items: analytics_sub_nav_item
+ nav_sub_items: project_analytics_sub_nav_item
},
{
nav_item: _('Wiki'),
@@ -126,9 +124,7 @@ RSpec.shared_context 'group navbar structure' do
let(:analytics_nav_item) do
{
nav_item: _('Analytics'),
- nav_sub_items: [
- _('Contribution')
- ]
+ nav_sub_items: group_analytics_sub_nav_item
}
end
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 76db2bd82f1..483bca07ba6 100644
--- a/spec/support/shared_contexts/policies/group_policy_shared_context.rb
+++ b/spec/support/shared_contexts/policies/group_policy_shared_context.rb
@@ -28,11 +28,11 @@ RSpec.shared_context 'GroupPolicy context' do
let(:reporter_permissions) do
%i[
admin_label
+ admin_milestone
admin_issue_board
read_container_image
read_metrics_dashboard_annotation
read_prometheus
- read_package_settings
read_crm_contact
read_crm_organization
]
@@ -40,13 +40,11 @@ RSpec.shared_context 'GroupPolicy context' do
let(:developer_permissions) do
%i[
- admin_milestone
create_metrics_dashboard_annotation
delete_metrics_dashboard_annotation
update_metrics_dashboard_annotation
create_custom_emoji
create_package
- create_package_settings
read_cluster
]
end
@@ -54,6 +52,7 @@ RSpec.shared_context 'GroupPolicy context' do
let(:maintainer_permissions) do
%i[
destroy_package
+ admin_package
create_projects
create_cluster update_cluster admin_cluster add_cluster
]
diff --git a/spec/support/shared_contexts/policies/project_policy_shared_context.rb b/spec/support/shared_contexts/policies/project_policy_shared_context.rb
index a78953e8199..e50083a10e7 100644
--- a/spec/support/shared_contexts/policies/project_policy_shared_context.rb
+++ b/spec/support/shared_contexts/policies/project_policy_shared_context.rb
@@ -25,7 +25,7 @@ RSpec.shared_context 'ProjectPolicy context' do
let(:base_reporter_permissions) do
%i[
- admin_issue admin_issue_link admin_label admin_issue_board_list
+ admin_issue admin_issue_link admin_label admin_milestone admin_issue_board_list
create_snippet create_incident daily_statistics create_merge_request_in download_code
download_wiki_code fork_project metrics_dashboard read_build
read_commit_status read_confidential_issues read_container_image
@@ -41,7 +41,7 @@ RSpec.shared_context 'ProjectPolicy context' do
let(:developer_permissions) do
%i[
- admin_merge_request admin_milestone admin_tag create_build
+ admin_merge_request admin_tag create_build
create_commit_status create_container_image create_deployment
create_environment create_merge_request_from
create_metrics_dashboard_annotation create_pipeline create_release
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 3453f954c9d..e8ccb12e6b7 100644
--- a/spec/support/shared_contexts/sentry_error_tracking_shared_context.rb
+++ b/spec/support/shared_contexts/sentry_error_tracking_shared_context.rb
@@ -14,7 +14,7 @@ RSpec.shared_context 'sentry error tracking context' do
end
before do
- expect(project).to receive(:error_tracking_setting).at_least(:once).and_return(error_tracking_setting)
+ allow(project).to receive(:error_tracking_setting).at_least(:once).and_return(error_tracking_setting)
project.add_reporter(user)
end
diff --git a/spec/support/shared_contexts/services/service_ping/stubbed_service_ping_metrics_definitions_shared_context.rb b/spec/support/shared_contexts/services/service_ping/stubbed_service_ping_metrics_definitions_shared_context.rb
index 37d410a35bf..9746d287440 100644
--- a/spec/support/shared_contexts/services/service_ping/stubbed_service_ping_metrics_definitions_shared_context.rb
+++ b/spec/support/shared_contexts/services/service_ping/stubbed_service_ping_metrics_definitions_shared_context.rb
@@ -43,12 +43,12 @@ RSpec.shared_context 'stubbed service ping metrics definitions' do
Gitlab::Usage::MetricDefinition.instance_variable_set(:@all, nil)
end
- def metric_attributes(key_path, category, value_type = 'string', instrumentation_class = '')
+ def metric_attributes(key_path, category, value_type = 'string', instrumentation_class = '', status = 'active')
{
'key_path' => key_path,
'data_category' => category,
'value_type' => value_type,
- 'status' => 'active',
+ 'status' => status,
'instrumentation_class' => instrumentation_class,
'time_frame' => 'all'
}
diff --git a/spec/support/shared_examples/ci/log_downstream_pipeline_shared_examples.rb b/spec/support/shared_examples/ci/log_downstream_pipeline_shared_examples.rb
new file mode 100644
index 00000000000..db724dcfe99
--- /dev/null
+++ b/spec/support/shared_examples/ci/log_downstream_pipeline_shared_examples.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'logs downstream pipeline creation' do
+ def record_downstream_pipeline_logs
+ logs = []
+ allow(::Gitlab::AppLogger).to receive(:info) do |args|
+ logs << args
+ end
+
+ yield
+
+ logs.find { |log| log[:message] == "downstream pipeline created" }
+ end
+
+ it 'logs details' do
+ pipeline = nil
+
+ log_entry = record_downstream_pipeline_logs do
+ pipeline = subject
+ end
+
+ expect(log_entry).to be_present
+ expect(log_entry).to eq(
+ message: "downstream pipeline created",
+ class: described_class.name,
+ root_pipeline_id: expected_root_pipeline.id,
+ downstream_pipeline_id: pipeline.id,
+ downstream_pipeline_relationship: expected_downstream_relationship,
+ hierarchy_size: expected_hierarchy_size,
+ root_pipeline_plan: expected_root_pipeline.project.actual_plan_name,
+ root_pipeline_namespace_path: expected_root_pipeline.project.namespace.full_path,
+ root_pipeline_project_path: expected_root_pipeline.project.full_path)
+ end
+end
diff --git a/spec/support/shared_examples/controllers/environments_controller_shared_examples.rb b/spec/support/shared_examples/controllers/environments_controller_shared_examples.rb
index c6e880635aa..a79b94209f3 100644
--- a/spec/support/shared_examples/controllers/environments_controller_shared_examples.rb
+++ b/spec/support/shared_examples/controllers/environments_controller_shared_examples.rb
@@ -65,3 +65,20 @@ RSpec.shared_examples 'failed response for #cancel_auto_stop' do
end
end
end
+
+RSpec.shared_examples 'avoids N+1 queries on environment detail page' do
+ render_views
+
+ before do
+ create_deployment_with_associations(sequence: 0)
+ end
+
+ it 'avoids N+1 queries' do
+ control = ActiveRecord::QueryRecorder.new { get :show, params: environment_params }
+
+ create_deployment_with_associations(sequence: 1)
+ create_deployment_with_associations(sequence: 2)
+
+ expect { get :show, params: environment_params }.not_to exceed_query_limit(control.count).with_threshold(34)
+ end
+end
diff --git a/spec/support/shared_examples/controllers/repository_lfs_file_load_shared_examples.rb b/spec/support/shared_examples/controllers/repository_lfs_file_load_shared_examples.rb
index fadf428125a..9cf35325202 100644
--- a/spec/support/shared_examples/controllers/repository_lfs_file_load_shared_examples.rb
+++ b/spec/support/shared_examples/controllers/repository_lfs_file_load_shared_examples.rb
@@ -44,8 +44,10 @@ RSpec.shared_examples 'a controller that can serve LFS files' do |options = {}|
expect(controller).to receive(:send_file)
.with(
File.join(lfs_uploader.root, lfs_uploader.store_dir, lfs_uploader.filename),
- filename: filename,
- disposition: 'attachment')
+ {
+ filename: filename,
+ disposition: 'attachment'
+ })
subject
diff --git a/spec/support/shared_examples/features/content_editor_shared_examples.rb b/spec/support/shared_examples/features/content_editor_shared_examples.rb
index 5c44cb7f04b..c93d8e3d511 100644
--- a/spec/support/shared_examples/features/content_editor_shared_examples.rb
+++ b/spec/support/shared_examples/features/content_editor_shared_examples.rb
@@ -23,6 +23,8 @@ RSpec.shared_examples 'edits content using the content editor' do
describe 'code block bubble menu' do
it 'shows a code block bubble menu for a code block' do
+ find(content_editor_testid).send_keys [:enter, :enter]
+
find(content_editor_testid).send_keys '```js ' # trigger input rule
find(content_editor_testid).send_keys 'var a = 0'
find(content_editor_testid).send_keys [:shift, :left]
@@ -32,6 +34,8 @@ RSpec.shared_examples 'edits content using the content editor' do
end
it 'sets code block type to "javascript" for `js`' do
+ find(content_editor_testid).send_keys [:enter, :enter]
+
find(content_editor_testid).send_keys '```js '
find(content_editor_testid).send_keys 'var a = 0'
@@ -39,6 +43,8 @@ RSpec.shared_examples 'edits content using the content editor' do
end
it 'sets code block type to "Custom (nomnoml)" for `nomnoml`' do
+ find(content_editor_testid).send_keys [:enter, :enter]
+
find(content_editor_testid).send_keys '```nomnoml '
find(content_editor_testid).send_keys 'test'
diff --git a/spec/support/shared_examples/features/dependency_proxy_shared_examples.rb b/spec/support/shared_examples/features/dependency_proxy_shared_examples.rb
index 5d1488502d2..6fd844f0e5f 100644
--- a/spec/support/shared_examples/features/dependency_proxy_shared_examples.rb
+++ b/spec/support/shared_examples/features/dependency_proxy_shared_examples.rb
@@ -17,7 +17,7 @@ end
RSpec.shared_examples 'a successful manifest pull' do
it 'sends a file' do
- expect(controller).to receive(:send_file).with(manifest.file.path, type: manifest.content_type)
+ expect(controller).to receive(:send_file).with(manifest.file.path, { type: manifest.content_type })
subject
end
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 ccd063faac4..2fff4137934 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
@@ -48,7 +48,7 @@ RSpec.shared_examples 'an editable merge request' do
end
page.within '.reviewer' do
- expect(page).to have_content user.username
+ expect(page).to have_content user.name
end
page.within '.milestone' do
diff --git a/spec/support/shared_examples/features/inviting_groups_shared_examples.rb b/spec/support/shared_examples/features/inviting_groups_shared_examples.rb
new file mode 100644
index 00000000000..4921676a065
--- /dev/null
+++ b/spec/support/shared_examples/features/inviting_groups_shared_examples.rb
@@ -0,0 +1,144 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'inviting groups search results' do
+ context 'with instance admin considerations' do
+ let_it_be(:group_to_invite) { create(:group) }
+
+ context 'when user is an admin' do
+ let_it_be(:admin) { create(:admin) }
+
+ before do
+ sign_in(admin)
+ gitlab_enable_admin_mode_sign_in(admin)
+ end
+
+ it 'shows groups where the admin has no direct membership' do
+ visit members_page_path
+
+ click_on 'Invite a group'
+ click_on 'Select a group'
+ wait_for_requests
+
+ page.within(group_dropdown_selector) do
+ expect_to_have_group(group_to_invite)
+ expect_not_to_have_group(group)
+ end
+ end
+
+ it 'shows groups where the admin has at least guest level membership' do
+ group_to_invite.add_guest(admin)
+
+ visit members_page_path
+
+ click_on 'Invite a group'
+ click_on 'Select a group'
+ wait_for_requests
+
+ page.within(group_dropdown_selector) do
+ expect_to_have_group(group_to_invite)
+ expect_not_to_have_group(group)
+ end
+ end
+ end
+
+ context 'when user is not an admin' do
+ before do
+ group.add_owner(user)
+ sign_in(user)
+ end
+
+ it 'does not show groups where the user has no direct membership' do
+ visit members_page_path
+
+ click_on 'Invite a group'
+ click_on 'Select a group'
+ wait_for_requests
+
+ page.within(group_dropdown_selector) do
+ expect_not_to_have_group(group_to_invite)
+ expect_not_to_have_group(group)
+ end
+ end
+
+ it 'shows groups where the user has at least guest level membership' do
+ group_to_invite.add_guest(user)
+
+ visit members_page_path
+
+ click_on 'Invite a group'
+ click_on 'Select a group'
+ wait_for_requests
+
+ page.within(group_dropdown_selector) do
+ expect_to_have_group(group_to_invite)
+ expect_not_to_have_group(group)
+ end
+ end
+ end
+ end
+
+ context 'when user is not an admin and there are hierarchy considerations' do
+ let_it_be(:group_outside_hierarchy) { create(:group) }
+
+ before_all do
+ group.add_owner(user)
+ group_within_hierarchy.add_owner(user)
+ group_outside_hierarchy.add_owner(user)
+ end
+
+ before do
+ sign_in(user)
+ end
+
+ it 'does not show self or ancestors', :aggregate_failures do
+ group_sibling = create(:group, parent: group)
+ group_sibling.add_owner(user)
+
+ visit members_page_path_within_hierarchy
+
+ click_on 'Invite a group'
+ click_on 'Select a group'
+ wait_for_requests
+
+ page.within(group_dropdown_selector) do
+ expect_to_have_group(group_outside_hierarchy)
+ expect_to_have_group(group_sibling)
+ expect_not_to_have_group(group)
+ expect_not_to_have_group(group_within_hierarchy)
+ end
+ end
+
+ context 'when sharing with groups outside the hierarchy is enabled' do
+ it 'shows groups within and outside the hierarchy in search results' do
+ visit members_page_path
+
+ click_on 'Invite a group'
+ click_on 'Select a group'
+ wait_for_requests
+
+ page.within(group_dropdown_selector) do
+ expect_to_have_group(group_within_hierarchy)
+ expect_to_have_group(group_outside_hierarchy)
+ end
+ end
+ end
+
+ context 'when sharing with groups outside the hierarchy is disabled' do
+ before do
+ group.update!(prevent_sharing_groups_outside_hierarchy: true)
+ end
+
+ it 'shows only groups within the hierarchy in search results' do
+ visit members_page_path
+
+ click_on 'Invite a group'
+ click_on 'Select a group'
+
+ page.within(group_dropdown_selector) do
+ expect_to_have_group(group_within_hierarchy)
+ expect_not_to_have_group(group_outside_hierarchy)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/features/manage_applications_shared_examples.rb b/spec/support/shared_examples/features/manage_applications_shared_examples.rb
index 3a8267b21da..442264e7ae4 100644
--- a/spec/support/shared_examples/features/manage_applications_shared_examples.rb
+++ b/spec/support/shared_examples/features/manage_applications_shared_examples.rb
@@ -9,11 +9,9 @@ RSpec.shared_examples 'manage applications' do
visit new_application_path
expect(page).to have_content 'Add new application'
- expect(find('#doorkeeper_application_expire_access_tokens')).to be_checked
fill_in :doorkeeper_application_name, with: application_name
fill_in :doorkeeper_application_redirect_uri, with: application_redirect_uri
- uncheck :doorkeeper_application_expire_access_tokens
check :doorkeeper_application_scopes_read_user
click_on 'Save application'
@@ -25,8 +23,6 @@ RSpec.shared_examples 'manage applications' do
click_on 'Edit'
- expect(find('#doorkeeper_application_expire_access_tokens')).not_to be_checked
-
application_name_changed = "#{application_name} changed"
fill_in :doorkeeper_application_name, with: application_name_changed
diff --git a/spec/support/shared_examples/features/multiple_assignees_mr_shared_examples.rb b/spec/support/shared_examples/features/multiple_assignees_mr_shared_examples.rb
index 9d023d9514a..4565108b5e4 100644
--- a/spec/support/shared_examples/features/multiple_assignees_mr_shared_examples.rb
+++ b/spec/support/shared_examples/features/multiple_assignees_mr_shared_examples.rb
@@ -4,7 +4,7 @@ RSpec.shared_examples 'multiple assignees merge request' do |action, save_button
it "#{action} a MR with multiple assignees", :js do
find('.js-assignee-search').click
page.within '.dropdown-menu-user' do
- click_link user.name
+ click_link user.name unless action == 'creates'
click_link user2.name
end
diff --git a/spec/support/shared_examples/features/multiple_assignees_widget_mr_shared_examples.rb b/spec/support/shared_examples/features/multiple_assignees_widget_mr_shared_examples.rb
index bbde448a1a1..a44a699c878 100644
--- a/spec/support/shared_examples/features/multiple_assignees_widget_mr_shared_examples.rb
+++ b/spec/support/shared_examples/features/multiple_assignees_widget_mr_shared_examples.rb
@@ -4,7 +4,7 @@ RSpec.shared_examples 'multiple assignees widget merge request' do |action, save
it "#{action} a MR with multiple assignees", :js do
find('.js-assignee-search').click
page.within '.dropdown-menu-user' do
- click_link user.name
+ click_link user.name unless action == 'creates'
click_link user2.name
end
diff --git a/spec/support/shared_examples/features/multiple_reviewers_mr_shared_examples.rb b/spec/support/shared_examples/features/multiple_reviewers_mr_shared_examples.rb
index ad6ca3e1900..48cde90bd9b 100644
--- a/spec/support/shared_examples/features/multiple_reviewers_mr_shared_examples.rb
+++ b/spec/support/shared_examples/features/multiple_reviewers_mr_shared_examples.rb
@@ -40,7 +40,7 @@ RSpec.shared_examples 'multiple reviewers merge request' do |action, save_button
# Closing dropdown to persist
click_link 'Edit'
- expect(page).to have_content user2.username
+ expect(page).to have_content user2.name
end
end
end
diff --git a/spec/support/shared_examples/features/packages_shared_examples.rb b/spec/support/shared_examples/features/packages_shared_examples.rb
index ded30f32314..323bd4f5171 100644
--- a/spec/support/shared_examples/features/packages_shared_examples.rb
+++ b/spec/support/shared_examples/features/packages_shared_examples.rb
@@ -97,9 +97,9 @@ def click_sort_option(option, ascending)
wait_for_requests
end
- find('button.gl-dropdown-toggle').click
+ find('[data-testid="registry-sort-dropdown"]').click
- page.within('.dropdown-menu') do
+ page.within('[data-testid="registry-sort-dropdown"] .dropdown-menu') do
click_button option
end
diff --git a/spec/support/shared_examples/features/sidebar_shared_examples.rb b/spec/support/shared_examples/features/sidebar_shared_examples.rb
index 11d216ff4b6..af3ea0600a2 100644
--- a/spec/support/shared_examples/features/sidebar_shared_examples.rb
+++ b/spec/support/shared_examples/features/sidebar_shared_examples.rb
@@ -108,7 +108,11 @@ RSpec.shared_examples 'issue boards sidebar' do
wait_for_requests
- expect(page).to have_content('This issue is confidential')
+ expect(page).to have_content(
+ _('Only project members with at least' \
+ ' Reporter role can view or be' \
+ ' notified about this issue.')
+ )
end
end
end
diff --git a/spec/support/shared_examples/features/wiki/user_creates_wiki_page_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_creates_wiki_page_shared_examples.rb
index 41b1964cff0..8081c51577a 100644
--- a/spec/support/shared_examples/features/wiki/user_creates_wiki_page_shared_examples.rb
+++ b/spec/support/shared_examples/features/wiki/user_creates_wiki_page_shared_examples.rb
@@ -233,6 +233,23 @@ RSpec.shared_examples 'User creates wiki page' do
.and have_content("Last edited by #{user.name}")
.and have_content("My awesome wiki!")
end
+
+ context 'when a server side validation error is returned' do
+ it "still displays edit form", :js do
+ click_link("New page")
+
+ page.within(".wiki-form") do
+ fill_in(:wiki_title, with: "home")
+ fill_in(:wiki_content, with: "My awesome home page!")
+ end
+
+ # Submits page with a name already in use to trigger a validation error
+ click_button("Create page")
+
+ expect(page).to have_field(:wiki_title)
+ expect(page).to have_field(:wiki_content)
+ end
+ end
end
it "shows the emoji autocompletion dropdown", :js do
diff --git a/spec/support/shared_examples/graphql/mutations/incident_management_timeline_events_shared_examples.rb b/spec/support/shared_examples/graphql/mutations/incident_management_timeline_events_shared_examples.rb
new file mode 100644
index 00000000000..b989dbc6524
--- /dev/null
+++ b/spec/support/shared_examples/graphql/mutations/incident_management_timeline_events_shared_examples.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+# Requres:
+# * subject with a 'resolve' name
+# * Defined expected timeline event via `let(:expected_timeline_event) { instance_double(...) }`
+RSpec.shared_examples 'creating an incident timeline event' do
+ it 'creates a timeline event' do
+ expect { resolve }.to change(IncidentManagement::TimelineEvent, :count).by(1)
+ end
+
+ it 'responds with a timeline event', :aggregate_failures do
+ response = resolve
+ timeline_event = IncidentManagement::TimelineEvent.last!
+
+ expect(response).to match(timeline_event: timeline_event, errors: be_empty)
+
+ expect(timeline_event.promoted_from_note).to eq(expected_timeline_event.promoted_from_note)
+ expect(timeline_event.note).to eq(expected_timeline_event.note)
+ expect(timeline_event.occurred_at.to_s).to eq(expected_timeline_event.occurred_at)
+ expect(timeline_event.incident).to eq(expected_timeline_event.incident)
+ expect(timeline_event.author).to eq(expected_timeline_event.author)
+ end
+end
+
+# Requres
+# * subject with a 'resolve' name
+# * a user factory with a 'current_user' name
+RSpec.shared_examples 'failing to create an incident timeline event' do
+ context 'when a user has no permissions to create timeline event' do
+ before do
+ project.add_guest(current_user)
+ end
+
+ it 'raises an error' do
+ expect { resolve }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
+ end
+ end
+end
+
+# Requres:
+# * subject with a 'resolve' name
+RSpec.shared_examples 'responding with an incident timeline errors' do |errors:|
+ it 'returns errors' do
+ expect(resolve).to eq(timeline_event: nil, errors: errors)
+ end
+end
diff --git a/spec/support/shared_examples/graphql/resolvers/packages_resolvers_shared_examples.rb b/spec/support/shared_examples/graphql/resolvers/packages_resolvers_shared_examples.rb
index 3d6fec85490..da8562161e7 100644
--- a/spec/support/shared_examples/graphql/resolvers/packages_resolvers_shared_examples.rb
+++ b/spec/support/shared_examples/graphql/resolvers/packages_resolvers_shared_examples.rb
@@ -4,7 +4,11 @@ RSpec.shared_examples 'group and projects packages resolver' do
context 'without sort' do
let_it_be(:npm_package) { create(:package, project: project) }
- it { is_expected.to contain_exactly(npm_package) }
+ it 'returns the proper packages' do
+ expect(::Packages::Package).not_to receive(:preload_pipelines)
+
+ expect(subject).to contain_exactly(npm_package)
+ end
end
context 'with sorting and filtering' do
diff --git a/spec/support/shared_examples/graphql/sorted_paginated_query_shared_examples.rb b/spec/support/shared_examples/graphql/sorted_paginated_query_shared_examples.rb
index 37a805902a9..6d6e7b761f6 100644
--- a/spec/support/shared_examples/graphql/sorted_paginated_query_shared_examples.rb
+++ b/spec/support/shared_examples/graphql/sorted_paginated_query_shared_examples.rb
@@ -101,7 +101,7 @@ RSpec.shared_examples 'sorted paginated query' do |conditions = {}|
context 'when sorting' do
it 'sorts correctly' do
- expect(results).to eq all_records
+ expect(results).to match all_records
end
context 'when paginating' do
@@ -110,17 +110,17 @@ RSpec.shared_examples 'sorted paginated query' do |conditions = {}|
let(:rest) { all_records.drop(first_param) }
it 'paginates correctly' do
- expect(results).to eq first_page
+ expect(results).to match first_page
fwds = pagination_query(sort_argument.merge(after: end_cursor))
post_graphql(fwds, current_user: current_user)
- expect(results).to eq rest
+ expect(results).to match rest
bwds = pagination_query(sort_argument.merge(before: start_cursor))
post_graphql(bwds, current_user: current_user)
- expect(results).to eq first_page
+ expect(results).to match first_page
end
end
@@ -130,7 +130,7 @@ RSpec.shared_examples 'sorted paginated query' do |conditions = {}|
it 'fetches last elements without error' do
post_graphql(pagination_query(params), current_user: current_user)
- expect(results.first).to eq(all_records.last)
+ expect(results.first).to match all_records.last
end
end
end
diff --git a/spec/support/shared_examples/graphql/types/gitlab_style_deprecations_shared_examples.rb b/spec/support/shared_examples/graphql/types/gitlab_style_deprecations_shared_examples.rb
index 3caf153c2fa..cf9c36fafe8 100644
--- a/spec/support/shared_examples/graphql/types/gitlab_style_deprecations_shared_examples.rb
+++ b/spec/support/shared_examples/graphql/types/gitlab_style_deprecations_shared_examples.rb
@@ -6,7 +6,7 @@ RSpec.shared_examples 'Gitlab-style deprecations' do
expect { subject(deprecation_reason: 'foo') }.to raise_error(
ArgumentError,
'Use `deprecated` property instead of `deprecation_reason`. ' \
- 'See https://docs.gitlab.com/ee/development/api_graphql_styleguide.html#deprecating-fields-arguments-and-enum-values'
+ 'See https://docs.gitlab.com/ee/development/api_graphql_styleguide.html#deprecating-schema-items'
)
end
diff --git a/spec/support/shared_examples/lib/gitlab/ci/ci_trace_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/ci/ci_trace_shared_examples.rb
index a3c67210a4a..e886ec65b02 100644
--- a/spec/support/shared_examples/lib/gitlab/ci/ci_trace_shared_examples.rb
+++ b/spec/support/shared_examples/lib/gitlab/ci/ci_trace_shared_examples.rb
@@ -796,8 +796,8 @@ RSpec.shared_examples 'trace with enabled live trace feature' do
end
end
- describe '#archived_trace_exist?' do
- subject { trace.archived_trace_exist? }
+ describe '#archived?' do
+ subject { trace.archived? }
context 'when trace does not exist' do
it { is_expected.to be_falsy }
diff --git a/spec/support/shared_examples/lib/gitlab/cycle_analytics/deployment_metrics.rb b/spec/support/shared_examples/lib/gitlab/cycle_analytics/deployment_metrics.rb
index bea7cca2744..beec072e474 100644
--- a/spec/support/shared_examples/lib/gitlab/cycle_analytics/deployment_metrics.rb
+++ b/spec/support/shared_examples/lib/gitlab/cycle_analytics/deployment_metrics.rb
@@ -62,8 +62,8 @@ shared_examples 'deployment metrics examples' do
describe '#deployment_frequency' do
subject { stage_summary.fourth[:value] }
- it 'includes the unit: `per day`' do
- expect(stage_summary.fourth[:unit]).to eq _('per day')
+ it 'includes the unit: `/day`' do
+ expect(stage_summary.fourth[:unit]).to eq _('/day')
end
before do
diff --git a/spec/support/shared_examples/lib/gitlab/database/reestablished_connection_stack_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/database/reestablished_connection_stack_shared_examples.rb
new file mode 100644
index 00000000000..67d739b79ab
--- /dev/null
+++ b/spec/support/shared_examples/lib/gitlab/database/reestablished_connection_stack_shared_examples.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+RSpec.shared_context 'reconfigures connection stack' do |db_config_name|
+ before do
+ skip_if_multiple_databases_not_setup
+
+ # Due to lib/gitlab/database/load_balancing/configuration.rb:92 requiring RequestStore
+ # we cannot use stub_feature_flags(force_no_sharing_primary_model: true)
+ Gitlab::Database.database_base_models.each do |_, model_class|
+ allow(model_class.load_balancer.configuration).to receive(:use_dedicated_connection?).and_return(true)
+ end
+
+ ActiveRecord::Base.establish_connection(db_config_name.to_sym) # rubocop:disable Database/EstablishConnection
+
+ expect(Gitlab::Database.db_config_name(ActiveRecord::Base.connection)) # rubocop:disable Database/MultipleDatabases
+ .to eq(db_config_name)
+ end
+
+ around do |example|
+ with_reestablished_active_record_base do
+ example.run
+ end
+ end
+
+ def validate_connections!
+ model_connections = Gitlab::Database.database_base_models.to_h do |db_config_name, model_class|
+ [model_class, Gitlab::Database.db_config_name(model_class.connection)]
+ end
+
+ expect(model_connections).to eq(Gitlab::Database.database_base_models.invert)
+ end
+end
diff --git a/spec/support/shared_examples/lib/gitlab/kubernetes/network_policy_common_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/kubernetes/network_policy_common_shared_examples.rb
deleted file mode 100644
index 2633a89eeee..00000000000
--- a/spec/support/shared_examples/lib/gitlab/kubernetes/network_policy_common_shared_examples.rb
+++ /dev/null
@@ -1,162 +0,0 @@
-# frozen_string_literal: true
-
-RSpec.shared_examples 'network policy common specs' do
- let(:name) { 'example-name' }
- let(:namespace) { 'example-namespace' }
- let(:labels) { nil }
-
- describe '#generate' do
- subject { policy.generate }
-
- it { is_expected.to eq(Kubeclient::Resource.new(policy.resource)) }
- end
-
- describe 'as_json' do
- let(:json_policy) do
- {
- name: name,
- namespace: namespace,
- creation_timestamp: nil,
- manifest: YAML.dump(policy.resource.deep_stringify_keys),
- is_autodevops: false,
- is_enabled: true,
- environment_ids: []
- }
- end
-
- subject { policy.as_json }
-
- it { is_expected.to eq(json_policy) }
- end
-
- describe 'autodevops?' do
- subject { policy.autodevops? }
-
- let(:labels) { { chart: chart } }
- let(:chart) { nil }
-
- it { is_expected.to be false }
-
- context 'with non-autodevops chart' do
- let(:chart) { 'foo' }
-
- it { is_expected.to be false }
- end
-
- context 'with autodevops chart' do
- let(:chart) { 'auto-deploy-app-0.6.0' }
-
- it { is_expected.to be true }
- end
- end
-
- describe 'enabled?' do
- subject { policy.enabled? }
-
- let(:selector) { nil }
-
- it { is_expected.to be true }
-
- context 'with empty selector' do
- let(:selector) { {} }
-
- it { is_expected.to be true }
- end
-
- context 'with nil matchLabels in selector' do
- let(:selector) { { matchLabels: nil } }
-
- it { is_expected.to be true }
- end
-
- context 'with empty matchLabels in selector' do
- let(:selector) { { matchLabels: {} } }
-
- it { is_expected.to be true }
- end
-
- context 'with disabled_by label in matchLabels in selector' do
- let(:selector) do
- { matchLabels: { Gitlab::Kubernetes::NetworkPolicyCommon::DISABLED_BY_LABEL => 'gitlab' } }
- end
-
- it { is_expected.to be false }
- end
- end
-
- describe 'enable' do
- subject { policy.enabled? }
-
- let(:selector) { nil }
-
- before do
- policy.enable
- end
-
- it { is_expected.to be true }
-
- context 'with empty selector' do
- let(:selector) { {} }
-
- it { is_expected.to be true }
- end
-
- context 'with nil matchLabels in selector' do
- let(:selector) { { matchLabels: nil } }
-
- it { is_expected.to be true }
- end
-
- context 'with empty matchLabels in selector' do
- let(:selector) { { matchLabels: {} } }
-
- it { is_expected.to be true }
- end
-
- context 'with disabled_by label in matchLabels in selector' do
- let(:selector) do
- { matchLabels: { Gitlab::Kubernetes::NetworkPolicyCommon::DISABLED_BY_LABEL => 'gitlab' } }
- end
-
- it { is_expected.to be true }
- end
- end
-
- describe 'disable' do
- subject { policy.enabled? }
-
- let(:selector) { nil }
-
- before do
- policy.disable
- end
-
- it { is_expected.to be false }
-
- context 'with empty selector' do
- let(:selector) { {} }
-
- it { is_expected.to be false }
- end
-
- context 'with nil matchLabels in selector' do
- let(:selector) { { matchLabels: nil } }
-
- it { is_expected.to be false }
- end
-
- context 'with empty matchLabels in selector' do
- let(:selector) { { matchLabels: {} } }
-
- it { is_expected.to be false }
- end
-
- context 'with disabled_by label in matchLabels in selector' do
- let(:selector) do
- { matchLabels: { Gitlab::Kubernetes::NetworkPolicyCommon::DISABLED_BY_LABEL => 'gitlab' } }
- end
-
- it { is_expected.to be false }
- end
- end
-end
diff --git a/spec/support/shared_examples/merge_request_author_auto_assign_shared_examples.rb b/spec/support/shared_examples/merge_request_author_auto_assign_shared_examples.rb
new file mode 100644
index 00000000000..d4986975f03
--- /dev/null
+++ b/spec/support/shared_examples/merge_request_author_auto_assign_shared_examples.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'merge request author auto assign' do
+ it 'populates merge request author as assignee' do
+ expect(find('.js-assignee-search')).to have_content(user.name)
+ expect(page).not_to have_content 'Assign yourself'
+ end
+end
diff --git a/spec/support/shared_examples/models/chat_integration_shared_examples.rb b/spec/support/shared_examples/models/chat_integration_shared_examples.rb
index e6b270c6188..fa10b03fa90 100644
--- a/spec/support/shared_examples/models/chat_integration_shared_examples.rb
+++ b/spec/support/shared_examples/models/chat_integration_shared_examples.rb
@@ -199,7 +199,7 @@ RSpec.shared_examples "chat integration" do |integration_name|
{
title: "Awesome wiki_page",
content: "Some text describing some thing or another",
- format: "md",
+ format: :markdown,
message: "user created page: Awesome wiki_page"
}
end
diff --git a/spec/support/shared_examples/models/concerns/integrations/reset_secret_fields_shared_examples.rb b/spec/support/shared_examples/models/concerns/integrations/reset_secret_fields_shared_examples.rb
new file mode 100644
index 00000000000..873f858e432
--- /dev/null
+++ b/spec/support/shared_examples/models/concerns/integrations/reset_secret_fields_shared_examples.rb
@@ -0,0 +1,110 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples Integrations::ResetSecretFields do
+ describe '#exposing_secrets_fields' do
+ it 'returns an array of strings' do
+ expect(integration.exposing_secrets_fields).to be_a(Array)
+ expect(integration.exposing_secrets_fields).to all(be_a(String))
+ end
+ end
+
+ describe '#reset_secret_fields?' do
+ let(:exposing_fields) { integration.exposing_secrets_fields }
+
+ it 'returns false if no exposing field has changed' do
+ exposing_fields.each do |field|
+ allow(integration).to receive("#{field}_changed?").and_return(false)
+ end
+
+ expect(integration.send(:reset_secret_fields?)).to be(false)
+ end
+
+ it 'returns true if any exposing field has changed' do
+ exposing_fields.each do |field|
+ allow(integration).to receive("#{field}_changed?").and_return(true)
+
+ other_exposing_fields = exposing_fields.without(field)
+ other_exposing_fields.each do |other_field|
+ allow(integration).to receive("#{other_field}_changed?").and_return(false)
+ end
+
+ expect(integration.send(:reset_secret_fields?)).to be(true)
+ end
+ end
+ end
+
+ describe 'validation callback' do
+ before do
+ # Store a value in each password field
+ integration.secret_fields.each do |field|
+ integration.public_send("#{field}=", 'old value')
+ end
+
+ # Treat values as persisted
+ integration.reset_updated_properties
+ integration.instance_variable_set('@old_data_fields', nil) if integration.supports_data_fields?
+ end
+
+ context 'when an exposing field has changed' do
+ let(:exposing_field) { integration.exposing_secrets_fields.first }
+
+ before do
+ integration.public_send("#{exposing_field}=", 'new value')
+ end
+
+ it 'clears all secret fields' do
+ integration.valid?
+
+ integration.secret_fields.each do |field|
+ expect(integration.public_send(field)).to be_nil
+ expect(integration.properties[field]).to be_nil if integration.properties.present?
+ expect(integration.data_fields[field]).to be_nil if integration.supports_data_fields?
+ end
+ end
+
+ context 'when a secret field has been updated' do
+ let(:secret_field) { integration.secret_fields.first }
+ let(:other_secret_fields) { integration.secret_fields.without(secret_field) }
+ let(:new_value) { 'new value' }
+
+ before do
+ integration.public_send("#{secret_field}=", new_value)
+ end
+
+ it 'does not clear this secret field' do
+ integration.valid?
+
+ expect(integration.public_send(secret_field)).to eq('new value')
+
+ other_secret_fields.each do |field|
+ expect(integration.public_send(field)).to be_nil
+ end
+ end
+
+ context 'when a secret field has been updated with the same value' do
+ let(:new_value) { 'old value' }
+
+ it 'does not clear this secret field' do
+ integration.valid?
+
+ expect(integration.public_send(secret_field)).to eq('old value')
+
+ other_secret_fields.each do |field|
+ expect(integration.public_send(field)).to be_nil
+ end
+ end
+ end
+ end
+ end
+
+ context 'when no exposing field has changed' do
+ it 'does not clear any secret fields' do
+ integration.valid?
+
+ integration.secret_fields.each do |field|
+ expect(integration.public_send(field)).to eq('old value')
+ end
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/models/concerns/integrations/slack_mattermost_notifier_shared_examples.rb b/spec/support/shared_examples/models/concerns/integrations/slack_mattermost_notifier_shared_examples.rb
index da5c35c970a..2e062cda4e9 100644
--- a/spec/support/shared_examples/models/concerns/integrations/slack_mattermost_notifier_shared_examples.rb
+++ b/spec/support/shared_examples/models/concerns/integrations/slack_mattermost_notifier_shared_examples.rb
@@ -45,9 +45,33 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |integration_name
end
it "notifies about #{event_type} events" do
+ expect(chat_integration).not_to receive(:log_error)
+
chat_integration.execute(data)
+
expect(WebMock).to have_requested(:post, stubbed_resolved_hostname)
end
+
+ context 'when the response is not successful' do
+ let!(:stubbed_resolved_hostname) do
+ stub_full_request(webhook_url, method: :post)
+ .to_return(status: 409, body: 'error message')
+ .request_pattern.uri_pattern.to_s
+ end
+
+ it 'logs an error' do
+ expect(chat_integration).to receive(:log_error).with(
+ 'SlackMattermostNotifier HTTP error response',
+ request_host: 'example.gitlab.com',
+ response_code: 409,
+ response_body: 'error message'
+ )
+
+ chat_integration.execute(data)
+
+ expect(WebMock).to have_requested(:post, stubbed_resolved_hostname)
+ end
+ end
end
shared_examples "untriggered #{integration_name} integration" do |event_type: nil, branches_to_be_notified: nil|
@@ -59,8 +83,9 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |integration_name
stub_full_request(webhook_url, method: :post).request_pattern.uri_pattern.to_s
end
- it "notifies about #{event_type} events" do
+ it "does not notify about #{event_type} events" do
chat_integration.execute(data)
+
expect(WebMock).not_to have_requested(:post, stubbed_resolved_hostname)
end
end
diff --git a/spec/support/shared_examples/models/member_shared_examples.rb b/spec/support/shared_examples/models/member_shared_examples.rb
index a329a6dca91..e293d10964b 100644
--- a/spec/support/shared_examples/models/member_shared_examples.rb
+++ b/spec/support/shared_examples/models/member_shared_examples.rb
@@ -77,312 +77,309 @@ RSpec.shared_examples '#valid_level_roles' do |entity_name|
end
RSpec.shared_examples_for "member creation" do
- let_it_be(:user) { create(:user) }
let_it_be(:admin) { create(:admin) }
- describe '#execute' do
- it 'returns a Member object', :aggregate_failures do
- member = described_class.new(source, user, :maintainer).execute
-
- expect(member).to be_a member_type
- expect(member).to be_persisted
- end
+ it 'returns a Member object', :aggregate_failures do
+ member = described_class.new(source, user, :maintainer).execute
- context 'when adding a project_bot' do
- let_it_be(:project_bot) { create(:user, :project_bot) }
-
- before_all do
- source.add_owner(user)
- end
+ expect(member).to be_a member_type
+ expect(member).to be_persisted
+ end
- context 'when project_bot is already a member' do
- before do
- source.add_developer(project_bot)
- end
+ context 'when adding a project_bot' do
+ let_it_be(:project_bot) { create(:user, :project_bot) }
- it 'does not update the member' do
- member = described_class.new(source, project_bot, :maintainer, current_user: user).execute
+ before_all do
+ source.add_owner(user)
+ end
- expect(source.users.reload).to include(project_bot)
- expect(member).to be_persisted
- expect(member.access_level).to eq(Gitlab::Access::DEVELOPER)
- expect(member.errors.full_messages).to include(/not authorized to update member/)
- end
+ context 'when project_bot is already a member' do
+ before do
+ source.add_developer(project_bot)
end
- context 'when project_bot is not already a member' do
- it 'adds the member' do
- member = described_class.new(source, project_bot, :maintainer, current_user: user).execute
+ it 'does not update the member' do
+ member = described_class.new(source, project_bot, :maintainer, current_user: user).execute
- expect(source.users.reload).to include(project_bot)
- expect(member).to be_persisted
- end
+ expect(source.users.reload).to include(project_bot)
+ expect(member).to be_persisted
+ expect(member.access_level).to eq(Gitlab::Access::DEVELOPER)
+ expect(member.errors.full_messages).to include(/not authorized to update member/)
end
end
- context 'when admin mode is enabled', :enable_admin_mode, :aggregate_failures do
- it 'sets members.created_by to the given admin current_user' do
- member = described_class.new(source, user, :maintainer, current_user: admin).execute
+ context 'when project_bot is not already a member' do
+ it 'adds the member' do
+ member = described_class.new(source, project_bot, :maintainer, current_user: user).execute
+ expect(source.users.reload).to include(project_bot)
expect(member).to be_persisted
- expect(source.users.reload).to include(user)
- expect(member.created_by).to eq(admin)
end
end
+ end
- context 'when admin mode is disabled' do
- it 'rejects setting members.created_by to the given admin current_user', :aggregate_failures do
- member = described_class.new(source, user, :maintainer, current_user: admin).execute
+ context 'when admin mode is enabled', :enable_admin_mode, :aggregate_failures do
+ it 'sets members.created_by to the given admin current_user' do
+ member = described_class.new(source, user, :maintainer, current_user: admin).execute
- expect(member).not_to be_persisted
- expect(source.users.reload).not_to include(user)
- expect(member.errors.full_messages).to include(/not authorized to create member/)
- end
+ expect(member).to be_persisted
+ expect(source.users.reload).to include(user)
+ expect(member.created_by).to eq(admin)
end
+ end
- it 'sets members.expires_at to the given expires_at' do
- member = described_class.new(source, user, :maintainer, expires_at: Date.new(2016, 9, 22)).execute
+ context 'when admin mode is disabled' do
+ it 'rejects setting members.created_by to the given admin current_user', :aggregate_failures do
+ member = described_class.new(source, user, :maintainer, current_user: admin).execute
- expect(member.expires_at).to eq(Date.new(2016, 9, 22))
+ expect(member).not_to be_persisted
+ expect(source.users.reload).not_to include(user)
+ expect(member.errors.full_messages).to include(/not authorized to create member/)
end
+ end
- described_class.access_levels.each do |sym_key, int_access_level|
- it "accepts the :#{sym_key} symbol as access level", :aggregate_failures do
- expect(source.users).not_to include(user)
+ it 'sets members.expires_at to the given expires_at' do
+ member = described_class.new(source, user, :maintainer, expires_at: Date.new(2016, 9, 22)).execute
- member = described_class.new(source, user.id, sym_key).execute
+ expect(member.expires_at).to eq(Date.new(2016, 9, 22))
+ end
- expect(member.access_level).to eq(int_access_level)
- expect(source.users.reload).to include(user)
- end
+ described_class.access_levels.each do |sym_key, int_access_level|
+ it "accepts the :#{sym_key} symbol as access level", :aggregate_failures do
+ expect(source.users).not_to include(user)
+
+ member = described_class.new(source, user.id, sym_key).execute
- it "accepts the #{int_access_level} integer as access level", :aggregate_failures do
+ expect(member.access_level).to eq(int_access_level)
+ expect(source.users.reload).to include(user)
+ end
+
+ it "accepts the #{int_access_level} integer as access level", :aggregate_failures do
+ expect(source.users).not_to include(user)
+
+ member = described_class.new(source, user.id, int_access_level).execute
+
+ expect(member.access_level).to eq(int_access_level)
+ expect(source.users.reload).to include(user)
+ end
+ end
+
+ context 'with no current_user' do
+ context 'when called with a known user id' do
+ it 'adds the user as a member' do
expect(source.users).not_to include(user)
- member = described_class.new(source, user.id, int_access_level).execute
+ described_class.new(source, user.id, :maintainer).execute
- expect(member.access_level).to eq(int_access_level)
expect(source.users.reload).to include(user)
end
end
- context 'with no current_user' do
- context 'when called with a known user id' do
- it 'adds the user as a member' do
- expect(source.users).not_to include(user)
+ context 'when called with an unknown user id' do
+ it 'does not add the user as a member' do
+ expect(source.users).not_to include(user)
- described_class.new(source, user.id, :maintainer).execute
+ described_class.new(source, non_existing_record_id, :maintainer).execute
- expect(source.users.reload).to include(user)
- end
+ expect(source.users.reload).not_to include(user)
end
+ end
- context 'when called with an unknown user id' do
- it 'does not add the user as a member' do
- expect(source.users).not_to include(user)
+ context 'when called with a user object' do
+ it 'adds the user as a member' do
+ expect(source.users).not_to include(user)
- described_class.new(source, non_existing_record_id, :maintainer).execute
+ described_class.new(source, user, :maintainer).execute
- expect(source.users.reload).not_to include(user)
- end
+ expect(source.users.reload).to include(user)
+ end
+ end
+
+ context 'when called with a requester user object' do
+ before do
+ source.request_access(user)
end
- context 'when called with a user object' do
- it 'adds the user as a member' do
- expect(source.users).not_to include(user)
+ it 'adds the requester as a member', :aggregate_failures do
+ expect(source.users).not_to include(user)
+ expect(source.requesters.exists?(user_id: user)).to be_truthy
+ expect do
described_class.new(source, user, :maintainer).execute
+ end.to raise_error(Gitlab::Access::AccessDeniedError)
- expect(source.users.reload).to include(user)
- end
+ expect(source.users.reload).not_to include(user)
+ expect(source.requesters.reload.exists?(user_id: user)).to be_truthy
end
+ end
- context 'when called with a requester user object' do
- before do
- source.request_access(user)
- end
-
- it 'adds the requester as a member', :aggregate_failures do
- expect(source.users).not_to include(user)
- expect(source.requesters.exists?(user_id: user)).to be_truthy
+ context 'when called with a known user email' do
+ it 'adds the user as a member' do
+ expect(source.users).not_to include(user)
- expect do
- described_class.new(source, user, :maintainer).execute
- end.to raise_error(Gitlab::Access::AccessDeniedError)
+ described_class.new(source, user.email, :maintainer).execute
- expect(source.users.reload).not_to include(user)
- expect(source.requesters.reload.exists?(user_id: user)).to be_truthy
- end
+ expect(source.users.reload).to include(user)
end
+ end
- context 'when called with a known user email' do
- it 'adds the user as a member' do
- expect(source.users).not_to include(user)
+ context 'when called with an unknown user email' do
+ it 'creates an invited member' do
+ expect(source.users).not_to include(user)
- described_class.new(source, user.email, :maintainer).execute
+ described_class.new(source, 'user@example.com', :maintainer).execute
- expect(source.users.reload).to include(user)
- end
+ expect(source.members.invite.pluck(:invite_email)).to include('user@example.com')
end
+ end
- context 'when called with an unknown user email' do
- it 'creates an invited member' do
- expect(source.users).not_to include(user)
+ context 'when called with an unknown user email starting with a number' do
+ it 'creates an invited member', :aggregate_failures do
+ email_starting_with_number = "#{user.id}_email@example.com"
- described_class.new(source, 'user@example.com', :maintainer).execute
+ described_class.new(source, email_starting_with_number, :maintainer).execute
- expect(source.members.invite.pluck(:invite_email)).to include('user@example.com')
- end
+ expect(source.members.invite.pluck(:invite_email)).to include(email_starting_with_number)
+ expect(source.users.reload).not_to include(user)
end
+ end
+ end
- context 'when called with an unknown user email starting with a number' do
- it 'creates an invited member', :aggregate_failures do
- email_starting_with_number = "#{user.id}_email@example.com"
+ context 'when current_user can update member', :enable_admin_mode do
+ it 'creates the member' do
+ expect(source.users).not_to include(user)
- described_class.new(source, email_starting_with_number, :maintainer).execute
+ described_class.new(source, user, :maintainer, current_user: admin).execute
- expect(source.members.invite.pluck(:invite_email)).to include(email_starting_with_number)
- expect(source.users.reload).not_to include(user)
- end
- end
+ expect(source.users.reload).to include(user)
end
- context 'when current_user can update member', :enable_admin_mode do
- it 'creates the member' do
+ context 'when called with a requester user object' do
+ before do
+ source.request_access(user)
+ end
+
+ it 'adds the requester as a member', :aggregate_failures do
expect(source.users).not_to include(user)
+ expect(source.requesters.exists?(user_id: user)).to be_truthy
described_class.new(source, user, :maintainer, current_user: admin).execute
expect(source.users.reload).to include(user)
+ expect(source.requesters.reload.exists?(user_id: user)).to be_falsy
end
+ end
+ end
- context 'when called with a requester user object' do
- before do
- source.request_access(user)
- end
+ context 'when current_user cannot update member' do
+ it 'does not create the member', :aggregate_failures do
+ expect(source.users).not_to include(user)
- it 'adds the requester as a member', :aggregate_failures do
- expect(source.users).not_to include(user)
- expect(source.requesters.exists?(user_id: user)).to be_truthy
+ member = described_class.new(source, user, :maintainer, current_user: user).execute
- described_class.new(source, user, :maintainer, current_user: admin).execute
+ expect(source.users.reload).not_to include(user)
+ expect(member).not_to be_persisted
+ end
- expect(source.users.reload).to include(user)
- expect(source.requesters.reload.exists?(user_id: user)).to be_falsy
- end
+ context 'when called with a requester user object' do
+ before do
+ source.request_access(user)
end
- end
- context 'when current_user cannot update member' do
- it 'does not create the member', :aggregate_failures do
+ it 'does not destroy the requester', :aggregate_failures do
expect(source.users).not_to include(user)
+ expect(source.requesters.exists?(user_id: user)).to be_truthy
- member = described_class.new(source, user, :maintainer, current_user: user).execute
+ described_class.new(source, user, :maintainer, current_user: user).execute
expect(source.users.reload).not_to include(user)
- expect(member).not_to be_persisted
+ expect(source.requesters.exists?(user_id: user)).to be_truthy
end
+ end
+ end
- context 'when called with a requester user object' do
- before do
- source.request_access(user)
- end
+ context 'when member already exists' do
+ before do
+ source.add_user(user, :developer)
+ end
- it 'does not destroy the requester', :aggregate_failures do
- expect(source.users).not_to include(user)
- expect(source.requesters.exists?(user_id: user)).to be_truthy
+ context 'with no current_user' do
+ it 'updates the member' do
+ expect(source.users).to include(user)
- described_class.new(source, user, :maintainer, current_user: user).execute
+ described_class.new(source, user, :maintainer).execute
- expect(source.users.reload).not_to include(user)
- expect(source.requesters.exists?(user_id: user)).to be_truthy
- end
+ expect(source.members.find_by(user_id: user).access_level).to eq(Gitlab::Access::MAINTAINER)
end
end
- context 'when member already exists' do
- before do
- source.add_user(user, :developer)
- end
-
- context 'with no current_user' do
- it 'updates the member' do
- expect(source.users).to include(user)
+ context 'when current_user can update member', :enable_admin_mode do
+ it 'updates the member' do
+ expect(source.users).to include(user)
- described_class.new(source, user, :maintainer).execute
+ described_class.new(source, user, :maintainer, current_user: admin).execute
- expect(source.members.find_by(user_id: user).access_level).to eq(Gitlab::Access::MAINTAINER)
- end
+ expect(source.members.find_by(user_id: user).access_level).to eq(Gitlab::Access::MAINTAINER)
end
+ end
- context 'when current_user can update member', :enable_admin_mode do
- it 'updates the member' do
- expect(source.users).to include(user)
+ context 'when current_user cannot update member' do
+ it 'does not update the member' do
+ expect(source.users).to include(user)
- described_class.new(source, user, :maintainer, current_user: admin).execute
+ described_class.new(source, user, :maintainer, current_user: user).execute
- expect(source.members.find_by(user_id: user).access_level).to eq(Gitlab::Access::MAINTAINER)
- end
+ expect(source.members.find_by(user_id: user).access_level).to eq(Gitlab::Access::DEVELOPER)
end
+ end
+ end
- context 'when current_user cannot update member' do
- it 'does not update the member' do
- expect(source.users).to include(user)
+ context 'when `tasks_to_be_done` and `tasks_project_id` are passed' do
+ let(:task_project) { source.is_a?(Group) ? create(:project, group: source) : source }
- described_class.new(source, user, :maintainer, current_user: user).execute
+ it 'creates a member_task with the correct attributes', :aggregate_failures do
+ described_class.new(source, user, :developer, tasks_to_be_done: %w(ci code), tasks_project_id: task_project.id).execute
- expect(source.members.find_by(user_id: user).access_level).to eq(Gitlab::Access::DEVELOPER)
- end
- end
- end
+ member = source.members.last
- context 'when `tasks_to_be_done` and `tasks_project_id` are passed' do
- let(:task_project) { source.is_a?(Group) ? create(:project, group: source) : source }
+ expect(member.tasks_to_be_done).to match_array([:ci, :code])
+ expect(member.member_task.project).to eq(task_project)
+ end
- it 'creates a member_task with the correct attributes', :aggregate_failures do
- described_class.new(source, user, :developer, tasks_to_be_done: %w(ci code), tasks_project_id: task_project.id).execute
+ context 'with an already existing member' do
+ before do
+ source.add_user(user, :developer)
+ end
- member = source.members.last
+ it 'does not update tasks to be done if tasks already exist', :aggregate_failures do
+ member = source.members.find_by(user_id: user.id)
+ create(:member_task, member: member, project: task_project, tasks_to_be_done: %w(code ci))
- expect(member.tasks_to_be_done).to match_array([:ci, :code])
+ expect do
+ described_class.new(source,
+ user,
+ :developer,
+ tasks_to_be_done: %w(issues),
+ tasks_project_id: task_project.id).execute
+ end.not_to change(MemberTask, :count)
+
+ member.reset
+ expect(member.tasks_to_be_done).to match_array([:code, :ci])
expect(member.member_task.project).to eq(task_project)
end
- context 'with an already existing member' do
- before do
- source.add_user(user, :developer)
- end
-
- it 'does not update tasks to be done if tasks already exist', :aggregate_failures do
- member = source.members.find_by(user_id: user.id)
- create(:member_task, member: member, project: task_project, tasks_to_be_done: %w(code ci))
-
- expect do
- described_class.new(source,
- user,
- :developer,
- tasks_to_be_done: %w(issues),
- tasks_project_id: task_project.id).execute
- end.not_to change(MemberTask, :count)
-
- member.reset
- expect(member.tasks_to_be_done).to match_array([:code, :ci])
- expect(member.member_task.project).to eq(task_project)
- end
-
- it 'adds tasks to be done if they do not exist', :aggregate_failures do
- expect do
- described_class.new(source,
- user,
- :developer,
- tasks_to_be_done: %w(issues),
- tasks_project_id: task_project.id).execute
- end.to change(MemberTask, :count).by(1)
-
- member = source.members.find_by(user_id: user.id)
- expect(member.tasks_to_be_done).to match_array([:issues])
- expect(member.member_task.project).to eq(task_project)
- end
+ it 'adds tasks to be done if they do not exist', :aggregate_failures do
+ expect do
+ described_class.new(source,
+ user,
+ :developer,
+ tasks_to_be_done: %w(issues),
+ tasks_project_id: task_project.id).execute
+ end.to change(MemberTask, :count).by(1)
+
+ member = source.members.find_by(user_id: user.id)
+ expect(member.tasks_to_be_done).to match_array([:issues])
+ expect(member.member_task.project).to eq(task_project)
end
end
end
diff --git a/spec/support/shared_examples/models/reviewer_state_shared_examples.rb b/spec/support/shared_examples/models/reviewer_state_shared_examples.rb
deleted file mode 100644
index f1392768b06..00000000000
--- a/spec/support/shared_examples/models/reviewer_state_shared_examples.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-# frozen_string_literal: true
-
-RSpec.shared_examples 'having reviewer state' do
- describe 'mr_attention_requests feature flag is disabled' do
- before do
- stub_feature_flags(mr_attention_requests: false)
- end
-
- it { is_expected.to have_attributes(state: 'unreviewed') }
- end
-
- describe 'mr_attention_requests feature flag is enabled' do
- it { is_expected.to have_attributes(state: 'attention_requested') }
- end
-end
diff --git a/spec/support/shared_examples/models/wiki_shared_examples.rb b/spec/support/shared_examples/models/wiki_shared_examples.rb
index 03e9dd65e33..6f17231a040 100644
--- a/spec/support/shared_examples/models/wiki_shared_examples.rb
+++ b/spec/support/shared_examples/models/wiki_shared_examples.rb
@@ -392,41 +392,161 @@ RSpec.shared_examples 'wiki model' do
end
describe '#create_page' do
- it 'creates a new wiki page' do
- expect(subject.create_page('test page', 'this is content')).not_to eq(false)
- expect(subject.list_pages.count).to eq(1)
- end
+ shared_examples 'create_page tests' do
+ it 'creates a new wiki page' do
+ expect(subject.create_page('test page', 'this is content')).not_to eq(false)
+ expect(subject.list_pages.count).to eq(1)
+ end
- it 'returns false when a duplicate page exists' do
- subject.create_page('test page', 'content')
+ it 'returns false when a duplicate page exists' do
+ subject.create_page('test page', 'content')
- expect(subject.create_page('test page', 'content')).to eq(false)
- end
+ expect(subject.create_page('test page', 'content')).to eq(false)
+ end
- it 'stores an error message when a duplicate page exists' do
- 2.times { subject.create_page('test page', 'content') }
+ it 'stores an error message when a duplicate page exists' do
+ 2.times { subject.create_page('test page', 'content') }
- expect(subject.error_message).to match(/Duplicate page:/)
- end
+ expect(subject.error_message).to match(/Duplicate page:/)
+ end
+
+ it 'sets the correct commit message' do
+ subject.create_page('test page', 'some content', :markdown, 'commit message')
+
+ expect(subject.list_pages.first.page.version.message).to eq('commit message')
+ end
+
+ it 'sets the correct commit email' do
+ subject.create_page('test page', 'content')
+
+ expect(user.commit_email).not_to eq(user.email)
+ expect(commit.author_email).to eq(user.commit_email)
+ expect(commit.committer_email).to eq(user.commit_email)
+ end
+
+ it 'runs after_wiki_activity callbacks' do
+ expect(subject).to receive(:after_wiki_activity)
- it 'sets the correct commit message' do
- subject.create_page('test page', 'some content', :markdown, 'commit message')
+ subject.create_page('Test Page', 'This is content')
+ end
+
+ it 'cannot create two pages with the same title but different format' do
+ subject.create_page('test page', 'content', :markdown)
+ subject.create_page('test page', 'content', :rdoc)
+
+ expect(subject.error_message).to match(/Duplicate page:/)
+ end
+
+ it 'cannot create two pages with the same title but different capitalization' do
+ subject.create_page('test page', 'content')
+ subject.create_page('Test page', 'content')
+
+ expect(subject.error_message).to match(/Duplicate page:/)
+ end
- expect(subject.list_pages.first.page.version.message).to eq('commit message')
+ it 'cannot create two pages with the same title, different capitalization, and different format' do
+ subject.create_page('test page', 'content')
+ subject.create_page('Test page', 'content', :rdoc)
+
+ expect(subject.error_message).to match(/Duplicate page:/)
+ end
end
- it 'sets the correct commit email' do
- subject.create_page('test page', 'content')
+ it_behaves_like 'create_page tests' do
+ it 'returns false if a page exists already in the repository', :aggregate_failures do
+ subject.create_page('test page', 'content')
- expect(user.commit_email).not_to eq(user.email)
- expect(commit.author_email).to eq(user.commit_email)
- expect(commit.committer_email).to eq(user.commit_email)
+ allow(subject).to receive(:file_exists_by_regex?).and_return(false)
+
+ expect(subject.create_page('test page', 'content')).to eq false
+ expect(subject.error_message).to match(/Duplicate page:/)
+ end
+
+ it 'returns false if it has an invalid format', :aggregate_failures do
+ expect(subject.create_page('test page', 'content', :foobar)).to eq false
+ expect(subject.error_message).to match(/Invalid format selected/)
+ end
+
+ using RSpec::Parameterized::TableSyntax
+
+ where(:new_file, :format, :existing_repo_files, :success) do
+ 'foo' | :markdown | [] | true
+ 'foo' | :rdoc | [] | true
+ 'foo' | :asciidoc | [] | true
+ 'foo' | :org | [] | true
+ 'foo' | :textile | [] | false
+ 'foo' | :creole | [] | false
+ 'foo' | :rest | [] | false
+ 'foo' | :mediawiki | [] | false
+ 'foo' | :pod | [] | false
+ 'foo' | :plaintext | [] | false
+ 'foo' | :markdown | ['foo.md'] | false
+ 'foo' | :markdown | ['foO.md'] | false
+ 'foO' | :markdown | ['foo.md'] | false
+ 'foo' | :markdown | ['foo.mdfoo'] | true
+ 'foo' | :markdown | ['foo.markdown'] | false
+ 'foo' | :markdown | ['foo.mkd'] | false
+ 'foo' | :markdown | ['foo.mkdn'] | false
+ 'foo' | :markdown | ['foo.mdown'] | false
+ 'foo' | :markdown | ['foo.adoc'] | false
+ 'foo' | :markdown | ['foo.asciidoc'] | false
+ 'foo' | :markdown | ['foo.org'] | false
+ 'foo' | :markdown | ['foo.rdoc'] | false
+ 'foo' | :markdown | ['foo.textile'] | false
+ 'foo' | :markdown | ['foo.creole'] | false
+ 'foo' | :markdown | ['foo.rest'] | false
+ 'foo' | :markdown | ['foo.rest.txt'] | false
+ 'foo' | :markdown | ['foo.rst'] | false
+ 'foo' | :markdown | ['foo.rst.txt'] | false
+ 'foo' | :markdown | ['foo.rst.txtfoo'] | true
+ 'foo' | :markdown | ['foo.mediawiki'] | false
+ 'foo' | :markdown | ['foo.wiki'] | false
+ 'foo' | :markdown | ['foo.pod'] | false
+ 'foo' | :markdown | ['foo.txt'] | false
+ 'foo' | :markdown | ['foo.Md'] | false
+ 'foo' | :markdown | ['foo.jpg'] | true
+ 'foo' | :rdoc | ['foo.md'] | false
+ 'foo' | :rdoc | ['foO.md'] | false
+ 'foO' | :rdoc | ['foo.md'] | false
+ 'foo' | :asciidoc | ['foo.md'] | false
+ 'foo' | :org | ['foo.md'] | false
+ 'foo' | :markdown | ['dir/foo.md'] | true
+ '/foo' | :markdown | ['foo.md'] | false
+ './foo' | :markdown | ['foo.md'] | false
+ '../foo' | :markdown | ['foo.md'] | false
+ '../../foo' | :markdown | ['foo.md'] | false
+ '../../foo' | :markdown | ['dir/foo.md'] | true
+ 'dir/foo' | :markdown | ['foo.md'] | true
+ 'dir/foo' | :markdown | ['dir/foo.md'] | false
+ 'dir/foo' | :markdown | ['dir/foo.rdoc'] | false
+ '/dir/foo' | :markdown | ['dir/foo.rdoc'] | false
+ './dir/foo' | :markdown | ['dir/foo.rdoc'] | false
+ '../dir/foo' | :markdown | ['dir/foo.rdoc'] | false
+ '../dir/../foo' | :markdown | ['dir/foo.rdoc'] | true
+ '../dir/../foo' | :markdown | ['foo.rdoc'] | false
+ '../dir/../dir/foo' | :markdown | ['dir/foo.rdoc'] | false
+ '../dir/../another/foo' | :markdown | ['dir/foo.rdoc'] | true
+ 'another/dir/foo' | :markdown | ['dir/foo.md'] | true
+ 'foo bar' | :markdown | ['foo-bar.md'] | false
+ 'foo bar' | :markdown | ['foo-bar.md'] | true
+ 'föö'.encode('ISO-8859-1') | :markdown | ['f��.md'] | false
+ end
+
+ with_them do
+ specify do
+ allow(subject.repository).to receive(:ls_files).and_return(existing_repo_files)
+
+ expect(subject.create_page(new_file, 'content', format)).to eq success
+ end
+ end
end
- it 'runs after_wiki_activity callbacks' do
- expect(subject).to receive(:after_wiki_activity)
+ context 'when feature flag :gitaly_replace_wiki_create_page is disabled' do
+ before do
+ stub_feature_flags(gitaly_replace_wiki_create_page: false)
+ end
- subject.create_page('Test Page', 'This is content')
+ it_behaves_like 'create_page tests'
end
end
@@ -452,7 +572,7 @@ RSpec.shared_examples 'wiki model' do
expect(subject).to receive(:after_wiki_activity)
expect(update_page).to eq true
- page = subject.find_page(updated_title.presence || original_title)
+ page = subject.find_page(expected_title)
expect(page.raw_content).to eq(updated_content)
expect(page.path).to eq(expected_path)
@@ -467,23 +587,25 @@ RSpec.shared_examples 'wiki model' do
shared_context 'common examples' do
using RSpec::Parameterized::TableSyntax
- where(:original_title, :original_format, :updated_title, :updated_format, :expected_path) do
- 'test page' | :markdown | 'new test page' | :markdown | 'new-test-page.md'
- 'test page' | :markdown | 'test page' | :markdown | 'test-page.md'
- 'test page' | :markdown | 'test page' | :asciidoc | 'test-page.asciidoc'
+ where(:original_title, :original_format, :updated_title, :updated_format, :expected_title, :expected_path) do
+ 'test page' | :markdown | 'new test page' | :markdown | 'new test page' | 'new-test-page.md'
+ 'test page' | :markdown | 'test page' | :markdown | 'test page' | 'test-page.md'
+ 'test page' | :markdown | 'test page' | :asciidoc | 'test page' | 'test-page.asciidoc'
+
+ 'test page' | :markdown | 'new dir/new test page' | :markdown | 'new dir/new test page' | 'new-dir/new-test-page.md'
+ 'test page' | :markdown | 'new dir/test page' | :markdown | 'new dir/test page' | 'new-dir/test-page.md'
- 'test page' | :markdown | 'new dir/new test page' | :markdown | 'new-dir/new-test-page.md'
- 'test page' | :markdown | 'new dir/test page' | :markdown | 'new-dir/test-page.md'
+ 'test dir/test page' | :markdown | 'new dir/new test page' | :markdown | 'new dir/new test page' | 'new-dir/new-test-page.md'
+ 'test dir/test page' | :markdown | 'test dir/test page' | :markdown | 'test dir/test page' | 'test-dir/test-page.md'
+ 'test dir/test page' | :markdown | 'test dir/test page' | :asciidoc | 'test dir/test page' | 'test-dir/test-page.asciidoc'
- 'test dir/test page' | :markdown | 'new dir/new test page' | :markdown | 'new-dir/new-test-page.md'
- 'test dir/test page' | :markdown | 'test dir/test page' | :markdown | 'test-dir/test-page.md'
- 'test dir/test page' | :markdown | 'test dir/test page' | :asciidoc | 'test-dir/test-page.asciidoc'
+ 'test dir/test page' | :markdown | 'new test page' | :markdown | 'new test page' | 'new-test-page.md'
+ 'test dir/test page' | :markdown | 'test page' | :markdown | 'test page' | 'test-page.md'
- 'test dir/test page' | :markdown | 'new test page' | :markdown | 'new-test-page.md'
- 'test dir/test page' | :markdown | 'test page' | :markdown | 'test-page.md'
+ 'test page' | :markdown | nil | :markdown | 'test page' | 'test-page.md'
+ 'test.page' | :markdown | nil | :markdown | 'test.page' | 'test.page.md'
- 'test page' | :markdown | nil | :markdown | 'test-page.md'
- 'test.page' | :markdown | nil | :markdown | 'test.page.md'
+ 'testpage' | :markdown | './testpage' | :markdown | 'testpage' | 'testpage.md'
end
end
@@ -497,16 +619,23 @@ RSpec.shared_examples 'wiki model' do
shared_context 'extended examples' do
using RSpec::Parameterized::TableSyntax
- where(:original_title, :original_format, :updated_title, :updated_format, :expected_path) do
- 'test page' | :markdown | 'new test page' | :asciidoc | 'new-test-page.asciidoc'
- 'test page' | :markdown | 'new dir/new test page' | :asciidoc | 'new-dir/new-test-page.asciidoc'
- 'test dir/test page' | :markdown | 'new dir/new test page' | :asciidoc | 'new-dir/new-test-page.asciidoc'
- 'test dir/test page' | :markdown | 'new test page' | :asciidoc | 'new-test-page.asciidoc'
- 'test page' | :markdown | nil | :asciidoc | 'test-page.asciidoc'
- 'test dir/test page' | :markdown | nil | :asciidoc | 'test-dir/test-page.asciidoc'
- 'test dir/test page' | :markdown | nil | :markdown | 'test-dir/test-page.md'
- 'test page' | :markdown | '' | :markdown | 'test-page.md'
- 'test.page' | :markdown | '' | :markdown | 'test.page.md'
+ where(:original_title, :original_format, :updated_title, :updated_format, :expected_title, :expected_path) do
+ 'test page' | :markdown | 'new test page' | :asciidoc | 'new test page' | 'new-test-page.asciidoc'
+ 'test page' | :markdown | 'new dir/new test page' | :asciidoc | 'new dir/new test page' | 'new-dir/new-test-page.asciidoc'
+ 'test dir/test page' | :markdown | 'new dir/new test page' | :asciidoc | 'new dir/new test page' | 'new-dir/new-test-page.asciidoc'
+ 'test dir/test page' | :markdown | 'new test page' | :asciidoc | 'new test page' | 'new-test-page.asciidoc'
+ 'test page' | :markdown | nil | :asciidoc | 'test page' | 'test-page.asciidoc'
+ 'test dir/test page' | :markdown | nil | :asciidoc | 'test dir/test page' | 'test-dir/test-page.asciidoc'
+ 'test dir/test page' | :markdown | nil | :markdown | 'test dir/test page' | 'test-dir/test-page.md'
+ 'test page' | :markdown | '' | :markdown | 'test page' | 'test-page.md'
+ 'test.page' | :markdown | '' | :markdown | 'test.page' | 'test.page.md'
+ 'testpage' | :markdown | '../testpage' | :markdown | 'testpage' | 'testpage.md'
+ 'dir/testpage' | :markdown | 'dir/../testpage' | :markdown | 'testpage' | 'testpage.md'
+ 'dir/testpage' | :markdown | './dir/testpage' | :markdown | 'dir/testpage' | 'dir/testpage.md'
+ 'dir/testpage' | :markdown | '../dir/testpage' | :markdown | 'dir/testpage' | 'dir/testpage.md'
+ 'dir/testpage' | :markdown | '../dir/../testpage' | :markdown | 'testpage' | 'testpage.md'
+ 'dir/testpage' | :markdown | '../dir/../dir/testpage' | :markdown | 'dir/testpage' | 'dir/testpage.md'
+ 'dir/testpage' | :markdown | '../dir/../another/testpage' | :markdown | 'another/testpage' | 'another/testpage.md'
end
end
@@ -547,16 +676,6 @@ RSpec.shared_examples 'wiki model' do
end
end
end
-
- context 'when feature flag :gitaly_replace_wiki_update_page is disabled' do
- before do
- stub_feature_flags(gitaly_replace_wiki_update_page: false)
- end
-
- it_behaves_like 'update_page tests' do
- include_context 'common examples'
- end
- end
end
describe '#delete_page' do
diff --git a/spec/support/shared_examples/nav_sidebar_shared_examples.rb b/spec/support/shared_examples/nav_sidebar_shared_examples.rb
index 3e500683712..4b815988bc5 100644
--- a/spec/support/shared_examples/nav_sidebar_shared_examples.rb
+++ b/spec/support/shared_examples/nav_sidebar_shared_examples.rb
@@ -27,7 +27,7 @@ end
RSpec.shared_examples 'sidebar includes snowplow attributes' do |track_action, track_label, track_property|
specify do
- allow(view).to receive(:tracking_enabled?).and_return(true)
+ stub_application_setting(snowplow_enabled: true)
render
diff --git a/spec/support/shared_examples/requests/api/container_repositories_shared_examples.rb b/spec/support/shared_examples/requests/api/container_repositories_shared_examples.rb
index c1eccafa987..f5c41416763 100644
--- a/spec/support/shared_examples/requests/api/container_repositories_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/container_repositories_shared_examples.rb
@@ -21,6 +21,7 @@ RSpec.shared_examples 'returns repositories for allowed users' do |user_type, sc
expect(json_response.map { |repository| repository['id'] }).to contain_exactly(
root_repository.id, test_repository.id)
expect(response.body).not_to include('tags')
+ expect(response.body).not_to include('tags_count')
end
it 'returns a matching schema' do
@@ -29,7 +30,11 @@ RSpec.shared_examples 'returns repositories for allowed users' do |user_type, sc
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('registry/repositories')
end
+ end
+end
+RSpec.shared_examples 'returns tags for allowed users' do |user_type, scope|
+ context "for #{user_type}" do
context 'with tags param' do
let(:url) { "/#{scope}s/#{object.id}/registry/repositories?tags=true" }
@@ -169,10 +174,12 @@ RSpec.shared_examples 'reconciling migration_state' do
end
end
- context 'import_failed response' do
- let(:status) { 'import_failed' }
+ %w[import_canceled import_failed].each do |status|
+ context "#{status} response" do
+ let(:status) { status }
- it_behaves_like 'retrying the import'
+ it_behaves_like 'retrying the import'
+ end
end
context 'pre_import_in_progress response' do
@@ -192,17 +199,11 @@ RSpec.shared_examples 'reconciling migration_state' do
end
end
- context 'pre_import_failed response' do
- let(:status) { 'pre_import_failed' }
-
- it_behaves_like 'retrying the pre_import'
- end
-
- %w[pre_import_canceled import_canceled].each do |canceled_status|
- context "#{canceled_status} response" do
- let(:status) { canceled_status }
+ %w[pre_import_canceled pre_import_failed].each do |status|
+ context "#{status} response" do
+ let(:status) { status }
- it_behaves_like 'enforcing states coherence to', 'import_skipped'
+ it_behaves_like 'retrying the pre_import'
end
end
end
diff --git a/spec/support/shared_examples/requests/api/graphql/group_and_project_boards_query_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/group_and_project_boards_query_shared_examples.rb
index da9d254039b..e534a02e562 100644
--- a/spec/support/shared_examples/requests/api/graphql/group_and_project_boards_query_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/graphql/group_and_project_boards_query_shared_examples.rb
@@ -67,11 +67,15 @@ RSpec.shared_examples 'group and project boards query' do
let(:sort_param) { }
let(:first_param) { 2 }
+ def pagination_results_data(nodes)
+ nodes
+ end
+
let(:all_records) do
if board_parent.multiple_issue_boards_available?
- boards.map { |board| global_id_of(board) }
+ boards.map { |board| a_graphql_entity_for(board) }
else
- [global_id_of(boards.first)]
+ [a_graphql_entity_for(boards.first)]
end
end
end
diff --git a/spec/support/shared_examples/requests/api/graphql/noteable_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/noteable_shared_examples.rb
index 7e1f4500779..9033a8b4d3a 100644
--- a/spec/support/shared_examples/requests/api/graphql/noteable_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/graphql/noteable_shared_examples.rb
@@ -12,9 +12,9 @@ RSpec.shared_examples 'a noteable graphql type we can query' do
def expected
noteable.discussions.map do |discussion|
- include(
- 'id' => global_id_of(discussion),
- 'replyId' => global_id_of(discussion, id: discussion.reply_id),
+ a_graphql_entity_for(
+ discussion,
+ 'replyId' => global_id_of(discussion, id: discussion.reply_id).to_s,
'createdAt' => discussion.created_at.iso8601,
'notes' => include(
'nodes' => have_attributes(size: discussion.notes.size)
@@ -50,8 +50,8 @@ RSpec.shared_examples 'a noteable graphql type we can query' do
post_graphql(query(fields), current_user: current_user)
- data = graphql_data_at(*path_to_noteable, :discussions, :nodes, :noteable, :id)
- expect(data[0]).to eq(global_id_of(noteable))
+ entities = graphql_data_at(*path_to_noteable, :discussions, :nodes, :noteable)
+ expect(entities).to all(match(a_graphql_entity_for(noteable)))
end
end
@@ -62,10 +62,10 @@ RSpec.shared_examples 'a noteable graphql type we can query' do
def expected
noteable.notes.map do |note|
- include(
- 'id' => global_id_of(note),
- 'project' => include('id' => global_id_of(project)),
- 'author' => include('id' => global_id_of(note.author)),
+ a_graphql_entity_for(
+ note,
+ 'project' => a_graphql_entity_for(project),
+ 'author' => a_graphql_entity_for(note.author),
'createdAt' => note.created_at.iso8601,
'body' => eq(note.note)
)
diff --git a/spec/support/shared_examples/requests/api/graphql/packages/group_and_project_packages_list_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/packages/group_and_project_packages_list_shared_examples.rb
index 127b1a6d4c4..9f7ec6e90e9 100644
--- a/spec/support/shared_examples/requests/api/graphql/packages/group_and_project_packages_list_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/graphql/packages/group_and_project_packages_list_shared_examples.rb
@@ -104,7 +104,7 @@ RSpec.shared_examples 'group and project packages query' do
}
end
- let(:expected_packages) { sorted_packages.map { |package| global_id_of(package) } }
+ let(:expected_packages) { sorted_packages.map { |package| global_id_of(package).to_s } }
let(:data_path) { [resource_type, :packages] }
@@ -191,4 +191,91 @@ RSpec.shared_examples 'group and project packages query' do
it { is_expected.to include({ "name" => versionless_package.name }) }
end
end
+
+ context 'when reading pipelines' do
+ let(:npm_pipelines) { create_list(:ci_pipeline, 6, project: project1) }
+ let(:npm_pipeline_gids) { npm_pipelines.sort_by(&:id).map(&:to_gid).map(&:to_s).reverse }
+ let(:composer_pipelines) { create_list(:ci_pipeline, 6, project: project2) }
+ let(:composer_pipeline_gids) { composer_pipelines.sort_by(&:id).map(&:to_gid).map(&:to_s).reverse }
+ let(:npm_end_cursor) { graphql_data_npm_package.dig('pipelines', 'pageInfo', 'endCursor') }
+ let(:npm_start_cursor) { graphql_data_npm_package.dig('pipelines', 'pageInfo', 'startCursor') }
+ let(:pipelines_nodes) do
+ <<~QUERY
+ nodes {
+ id
+ }
+ pageInfo {
+ startCursor
+ endCursor
+ }
+ QUERY
+ end
+
+ before do
+ resource.add_maintainer(current_user)
+
+ npm_pipelines.each do |pipeline|
+ create(:package_build_info, package: npm_package, pipeline: pipeline)
+ end
+
+ composer_pipelines.each do |pipeline|
+ create(:package_build_info, package: composer_package, pipeline: pipeline)
+ end
+ end
+
+ it 'loads the second page with pagination first correctly' do
+ run_query(first: 2)
+ expect(npm_pipeline_ids).to eq(npm_pipeline_gids[0..1])
+ expect(composer_pipeline_ids).to eq(composer_pipeline_gids[0..1])
+
+ run_query(first: 2, after: npm_end_cursor)
+ expect(npm_pipeline_ids).to eq(npm_pipeline_gids[2..3])
+ expect(composer_pipeline_ids).to be_empty
+ end
+
+ it 'loads the second page with pagination last correctly' do
+ run_query(last: 2)
+ expect(npm_pipeline_ids).to eq(npm_pipeline_gids[4..5])
+ expect(composer_pipeline_ids).to eq(composer_pipeline_gids[4..5])
+
+ run_query(last: 2, before: npm_start_cursor)
+ expect(npm_pipeline_ids).to eq(npm_pipeline_gids[2..3])
+ expect(composer_pipeline_ids).to eq(composer_pipeline_gids[4..5])
+ end
+
+ def run_query(args)
+ pipelines_field = query_graphql_field('pipelines', args, pipelines_nodes)
+
+ packages_nodes = <<~QUERY
+ nodes {
+ id
+ #{pipelines_field}
+ }
+ QUERY
+
+ query = graphql_query_for(
+ resource_type,
+ { 'fullPath' => resource.full_path },
+ query_graphql_field('packages', {}, packages_nodes)
+ )
+
+ post_graphql(query, current_user: current_user)
+ end
+
+ def npm_pipeline_ids
+ graphql_data_npm_package.dig('pipelines', 'nodes').map { |pipeline| pipeline['id'] }
+ end
+
+ def composer_pipeline_ids
+ graphql_data_composer_package.dig('pipelines', 'nodes').map { |pipeline| pipeline['id'] }
+ end
+
+ def graphql_data_npm_package
+ graphql_data_at(resource_type, :packages, :nodes).find { |pkg| pkg['id'] == npm_package.to_gid.to_s }
+ end
+
+ def graphql_data_composer_package
+ graphql_data_at(resource_type, :packages, :nodes).find { |pkg| pkg['id'] == composer_package.to_gid.to_s }
+ end
+ end
end
diff --git a/spec/support/shared_examples/requests/api/graphql/packages/package_details_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/packages/package_details_shared_examples.rb
index ab93f54111b..b4019d7c232 100644
--- a/spec/support/shared_examples/requests/api/graphql/packages/package_details_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/graphql/packages/package_details_shared_examples.rb
@@ -28,14 +28,10 @@ RSpec.shared_examples 'a package with files' do
end
it 'has the basic package files data' do
- expect(first_file_response).to include(
- 'id' => global_id_of(first_file),
- 'fileName' => first_file.file_name,
- 'size' => first_file.size.to_s,
- 'downloadPath' => first_file.download_path,
- 'fileSha1' => first_file.file_sha1,
- 'fileMd5' => first_file.file_md5,
- 'fileSha256' => first_file.file_sha256
+ expect(first_file_response).to match a_graphql_entity_for(
+ first_file,
+ :file_name, :download_path, :file_sha1, :file_md5, :file_sha256,
+ 'size' => first_file.size.to_s
)
end
diff --git a/spec/support/shared_examples/requests/api/graphql/projects/alert_management/integrations_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/projects/alert_management/integrations_shared_examples.rb
index c134f7d1839..3c5f25baaa1 100644
--- a/spec/support/shared_examples/requests/api/graphql/projects/alert_management/integrations_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/graphql/projects/alert_management/integrations_shared_examples.rb
@@ -30,14 +30,12 @@ RSpec.shared_examples 'GraphQL query with several integrations requested' do |gr
it 'returns the correct properties of the integrations', :aggregate_failures do
post_graphql(multi_selection_query, current_user: current_user)
- expect(graphql_data.dig('project', 'ai', 'nodes')).to include(
- 'id' => global_id_of(active_http_integration),
- 'name' => active_http_integration.name
+ expect(graphql_data.dig('project', 'ai', 'nodes')).to match a_graphql_entity_for(
+ active_http_integration, :name
)
- expect(graphql_data.dig('project', 'ii', 'nodes')).to include(
- 'id' => global_id_of(inactive_http_integration),
- 'name' => inactive_http_integration.name
+ expect(graphql_data.dig('project', 'ii', 'nodes')).to match a_graphql_entity_for(
+ inactive_http_integration, :name
)
end
diff --git a/spec/support/shared_examples/requests/api/milestones_shared_examples.rb b/spec/support/shared_examples/requests/api/milestones_shared_examples.rb
index 249a7b7cdac..1ea11ba3d7c 100644
--- a/spec/support/shared_examples/requests/api/milestones_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/milestones_shared_examples.rb
@@ -203,16 +203,16 @@ RSpec.shared_examples 'group and project milestones' do |route_definition|
end
describe "DELETE #{route_definition}/:milestone_id" do
- it "rejects a member with reporter access from deleting a milestone" do
- reporter = create(:user)
- milestone.resource_parent.add_reporter(reporter)
+ it "rejects a member with guest access from deleting a milestone" do
+ guest = create(:user)
+ milestone.resource_parent.add_guest(guest)
- delete api(resource_route, reporter)
+ delete api(resource_route, guest)
expect(response).to have_gitlab_http_status(:forbidden)
end
- it 'deletes the milestone when the user has developer access to the project' do
+ it 'deletes the milestone when the user has reporter access to the project' do
delete api(resource_route, user)
expect(project.milestones.find_by_id(milestone.id)).to be_nil
diff --git a/spec/support/shared_examples/requests/rack_attack_shared_examples.rb b/spec/support/shared_examples/requests/rack_attack_shared_examples.rb
index 68cb91d7414..d4417b23a5f 100644
--- a/spec/support/shared_examples/requests/rack_attack_shared_examples.rb
+++ b/spec/support/shared_examples/requests/rack_attack_shared_examples.rb
@@ -149,6 +149,7 @@ RSpec.shared_examples 'rate-limited token requests' do
arguments = a_hash_including({
message: 'Rack_Attack',
+ status: 429,
env: :throttle,
remote_ip: '127.0.0.1',
request_method: request_method,
@@ -314,6 +315,7 @@ RSpec.shared_examples 'rate-limited web authenticated requests' do
arguments = a_hash_including({
message: 'Rack_Attack',
+ status: 429,
env: :throttle,
remote_ip: '127.0.0.1',
request_method: request_method,
@@ -391,14 +393,16 @@ RSpec.shared_examples 'tracking when dry-run mode is set' do
end
it 'logs RackAttack info into structured logs' do
- arguments = a_hash_including({
- message: 'Rack_Attack',
- env: :track,
- remote_ip: '127.0.0.1',
- matched: throttle_name
- })
+ expect(Gitlab::AuthLogger).to receive(:error) do |arguments|
+ expect(arguments).to include(
+ message: 'Rack_Attack',
+ env: :track,
+ remote_ip: '127.0.0.1',
+ matched: throttle_name
+ )
- expect(Gitlab::AuthLogger).to receive(:error).with(arguments)
+ expect(arguments).not_to have_key(:status)
+ end
(1 + requests_per_period).times do
do_request
@@ -576,6 +580,7 @@ RSpec.shared_examples 'rate-limited unauthenticated requests' do
arguments = a_hash_including({
message: 'Rack_Attack',
+ status: 429,
env: :throttle,
remote_ip: '127.0.0.1',
request_method: 'GET',
diff --git a/spec/support/shared_examples/serializers/environment_serializer_shared_examples.rb b/spec/support/shared_examples/serializers/environment_serializer_shared_examples.rb
index fcd52cdf7fa..e1baa594f3c 100644
--- a/spec/support/shared_examples/serializers/environment_serializer_shared_examples.rb
+++ b/spec/support/shared_examples/serializers/environment_serializer_shared_examples.rb
@@ -1,5 +1,5 @@
# frozen_string_literal: true
-RSpec.shared_examples 'avoid N+1 on environments serialization' do |ee: false|
+RSpec.shared_examples 'avoid N+1 on environments serialization' do
it 'avoids N+1 database queries with grouping', :request_store do
create_environment_with_associations(project)
diff --git a/spec/support/shared_examples/services/issuable/destroy_service_shared_examples.rb b/spec/support/shared_examples/services/issuable/destroy_service_shared_examples.rb
index e776c098fa0..31571b1ffb9 100644
--- a/spec/support/shared_examples/services/issuable/destroy_service_shared_examples.rb
+++ b/spec/support/shared_examples/services/issuable/destroy_service_shared_examples.rb
@@ -1,21 +1,33 @@
# frozen_string_literal: true
-shared_examples_for 'service deleting todos' do
+shared_examples_for 'service scheduling async deletes' do
it 'destroys associated todos asynchronously' do
- expect(TodosDestroyer::DestroyedIssuableWorker)
+ expect(worker_class)
.to receive(:perform_async)
.with(issuable.id, issuable.class.name)
subject.execute(issuable)
end
-end
-shared_examples_for 'service deleting label links' do
- it 'destroys associated label links asynchronously' do
- expect(Issuable::LabelLinksDestroyWorker)
+ it 'works inside a transaction' do
+ expect(worker_class)
.to receive(:perform_async)
.with(issuable.id, issuable.class.name)
- subject.execute(issuable)
+ ApplicationRecord.transaction do
+ subject.execute(issuable)
+ end
+ end
+end
+
+shared_examples_for 'service deleting todos' do
+ it_behaves_like 'service scheduling async deletes' do
+ let(:worker_class) { TodosDestroyer::DestroyedIssuableWorker }
+ end
+end
+
+shared_examples_for 'service deleting label links' do
+ it_behaves_like 'service scheduling async deletes' do
+ let(:worker_class) { Issuable::LabelLinksDestroyWorker }
end
end
diff --git a/spec/support/shared_examples/services/jira/requests/base_shared_examples.rb b/spec/support/shared_examples/services/jira/requests/base_shared_examples.rb
index c4f6273b46c..5e49bdd706c 100644
--- a/spec/support/shared_examples/services/jira/requests/base_shared_examples.rb
+++ b/spec/support/shared_examples/services/jira/requests/base_shared_examples.rb
@@ -66,18 +66,12 @@ RSpec.shared_examples 'a service that handles Jira API errors' do
it 'logs the error' do
stub_client_and_raise(Timeout::Error, 'foo')
- expect(Gitlab::ProjectServiceLogger).to receive(:error).with(
- hash_including(
- client_url: be_present,
- message: 'Error sending message',
- service_class: described_class.name,
- error: hash_including(
- exception_class: Timeout::Error.name,
- exception_message: 'foo',
- exception_backtrace: be_present
- )
- )
+ expect(jira_integration).to receive(:log_exception).with(
+ kind_of(Timeout::Error),
+ message: 'Error sending message',
+ client_url: jira_integration.url
)
+
expect(subject).to be_error
end
diff --git a/spec/support/shared_examples/work_item_base_types_importer.rb b/spec/support/shared_examples/work_item_base_types_importer.rb
index 68e37171ea2..593670ac4b8 100644
--- a/spec/support/shared_examples/work_item_base_types_importer.rb
+++ b/spec/support/shared_examples/work_item_base_types_importer.rb
@@ -1,10 +1,48 @@
# frozen_string_literal: true
RSpec.shared_examples 'work item base types importer' do
- it 'creates all base work item types' do
- # Fixtures need to run on a pristine DB, but the test suite preloads the base types before(:suite)
+ it "creates all base work item types if they don't exist" do
WorkItems::Type.delete_all
expect { subject }.to change(WorkItems::Type, :count).from(0).to(WorkItems::Type::BASE_TYPES.count)
+
+ types_in_db = WorkItems::Type.all.map { |type| type.slice(:base_type, :icon_name, :name).symbolize_keys }
+ expected_types = WorkItems::Type::BASE_TYPES.map do |type, attributes|
+ attributes.slice(:icon_name, :name).merge(base_type: type.to_s)
+ end
+
+ expect(types_in_db).to match_array(expected_types)
+ expect(WorkItems::Type.all).to all(be_valid)
+ end
+
+ it 'upserts base work item types if they already exist' do
+ first_type = WorkItems::Type.first
+ original_name = first_type.name
+
+ first_type.update!(name: original_name.upcase)
+
+ expect do
+ subject
+ first_type.reload
+ end.to not_change(WorkItems::Type, :count).and(
+ change(first_type, :name).from(original_name.upcase).to(original_name)
+ )
+ end
+
+ it 'executes a single INSERT query' do
+ expect { subject }.to make_queries_matching(/INSERT/, 1)
+ end
+
+ context 'when some base types exist' do
+ before do
+ WorkItems::Type.limit(1).delete_all
+ end
+
+ it 'inserts all types and does nothing if some already existed' do
+ expect { subject }.to make_queries_matching(/INSERT/, 1).and(
+ change(WorkItems::Type, :count).by(1)
+ )
+ expect(WorkItems::Type.count).to eq(WorkItems::Type::BASE_TYPES.count)
+ end
end
end
diff --git a/spec/support/shared_examples/workers/batched_background_migration_worker_shared_examples.rb b/spec/support/shared_examples/workers/batched_background_migration_worker_shared_examples.rb
index 26731f34ed6..3d4e840fe2d 100644
--- a/spec/support/shared_examples/workers/batched_background_migration_worker_shared_examples.rb
+++ b/spec/support/shared_examples/workers/batched_background_migration_worker_shared_examples.rb
@@ -205,4 +205,123 @@ RSpec.shared_examples 'it runs batched background migration jobs' do |tracking_d
end
end
end
+
+ describe 'executing an entire migration', :freeze_time, if: Gitlab::Database.has_config?(tracking_database) do
+ include Gitlab::Database::DynamicModelHelpers
+
+ let(:migration_class) do
+ Class.new(Gitlab::BackgroundMigration::BatchedMigrationJob) do
+ def perform(matching_status)
+ each_sub_batch(
+ operation_name: :update_all,
+ batching_scope: -> (relation) { relation.where(status: matching_status) }
+ ) do |sub_batch|
+ sub_batch.update_all(some_column: 0)
+ end
+ end
+ end
+ end
+
+ let!(:migration) do
+ create(
+ :batched_background_migration,
+ :active,
+ table_name: table_name,
+ column_name: :id,
+ max_value: migration_records,
+ batch_size: batch_size,
+ sub_batch_size: sub_batch_size,
+ job_class_name: 'ExampleDataMigration',
+ job_arguments: [1]
+ )
+ end
+
+ let(:table_name) { 'example_data' }
+ let(:batch_size) { 5 }
+ let(:sub_batch_size) { 2 }
+ let(:number_of_batches) { 10 }
+ let(:migration_records) { batch_size * number_of_batches }
+
+ let(:connection) { Gitlab::Database.database_base_models[tracking_database].connection }
+ let(:example_data) { define_batchable_model(table_name, connection: connection) }
+
+ around do |example|
+ Gitlab::Database::SharedModel.using_connection(connection) do
+ example.run
+ end
+ end
+
+ before do
+ # Create example table populated with test data to migrate.
+ #
+ # Test data should have two records that won't be updated:
+ # - one record beyond the migration's range
+ # - one record that doesn't match the migration job's batch condition
+ connection.execute(<<~SQL)
+ CREATE TABLE #{table_name} (
+ id integer primary key,
+ some_column integer,
+ status smallint);
+
+ INSERT INTO #{table_name} (id, some_column, status)
+ SELECT generate_series, generate_series, 1
+ FROM generate_series(1, #{migration_records + 1});
+
+ UPDATE #{table_name}
+ SET status = 0
+ WHERE some_column = #{migration_records - 5};
+ SQL
+
+ stub_feature_flags(execute_batched_migrations_on_schedule: true)
+
+ stub_const('Gitlab::BackgroundMigration::ExampleDataMigration', migration_class)
+ end
+
+ subject(:full_migration_run) do
+ # process all batches, then do an extra execution to mark the job as finished
+ (number_of_batches + 1).times do
+ described_class.new.perform
+
+ travel_to((migration.interval + described_class::INTERVAL_VARIANCE).seconds.from_now)
+ end
+ end
+
+ it 'marks the migration record as finished' do
+ expect { full_migration_run }.to change { migration.reload.status }.from(1).to(3) # active -> finished
+ end
+
+ it 'creates job records for each processed batch', :aggregate_failures do
+ expect { full_migration_run }.to change { migration.reload.batched_jobs.count }.from(0)
+
+ final_min_value = migration.batched_jobs.reduce(1) do |next_min_value, batched_job|
+ expect(batched_job.min_value).to eq(next_min_value)
+
+ batched_job.max_value + 1
+ end
+
+ final_max_value = final_min_value - 1
+ expect(final_max_value).to eq(migration_records)
+ end
+
+ it 'marks all job records as succeeded', :aggregate_failures do
+ expect { full_migration_run }.to change { migration.reload.batched_jobs.count }.from(0)
+
+ expect(migration.batched_jobs).to all(be_succeeded)
+ end
+
+ it 'updates matching records in the range', :aggregate_failures do
+ expect { full_migration_run }
+ .to change { example_data.where('status = 1 AND some_column <> 0').count }
+ .from(migration_records).to(1)
+
+ record_outside_range = example_data.last
+
+ expect(record_outside_range.status).to eq(1)
+ expect(record_outside_range.some_column).not_to eq(0)
+ end
+
+ it 'does not update non-matching records in the range' do
+ expect { full_migration_run }.not_to change { example_data.where('status <> 1 AND some_column <> 0').count }
+ end
+ end
end
diff --git a/spec/support/shared_examples/workers/concerns/git_garbage_collect_methods_shared_examples.rb b/spec/support/shared_examples/workers/concerns/git_garbage_collect_methods_shared_examples.rb
index 4751d91efde..77c4a3431e2 100644
--- a/spec/support/shared_examples/workers/concerns/git_garbage_collect_methods_shared_examples.rb
+++ b/spec/support/shared_examples/workers/concerns/git_garbage_collect_methods_shared_examples.rb
@@ -202,6 +202,8 @@ RSpec.shared_examples 'can collect git garbage' do |update_statistics: true|
before do
expect(subject).to receive(:get_lease_uuid).and_return(lease_uuid)
+
+ statistics_keys.delete(:repository_size)
end
it_behaves_like 'it calls Gitaly'
diff --git a/spec/support/shared_examples/workers/concerns/reenqueuer_shared_examples.rb b/spec/support/shared_examples/workers/concerns/reenqueuer_shared_examples.rb
index d9105981b4b..2741b2a9de7 100644
--- a/spec/support/shared_examples/workers/concerns/reenqueuer_shared_examples.rb
+++ b/spec/support/shared_examples/workers/concerns/reenqueuer_shared_examples.rb
@@ -19,7 +19,13 @@ RSpec.shared_examples 'reenqueuer' do
describe '#perform' do
it 'tries to obtain a lease' do
- expect_to_obtain_exclusive_lease(subject.lease_key)
+ lease_key = if subject.respond_to?(:set_custom_lease_key)
+ subject.set_custom_lease_key(*job_args)
+ else
+ subject.lease_key
+ end
+
+ expect_to_obtain_exclusive_lease(lease_key)
subject_perform
end