Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'spec/support')
-rw-r--r--spec/support/factory_bot.rb1
-rw-r--r--spec/support/graphql/field_inspection.rb2
-rw-r--r--spec/support/graphql/field_selection.rb8
-rw-r--r--spec/support/graphql/resolver_factories.rb4
-rw-r--r--spec/support/helpers/callouts_test_helper.rb9
-rw-r--r--spec/support/helpers/countries_controller_test_helper.rb9
-rw-r--r--spec/support/helpers/doc_url_helper.rb21
-rw-r--r--spec/support/helpers/emails_helper_test_helper.rb9
-rw-r--r--spec/support/helpers/form_builder_helpers.rb13
-rw-r--r--spec/support/helpers/gitaly_setup.rb14
-rw-r--r--spec/support/helpers/graphql_helpers.rb155
-rw-r--r--spec/support/helpers/jira_integration_helpers.rb (renamed from spec/support/helpers/jira_service_helper.rb)20
-rw-r--r--spec/support/helpers/login_helpers.rb8
-rw-r--r--spec/support/helpers/namespaces_test_helper.rb4
-rw-r--r--spec/support/helpers/next_instance_of.rb2
-rw-r--r--spec/support/helpers/project_helpers.rb4
-rw-r--r--spec/support/helpers/project_template_test_helper.rb17
-rw-r--r--spec/support/helpers/search_settings_helpers.rb2
-rw-r--r--spec/support/helpers/stub_method_calls.rb66
-rw-r--r--spec/support/helpers/subscription_portal_helper.rb13
-rw-r--r--spec/support/helpers/test_env.rb2
-rw-r--r--spec/support/matchers/background_migrations_matchers.rb6
-rw-r--r--spec/support/matchers/exceed_query_limit.rb92
-rw-r--r--spec/support/shared_contexts/features/integrations/integrations_shared_context.rb40
-rw-r--r--spec/support/shared_contexts/finders/issues_finder_shared_contexts.rb26
-rw-r--r--spec/support/shared_contexts/finders/work_items_finder_shared_contexts.rb79
-rw-r--r--spec/support/shared_contexts/lib/gitlab/sidekiq_logging/structured_logger_shared_context.rb18
-rw-r--r--spec/support/shared_contexts/markdown_snapshot_shared_examples.rb64
-rw-r--r--spec/support/shared_contexts/navbar_structure_context.rb4
-rw-r--r--spec/support/shared_contexts/policies/project_policy_shared_context.rb2
-rw-r--r--spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb10
-rw-r--r--spec/support/shared_examples/components/pajamas_shared_examples.rb13
-rw-r--r--spec/support/shared_examples/controllers/environments_controller_shared_examples.rb17
-rw-r--r--spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb13
-rw-r--r--spec/support/shared_examples/features/2fa_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/features/access_tokens_shared_examples.rb6
-rw-r--r--spec/support/shared_examples/features/cascading_settings_shared_examples.rb3
-rw-r--r--spec/support/shared_examples/features/container_registry_shared_examples.rb5
-rw-r--r--spec/support/shared_examples/features/content_editor_shared_examples.rb70
-rw-r--r--spec/support/shared_examples/features/issuables_user_dropdown_behaviors_shared_examples.rb23
-rw-r--r--spec/support/shared_examples/features/runners_shared_examples.rb16
-rw-r--r--spec/support/shared_examples/features/sidebar_shared_examples.rb5
-rw-r--r--spec/support/shared_examples/finders/issues_finder_shared_examples.rb1471
-rw-r--r--spec/support/shared_examples/graphql/members_shared_examples.rb6
-rw-r--r--spec/support/shared_examples/graphql/mutations/incident_management_timeline_events_shared_examples.rb1
-rw-r--r--spec/support/shared_examples/graphql/mutations/security/ci_configuration_shared_examples.rb6
-rw-r--r--spec/support/shared_examples/graphql/n_plus_one_query_examples.rb (renamed from spec/support/shared_examples/graphql/projects/merge_request_n_plus_one_query_examples.rb)0
-rw-r--r--spec/support/shared_examples/graphql/resolvers/packages_resolvers_shared_examples.rb12
-rw-r--r--spec/support/shared_examples/graphql/types/gitlab_style_deprecations_shared_examples.rb8
-rw-r--r--spec/support/shared_examples/integrations/integration_settings_form.rb28
-rw-r--r--spec/support/shared_examples/lib/gitlab/ci/ci_trace_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/lib/gitlab/database/reestablished_connection_stack_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/lib/gitlab/event_store_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/lib/gitlab/redis/multi_store_feature_flags_shared_examples.rb51
-rw-r--r--spec/support/shared_examples/models/application_setting_shared_examples.rb12
-rw-r--r--spec/support/shared_examples/models/commit_signature_shared_examples.rb51
-rw-r--r--spec/support/shared_examples/models/concerns/limitable_shared_examples.rb24
-rw-r--r--spec/support/shared_examples/models/integrations/base_data_fields_shared_examples.rb51
-rw-r--r--spec/support/shared_examples/models/member_shared_examples.rb323
-rw-r--r--spec/support/shared_examples/models/members_notifications_shared_example.rb12
-rw-r--r--spec/support/shared_examples/models/wiki_shared_examples.rb8
-rw-r--r--spec/support/shared_examples/namespaces/traversal_scope_examples.rb22
-rw-r--r--spec/support/shared_examples/requests/api/project_statistics_refresh_conflicts_shared_examples.rb21
-rw-r--r--spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb56
-rw-r--r--spec/support/shared_examples/requests/projects/environments_controller_spec_shared_examples.rb18
-rw-r--r--spec/support/shared_examples/serializers/environment_serializer_shared_examples.rb10
-rw-r--r--spec/support/shared_examples/services/alert_management_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/services/boards/items_list_service_shared_examples.rb40
-rw-r--r--spec/support/shared_examples/views/pagination_shared_examples.rb35
-rw-r--r--spec/support/shared_examples/workers/background_migration_worker_shared_examples.rb238
-rw-r--r--spec/support/shared_examples/workers/batched_background_migration_worker_shared_examples.rb42
-rw-r--r--spec/support/shared_examples/workers/idempotency_shared_examples.rb14
72 files changed, 2950 insertions, 519 deletions
diff --git a/spec/support/factory_bot.rb b/spec/support/factory_bot.rb
index 5f22fa11e9e..6faa2db3330 100644
--- a/spec/support/factory_bot.rb
+++ b/spec/support/factory_bot.rb
@@ -2,6 +2,7 @@
FactoryBot::SyntaxRunner.class_eval do
include RSpec::Mocks::ExampleMethods
+ include StubMethodCalls
# FactoryBot doesn't allow yet to add a helper that can be used in factories
# While the fixture_file_upload helper is reasonable to be used there:
diff --git a/spec/support/graphql/field_inspection.rb b/spec/support/graphql/field_inspection.rb
index e5fe37ec555..8730f82b893 100644
--- a/spec/support/graphql/field_inspection.rb
+++ b/spec/support/graphql/field_inspection.rb
@@ -20,7 +20,7 @@ module Graphql
def type
@type ||= begin
- field_type = @field.type.respond_to?(:to_graphql) ? @field.type.to_graphql : @field.type
+ field_type = @field.type
# The type could be nested. For example `[GraphQL::Types::String]`:
# - List
diff --git a/spec/support/graphql/field_selection.rb b/spec/support/graphql/field_selection.rb
index 00323c46d69..432340cfdb5 100644
--- a/spec/support/graphql/field_selection.rb
+++ b/spec/support/graphql/field_selection.rb
@@ -46,7 +46,7 @@ module Graphql
NO_SKIP = ->(_name, _field) { false }
- def self.select_fields(type, skip = NO_SKIP, parent_types = Set.new, max_depth = 3)
+ def self.select_fields(type, skip = NO_SKIP, max_depth = 3)
return new if max_depth <= 0 || !type.kind.fields?
new(type.fields.flat_map do |name, field|
@@ -55,12 +55,8 @@ module Graphql
inspected = ::Graphql::FieldInspection.new(field)
singular_field_type = inspected.type
- # If field type is the same as parent type, then we're hitting into
- # mutual dependency. Break it from infinite recursion
- next [] if parent_types.include?(singular_field_type)
-
if inspected.nested_fields?
- subselection = select_fields(singular_field_type, skip, parent_types | [type], max_depth - 1)
+ subselection = select_fields(singular_field_type, skip, max_depth - 1)
next [] if subselection.empty?
[[name, subselection.to_h]]
diff --git a/spec/support/graphql/resolver_factories.rb b/spec/support/graphql/resolver_factories.rb
index 8188f17cc43..3c5aad34e8b 100644
--- a/spec/support/graphql/resolver_factories.rb
+++ b/spec/support/graphql/resolver_factories.rb
@@ -15,8 +15,8 @@ module Graphql
private
- def simple_resolver(resolved_value = 'Resolved value')
- Class.new(Resolvers::BaseResolver) do
+ def simple_resolver(resolved_value = 'Resolved value', base_class: Resolvers::BaseResolver)
+ Class.new(base_class) do
define_method :resolve do |**_args|
resolved_value
end
diff --git a/spec/support/helpers/callouts_test_helper.rb b/spec/support/helpers/callouts_test_helper.rb
new file mode 100644
index 00000000000..8c7faa71d9f
--- /dev/null
+++ b/spec/support/helpers/callouts_test_helper.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module CalloutsTestHelper
+ def callouts_trials_link_path
+ '/-/trial_registrations/new?glm_content=gold-callout&glm_source=gitlab.com'
+ end
+end
+
+CalloutsTestHelper.prepend_mod
diff --git a/spec/support/helpers/countries_controller_test_helper.rb b/spec/support/helpers/countries_controller_test_helper.rb
new file mode 100644
index 00000000000..5d36a29bba7
--- /dev/null
+++ b/spec/support/helpers/countries_controller_test_helper.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module CountriesControllerTestHelper
+ def world_deny_list
+ ::World::DENYLIST + ::World::JH_MARKET
+ end
+end
+
+CountriesControllerTestHelper.prepend_mod
diff --git a/spec/support/helpers/doc_url_helper.rb b/spec/support/helpers/doc_url_helper.rb
new file mode 100644
index 00000000000..bbff4827c56
--- /dev/null
+++ b/spec/support/helpers/doc_url_helper.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module DocUrlHelper
+ def version
+ "13.4.0-ee"
+ end
+
+ def doc_url(documentation_base_url)
+ "#{documentation_base_url}/13.4/ee/#{path}.html"
+ end
+
+ def doc_url_without_version(documentation_base_url)
+ "#{documentation_base_url}/ee/#{path}.html"
+ end
+
+ def stub_doc_file_read(file_name: 'index.md', content: )
+ expect_file_read(File.join(Rails.root, 'doc', file_name), content: content)
+ end
+end
+
+DocUrlHelper.prepend_mod
diff --git a/spec/support/helpers/emails_helper_test_helper.rb b/spec/support/helpers/emails_helper_test_helper.rb
new file mode 100644
index 00000000000..ea7dbc89ebd
--- /dev/null
+++ b/spec/support/helpers/emails_helper_test_helper.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module EmailsHelperTestHelper
+ def default_header_logo
+ %r{<img alt="GitLab" src="/images/mailers/gitlab_logo\.(?:gif|png)" width="\d+" height="\d+" />}
+ end
+end
+
+EmailsHelperTestHelper.prepend_mod
diff --git a/spec/support/helpers/form_builder_helpers.rb b/spec/support/helpers/form_builder_helpers.rb
new file mode 100644
index 00000000000..4bae7421c4d
--- /dev/null
+++ b/spec/support/helpers/form_builder_helpers.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module FormBuilderHelpers
+ def fake_action_view_base
+ lookup_context = ActionView::LookupContext.new(ActionController::Base.view_paths)
+
+ ActionView::Base.new(lookup_context, {}, ApplicationController.new)
+ end
+
+ def fake_form_for(&block)
+ fake_action_view_base.form_for :user, url: '/user', &block
+ end
+end
diff --git a/spec/support/helpers/gitaly_setup.rb b/spec/support/helpers/gitaly_setup.rb
index 264281ef94a..56993fc27b7 100644
--- a/spec/support/helpers/gitaly_setup.rb
+++ b/spec/support/helpers/gitaly_setup.rb
@@ -9,6 +9,7 @@
require 'securerandom'
require 'socket'
require 'logger'
+require 'fileutils'
require 'bundler'
module GitalySetup
@@ -151,6 +152,9 @@ module GitalySetup
toml ||= config_path(service)
args = service_cmd(service, toml)
+ # Ensure that tmp/run exists
+ FileUtils.mkdir_p(runtime_dir)
+
# Ensure user configuration does not affect Git
# Context: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/58776#note_547613780
env = self.env.merge('HOME' => nil, 'XDG_CONFIG_HOME' => nil)
@@ -369,7 +373,7 @@ module GitalySetup
message += "- The `gitaly` binary does not exist: #{gitaly_binary}\n" unless File.exist?(gitaly_binary)
message += "- The `praefect` binary does not exist: #{praefect_binary}\n" unless File.exist?(praefect_binary)
- message += "- The `git` binary does not exist: #{git_binary}\n" unless File.exist?(git_binary)
+ message += "- No `git` binaries exist\n" if git_binaries.empty?
message += "\nCheck log/gitaly-test.log & log/praefect-test.log for errors.\n"
@@ -381,8 +385,8 @@ module GitalySetup
message
end
- def git_binary
- File.join(tmp_tests_gitaly_dir, "_build", "bin", "gitaly-git")
+ def git_binaries
+ Dir.glob(File.join(tmp_tests_gitaly_dir, "_build", "bin", "gitaly-git-v*"))
end
def gitaly_binary
@@ -392,8 +396,4 @@ module GitalySetup
def praefect_binary
File.join(tmp_tests_gitaly_dir, "_build", "bin", "praefect")
end
-
- def git_binary_exists?
- File.exist?(git_binary)
- end
end
diff --git a/spec/support/helpers/graphql_helpers.rb b/spec/support/helpers/graphql_helpers.rb
index db8d45f61ea..d0a1941817a 100644
--- a/spec/support/helpers/graphql_helpers.rb
+++ b/spec/support/helpers/graphql_helpers.rb
@@ -26,10 +26,44 @@ module GraphqlHelpers
end
end
+ # Some arguments use `as:` to expose a different name internally.
+ # Transform the args to use those names
+ def self.deep_transform_args(args, field)
+ args.to_h do |k, v|
+ argument = field.arguments[k.to_s.camelize(:lower)]
+ [argument.keyword, v.is_a?(Hash) ? deep_transform_args(v, argument.type) : v]
+ end
+ end
+
+ # Convert incoming args into the form usually passed in from the client,
+ # all strings, etc.
+ def self.as_graphql_argument_literals(args)
+ args.transform_values { |value| transform_arg_value(value) }
+ end
+
+ def self.transform_arg_value(value)
+ case value
+ when Hash
+ as_graphql_argument_literals(value)
+ when Array
+ value.map { |x| transform_arg_value(x) }
+ when Time, ActiveSupport::TimeWithZone
+ value.strftime("%F %T.%N %z")
+ when Date, GlobalID, Symbol
+ value.to_s
+ else
+ value
+ end
+ end
+
# Run this resolver exactly as it would be called in the framework. This
# includes all authorization hooks, all argument processing and all result
# wrapping.
# see: GraphqlHelpers#resolve_field
+ #
+ # TODO: this is too coupled to gem internals, making upgrades incredibly
+ # painful, and bypasses much of the validation of the framework.
+ # See https://gitlab.com/gitlab-org/gitlab/-/issues/363121
def resolve(
resolver_class, # [Class[<= BaseResolver]] The resolver at test.
obj: nil, # [Any] The BaseObject#object for the resolver (available as `#object` in the resolver).
@@ -37,7 +71,8 @@ module GraphqlHelpers
ctx: {}, # [#to_h] The current context values.
schema: GitlabSchema, # [GraphQL::Schema] Schema to use during execution.
parent: :not_given, # A GraphQL query node to be passed as the `:parent` extra.
- lookahead: :not_given # A GraphQL lookahead object to be passed as the `:lookahead` extra.
+ lookahead: :not_given, # A GraphQL lookahead object to be passed as the `:lookahead` extra.
+ arg_style: :internal_prepared # Args are in internal format, but should use more rigorous processing
)
# All resolution goes through fields, so we need to create one here that
# uses our resolver. Thankfully, apart from the field name, resolvers
@@ -49,7 +84,6 @@ module GraphqlHelpers
field = ::Types::BaseField.new(**field_options)
# All mutations accept a single `:input` argument. Wrap arguments here.
- # See the unwrapping below in GraphqlHelpers#resolve_field
args = { input: args } if resolver_class <= ::Mutations::BaseMutation && !args.key?(:input)
resolve_field(field, obj,
@@ -57,7 +91,8 @@ module GraphqlHelpers
ctx: ctx,
schema: schema,
object_type: resolver_parent,
- extras: { parent: parent, lookahead: lookahead })
+ extras: { parent: parent, lookahead: lookahead },
+ arg_style: arg_style)
end
# Resolve the value of a field on an object.
@@ -85,21 +120,22 @@ module GraphqlHelpers
# NB: Arguments are passed from the client's perspective. If there is an argument
# `foo` aliased as `bar`, then we would pass `args: { bar: the_value }`, and
# types are checked before resolution.
+ # rubocop:disable Metrics/ParameterLists
def resolve_field(
- field, # An instance of `BaseField`, or the name of a field on the current described_class
- object, # The current object of the `BaseObject` this field 'belongs' to
- args: {}, # Field arguments (keys will be fieldnamerized)
- ctx: {}, # Context values (important ones are :current_user)
- extras: {}, # Stub values for field extras (parent and lookahead)
- current_user: :not_given, # The current user (specified explicitly, overrides ctx[:current_user])
- schema: GitlabSchema, # A specific schema instance
- object_type: described_class # The `BaseObject` type this field belongs to
+ field, # An instance of `BaseField`, or the name of a field on the current described_class
+ object, # The current object of the `BaseObject` this field 'belongs' to
+ args: {}, # Field arguments (keys will be fieldnamerized)
+ ctx: {}, # Context values (important ones are :current_user)
+ extras: {}, # Stub values for field extras (parent and lookahead)
+ current_user: :not_given, # The current user (specified explicitly, overrides ctx[:current_user])
+ schema: GitlabSchema, # A specific schema instance
+ object_type: described_class, # The `BaseObject` type this field belongs to
+ arg_style: :internal_prepared # Args are in internal format, but should use more rigorous processing
)
field = to_base_field(field, object_type)
ctx[:current_user] = current_user unless current_user == :not_given
query = GraphQL::Query.new(schema, context: ctx.to_h)
extras[:lookahead] = negative_lookahead if extras[:lookahead] == :not_given && field.extras.include?(:lookahead)
-
query_ctx = query.context
mock_extras(query_ctx, **extras)
@@ -107,29 +143,58 @@ module GraphqlHelpers
parent = object_type.authorized_new(object, query_ctx)
raise UnauthorizedObject unless parent
- # TODO: This will need to change when we move to the interpreter:
- # At that point, arguments will be a plain ruby hash rather than
- # an Arguments object
- # see: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/27536
- # https://gitlab.com/gitlab-org/gitlab/-/issues/210556
- arguments = field.to_graphql.arguments_class.new(
- GraphqlHelpers.deep_fieldnamerize(args),
- context: query_ctx,
- defaults_used: []
- )
-
# we enable the request store so we can track gitaly calls.
::Gitlab::WithRequestStore.with_request_store do
- # TODO: This will need to change when we move to the interpreter - at that
- # point we will call `field#resolve`
-
- # Unwrap the arguments to mutations. This pairs with the wrapping in GraphqlHelpers#resolve
- # If arguments are not wrapped first, then arguments processing will raise.
- # If arguments are not unwrapped here, then the resolve method of the mutation will raise argument errors.
- arguments = arguments.to_kwargs[:input] if field.resolver && field.resolver <= ::Mutations::BaseMutation
+ prepared_args = case arg_style
+ when :internal_prepared
+ args_internal_prepared(field, args: args, query_ctx: query_ctx, parent: parent, extras: extras, query: query)
+ else
+ args_internal(field, args: args, query_ctx: query_ctx, parent: parent, extras: extras, query: query)
+ end
+
+ if prepared_args.class <= Gitlab::Graphql::Errors::BaseError
+ prepared_args
+ else
+ field.resolve(parent, prepared_args, query_ctx)
+ end
+ end
+ end
+ # rubocop:enable Metrics/ParameterLists
- field.resolve_field(parent, arguments, query_ctx)
+ # Pros:
+ # - Original way we handled arguments
+ #
+ # Cons:
+ # - the `prepare` method of a type is not called. Whether as a proc or as a method
+ # on the type, it's not called. For example `:cluster_id` in ee/app/graphql/resolvers/vulnerabilities_resolver.rb,
+ # or `prepare` in app/graphql/types/range_input_type.rb, used by Types::TimeframeInputType
+ def args_internal(field, args:, query_ctx:, parent:, extras:, query:)
+ arguments = GraphqlHelpers.deep_transform_args(args, field)
+ arguments.merge!(extras.reject {|k, v| v == :not_given})
+ end
+
+ # Pros:
+ # - Allows the use of ruby types, without having to pass in strings
+ # - All args are converted into strings just like if it was called from a client
+ # - Much stronger argument verification
+ #
+ # Cons:
+ # - Some values, such as enums, would need to be changed in the specs to use the
+ # external values, because there is no easy way to handle them.
+ #
+ # take internal style args, and force them into client style args
+ def args_internal_prepared(field, args:, query_ctx:, parent:, extras:, query:)
+ arguments = GraphqlHelpers.as_graphql_argument_literals(args)
+ arguments.merge!(extras.reject {|k, v| v == :not_given})
+
+ # Use public API to properly prepare the args for use by the resolver.
+ # It uses `coerce_arguments` under the covers
+ prepared_args = nil
+ query.arguments_cache.dataload_for(GraphqlHelpers.deep_fieldnamerize(arguments), field, parent) do |kwarg_arguments|
+ prepared_args = kwarg_arguments
end
+
+ prepared_args.respond_to?(:keyword_arguments) ? prepared_args.keyword_arguments : prepared_args
end
def mock_extras(context, parent: :not_given, lookahead: :not_given)
@@ -148,7 +213,7 @@ module GraphqlHelpers
def resolver_instance(resolver_class, obj: nil, ctx: {}, field: nil, schema: GitlabSchema, subscription_update: false)
if ctx.is_a?(Hash)
- q = double('Query', schema: schema, subscription_update?: subscription_update)
+ q = double('Query', schema: schema, subscription_update?: subscription_update, warden: GraphQL::Schema::Warden::PassThruWarden)
ctx = GraphQL::Query::Context.new(query: q, object: obj, values: ctx)
end
@@ -357,8 +422,8 @@ module GraphqlHelpers
end
end
- def query_double(schema:)
- double('query', schema: schema)
+ def query_double(schema: empty_schema)
+ double('query', schema: schema, warden: GraphQL::Schema::Warden::PassThruWarden)
end
def wrap_fields(fields)
@@ -380,7 +445,7 @@ module GraphqlHelpers
FIELDS
end
- def all_graphql_fields_for(class_name, parent_types = Set.new, max_depth: 3, excluded: [])
+ def all_graphql_fields_for(class_name, max_depth: 3, excluded: [])
# pulling _all_ fields can generate a _huge_ query (like complexity 180,000),
# and significantly increase spec runtime. so limit the depth by default
return if max_depth <= 0
@@ -397,7 +462,7 @@ module GraphqlHelpers
# We can't guess arguments, so skip fields that require them
skip = ->(name, field) { excluded.include?(name) || required_arguments?(field) }
- ::Graphql::FieldSelection.select_fields(type, skip, parent_types, max_depth)
+ ::Graphql::FieldSelection.select_fields(type, skip, max_depth)
end
def with_signature(variables, query)
@@ -569,8 +634,11 @@ module GraphqlHelpers
# Helps migrate to the new GraphQL interpreter,
# https://gitlab.com/gitlab-org/gitlab/-/issues/210556
- def expect_graphql_error_to_be_created(error_class, match_message = nil)
- expect { yield }.to raise_error(error_class, match_message)
+ def expect_graphql_error_to_be_created(error_class, match_message = '')
+ resolved = yield
+
+ expect(resolved).to be_instance_of(error_class)
+ expect(resolved.message).to match(match_message)
end
def flattened_errors
@@ -644,7 +712,7 @@ module GraphqlHelpers
end
def allow_high_graphql_recursion
- allow_any_instance_of(Gitlab::Graphql::QueryAnalyzers::RecursionAnalyzer).to receive(:recursion_threshold).and_return 1000
+ allow_any_instance_of(Gitlab::Graphql::QueryAnalyzers::AST::RecursionAnalyzer).to receive(:recursion_threshold).and_return 1000
end
def allow_high_graphql_transaction_threshold
@@ -699,13 +767,13 @@ module GraphqlHelpers
end
# 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)
+ def execute_query(query_type = Types::QueryType, schema: empty_schema, graphql: query_string, raise_on_error: false, variables: {})
schema.query(query_type)
r = schema.execute(
graphql,
context: { current_user: user },
- variables: {}
+ variables: variables
)
if raise_on_error && r.to_h['errors'].present?
@@ -717,7 +785,6 @@ module GraphqlHelpers
def empty_schema
Class.new(GraphQL::Schema) do
- use GraphQL::Pagination::Connections
use Gitlab::Graphql::Pagination::Connections
use BatchLoader::GraphQL
@@ -817,7 +884,3 @@ module GraphqlHelpers
object_type.fields[name] || (raise ArgumentError, "Unknown field #{name} for #{described_class.graphql_name}")
end
end
-
-# This warms our schema, doing this as part of loading the helpers to avoid
-# duplicate loading error when Rails tries autoload the types.
-GitlabSchema.graphql_definition
diff --git a/spec/support/helpers/jira_service_helper.rb b/spec/support/helpers/jira_integration_helpers.rb
index 3cfd0de06e8..66940314589 100644
--- a/spec/support/helpers/jira_service_helper.rb
+++ b/spec/support/helpers/jira_integration_helpers.rb
@@ -1,8 +1,8 @@
# frozen_string_literal: true
-module JiraServiceHelper
- JIRA_URL = "http://jira.example.net"
- JIRA_API = JIRA_URL + "/rest/api/2"
+module JiraIntegrationHelpers
+ JIRA_URL = 'http://jira.example.net'
+ JIRA_API = "#{JIRA_URL}/rest/api/2"
def jira_integration_settings
url = JIRA_URL
@@ -17,6 +17,7 @@ module JiraServiceHelper
end
def jira_issue_comments
+ # rubocop: disable Layout/LineLength
"{\"startAt\":0,\"maxResults\":11,\"total\":11,
\"comments\":[{\"self\":\"http://0.0.0.0:4567/rest/api/2/issue/10002/comment/10609\",
\"id\":\"10609\",\"author\":{\"self\":\"http://0.0.0.0:4567/rest/api/2/user?username=gitlab\",
@@ -51,30 +52,31 @@ module JiraServiceHelper
\"updated\":\"2015-04-01T03:45:55.667+0200\"
}
]}"
+ # rubocop: enable Layout/LineLength
end
def jira_project_url
- JIRA_API + "/project"
+ "#{JIRA_API}/project"
end
def jira_api_comment_url(issue_id)
- JIRA_API + "/issue/#{issue_id}/comment"
+ "#{JIRA_API}/issue/#{issue_id}/comment"
end
def jira_api_remote_link_url(issue_id)
- JIRA_API + "/issue/#{issue_id}/remotelink"
+ "#{JIRA_API}/issue/#{issue_id}/remotelink"
end
def jira_api_transition_url(issue_id)
- JIRA_API + "/issue/#{issue_id}/transitions"
+ "#{JIRA_API}/issue/#{issue_id}/transitions"
end
def jira_api_test_url
- JIRA_API + "/myself"
+ "#{JIRA_API}/myself"
end
def jira_issue_url(issue_id)
- JIRA_API + "/issue/#{issue_id}"
+ "#{JIRA_API}/issue/#{issue_id}"
end
def stub_jira_integration_test
diff --git a/spec/support/helpers/login_helpers.rb b/spec/support/helpers/login_helpers.rb
index 29b1bb260f2..c93ef8b0ead 100644
--- a/spec/support/helpers/login_helpers.rb
+++ b/spec/support/helpers/login_helpers.rb
@@ -117,6 +117,14 @@ module LoginHelpers
click_button "oauth-login-#{provider}"
end
+ def register_via(provider, uid, email, additional_info: {})
+ mock_auth_hash(provider, uid, email, additional_info: additional_info)
+ visit new_user_registration_path
+ expect(page).to have_content('Create an account using')
+
+ click_link_or_button "oauth-login-#{provider}"
+ end
+
def fake_successful_u2f_authentication
allow(U2fRegistration).to receive(:authenticate).and_return(true)
FakeU2fDevice.new(page, nil).fake_u2f_authentication
diff --git a/spec/support/helpers/namespaces_test_helper.rb b/spec/support/helpers/namespaces_test_helper.rb
index 9762c38a9bb..08224cfd43c 100644
--- a/spec/support/helpers/namespaces_test_helper.rb
+++ b/spec/support/helpers/namespaces_test_helper.rb
@@ -8,6 +8,10 @@ module NamespacesTestHelper
def get_buy_storage_path(namespace)
buy_storage_subscriptions_path(selected_group: namespace.id)
end
+
+ def get_buy_storage_url(namespace)
+ buy_storage_subscriptions_url(selected_group: namespace.id)
+ end
end
NamespacesTestHelper.prepend_mod
diff --git a/spec/support/helpers/next_instance_of.rb b/spec/support/helpers/next_instance_of.rb
index 461d411a5ce..3c88715615d 100644
--- a/spec/support/helpers/next_instance_of.rb
+++ b/spec/support/helpers/next_instance_of.rb
@@ -22,7 +22,7 @@ module NextInstanceOf
def stub_new(target, number, ordered = false, *new_args, &blk)
receive_new = receive(:new)
receive_new.ordered if ordered
- receive_new.with(*new_args) if new_args.any?
+ receive_new.with(*new_args) if new_args.present?
if number.is_a?(Range)
receive_new.at_least(number.begin).times if number.begin
diff --git a/spec/support/helpers/project_helpers.rb b/spec/support/helpers/project_helpers.rb
index 2ea6405e48c..ef8947ab340 100644
--- a/spec/support/helpers/project_helpers.rb
+++ b/spec/support/helpers/project_helpers.rb
@@ -17,12 +17,12 @@ module ProjectHelpers
end
end
- def update_feature_access_level(project, access_level)
+ def update_feature_access_level(project, access_level, additional_params = {})
features = ProjectFeature::FEATURES.dup
features.delete(:pages)
params = features.each_with_object({}) { |feature, h| h["#{feature}_access_level"] = access_level }
- project.update!(params)
+ project.update!(params.merge(additional_params))
end
def create_project_with_statistics(namespace = nil, with_data: false, size_multiplier: 1)
diff --git a/spec/support/helpers/project_template_test_helper.rb b/spec/support/helpers/project_template_test_helper.rb
new file mode 100644
index 00000000000..eab41f6a1cf
--- /dev/null
+++ b/spec/support/helpers/project_template_test_helper.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module ProjectTemplateTestHelper
+ def all_templates
+ %w[
+ rails spring express iosswift dotnetcore android
+ gomicro gatsby hugo jekyll plainhtml gitbook
+ hexo middleman gitpod_spring_petclinic nfhugo
+ nfjekyll nfplainhtml nfgitbook nfhexo salesforcedx
+ serverless_framework tencent_serverless_framework
+ jsonnet cluster_management kotlin_native_linux
+ pelican
+ ]
+ end
+end
+
+ProjectTemplateTestHelper.prepend_mod
diff --git a/spec/support/helpers/search_settings_helpers.rb b/spec/support/helpers/search_settings_helpers.rb
index 838f897bff5..a453ea7fa8f 100644
--- a/spec/support/helpers/search_settings_helpers.rb
+++ b/spec/support/helpers/search_settings_helpers.rb
@@ -1,5 +1,5 @@
# frozen_string_literal: true
module SearchHelpers
- self::INPUT_PLACEHOLDER = 'Search settings'
+ self::INPUT_PLACEHOLDER = 'Search page'
end
diff --git a/spec/support/helpers/stub_method_calls.rb b/spec/support/helpers/stub_method_calls.rb
new file mode 100644
index 00000000000..45d704958ca
--- /dev/null
+++ b/spec/support/helpers/stub_method_calls.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+# Used to stud methods for factories where we can't
+# use rspec-mocks.
+#
+# Examples:
+# stub_method(user, :some_method) { |var1, var2| var1 + var2 }
+# stub_method(user, :some_method) { true }
+# stub_method(user, :some_method) => nil
+# stub_method(user, :some_method) do |*args|
+# true
+# end
+#
+# restore_original_method(user, :some_method)
+# restore_original_methods(user)
+#
+module StubMethodCalls
+ AlreadyImplementedError = Class.new(StandardError)
+
+ def stub_method(object, method, &block)
+ Backup.stub_method(object, method, &block)
+ end
+
+ def restore_original_method(object, method)
+ Backup.restore_method(object, method)
+ end
+
+ def restore_original_methods(object)
+ Backup.stubbed_methods(object).each_key { |method, backed_up_method| restore_original_method(object, method) }
+ end
+
+ module Backup
+ def self.stubbed_methods(object)
+ return {} unless object.respond_to?(:_stubbed_methods)
+
+ object._stubbed_methods
+ end
+
+ def self.backup_method(object, method)
+ backed_up_methods = stubbed_methods(object)
+ backed_up_methods[method] = object.respond_to?(method) ? object.method(method) : nil
+
+ object.define_singleton_method(:_stubbed_methods) { backed_up_methods }
+ end
+
+ def self.stub_method(object, method, &block)
+ raise ArgumentError, "Block is required" unless block_given?
+
+ backup_method(object, method) unless backed_up_method?(object, method)
+ object.define_singleton_method(method, &block)
+ end
+
+ def self.restore_method(object, method)
+ raise NotImplementedError, "#{method} has not been stubbed on #{object}" unless backed_up_method?(object, method)
+
+ object.singleton_class.remove_method(method)
+ backed_up_method = stubbed_methods(object)[method]
+
+ object.define_singleton_method(method, backed_up_method) if backed_up_method
+ end
+
+ def self.backed_up_method?(object, method)
+ stubbed_methods(object).key?(method)
+ end
+ end
+end
diff --git a/spec/support/helpers/subscription_portal_helper.rb b/spec/support/helpers/subscription_portal_helper.rb
new file mode 100644
index 00000000000..53e8f78371d
--- /dev/null
+++ b/spec/support/helpers/subscription_portal_helper.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module SubscriptionPortalHelper
+ def staging_customers_url
+ 'https://customers.staging.gitlab.com'
+ end
+
+ def prod_customers_url
+ 'https://customers.gitlab.com'
+ end
+end
+
+SubscriptionPortalHelper.prepend_mod
diff --git a/spec/support/helpers/test_env.rb b/spec/support/helpers/test_env.rb
index 11f469c1d27..7c865dd7e11 100644
--- a/spec/support/helpers/test_env.rb
+++ b/spec/support/helpers/test_env.rb
@@ -53,7 +53,7 @@ module TestEnv
'wip' => 'b9238ee',
'csv' => '3dd0896',
'v1.1.0' => 'b83d6e3',
- 'add-ipython-files' => 'a867a602',
+ 'add-ipython-files' => '4963fef',
'add-pdf-file' => 'e774ebd',
'squash-large-files' => '54cec52',
'add-pdf-text-binary' => '79faa7b',
diff --git a/spec/support/matchers/background_migrations_matchers.rb b/spec/support/matchers/background_migrations_matchers.rb
index b471323dd72..c5b3e140585 100644
--- a/spec/support/matchers/background_migrations_matchers.rb
+++ b/spec/support/matchers/background_migrations_matchers.rb
@@ -65,11 +65,13 @@ RSpec::Matchers.define :be_scheduled_migration_with_multiple_args do |*expected|
end
end
-RSpec::Matchers.define :have_scheduled_batched_migration do |table_name: nil, column_name: nil, job_arguments: [], **attributes|
+RSpec::Matchers.define :have_scheduled_batched_migration do |gitlab_schema: :gitlab_main, table_name: nil, column_name: nil, job_arguments: [], **attributes|
define_method :matches? do |migration|
+ reset_column_information(Gitlab::Database::BackgroundMigration::BatchedMigration)
+
batched_migrations =
Gitlab::Database::BackgroundMigration::BatchedMigration
- .for_configuration(migration, table_name, column_name, job_arguments)
+ .for_configuration(gitlab_schema, migration, table_name, column_name, job_arguments)
expect(batched_migrations.count).to be(1)
expect(batched_migrations).to all(have_attributes(attributes)) if attributes.present?
diff --git a/spec/support/matchers/exceed_query_limit.rb b/spec/support/matchers/exceed_query_limit.rb
index b48c7f905b2..bfcaf9552b3 100644
--- a/spec/support/matchers/exceed_query_limit.rb
+++ b/spec/support/matchers/exceed_query_limit.rb
@@ -1,6 +1,68 @@
# frozen_string_literal: true
module ExceedQueryLimitHelpers
+ class QueryDiff
+ def initialize(expected, actual, show_common_queries)
+ @expected = expected
+ @actual = actual
+ @show_common_queries = show_common_queries
+ end
+
+ def diff
+ return combined_counts if @show_common_queries
+
+ combined_counts
+ .transform_values { select_suffixes_with_diffs(_1) }
+ .reject { |_prefix, suffs| suffs.empty? }
+ end
+
+ private
+
+ def select_suffixes_with_diffs(suffs)
+ reject_groups_with_different_parameters(reject_suffixes_with_identical_counts(suffs))
+ end
+
+ def reject_suffixes_with_identical_counts(suffs)
+ suffs.reject { |_k, counts| counts.first == counts.second }
+ end
+
+ # Eliminates groups that differ only in parameters,
+ # to make it easier to debug the output.
+ #
+ # For example, if we have a group `SELECT * FROM users...`,
+ # with the following suffixes
+ # `WHERE id = 1` (counts: N, 0)
+ # `WHERE id = 2` (counts: 0, N)
+ def reject_groups_with_different_parameters(suffs)
+ return suffs if suffs.size != 2
+
+ counts_a, counts_b = suffs.values
+ return {} if counts_a == counts_b.reverse && counts_a.include?(0)
+
+ suffs
+ end
+
+ def expected_counts
+ @expected.transform_values do |suffixes|
+ suffixes.transform_values { |n| [n, 0] }
+ end
+ end
+
+ def recorded_counts
+ @actual.transform_values do |suffixes|
+ suffixes.transform_values { |n| [0, n] }
+ end
+ end
+
+ def combined_counts
+ expected_counts.merge(recorded_counts) do |_k, exp, got|
+ exp.merge(got) do |_k, exp_counts, got_counts|
+ exp_counts.zip(got_counts).map { |a, b| a + b }
+ end
+ end
+ end
+ end
+
MARGINALIA_ANNOTATION_REGEX = %r{\s*\/\*.*\*\/}.freeze
DB_QUERY_RE = Regexp.union([
@@ -108,26 +170,7 @@ module ExceedQueryLimitHelpers
end
def diff_query_counts(expected, actual)
- expected_counts = expected.transform_values do |suffixes|
- suffixes.transform_values { |n| [n, 0] }
- end
- recorded_counts = actual.transform_values do |suffixes|
- suffixes.transform_values { |n| [0, n] }
- end
-
- combined_counts = expected_counts.merge(recorded_counts) do |_k, exp, got|
- exp.merge(got) do |_k, exp_counts, got_counts|
- exp_counts.zip(got_counts).map { |a, b| a + b }
- end
- end
-
- unless @show_common_queries
- combined_counts = combined_counts.transform_values do |suffs|
- suffs.reject { |_k, counts| counts.first == counts.second }
- end
- end
-
- combined_counts.reject { |_prefix, suffs| suffs.empty? }
+ QueryDiff.new(expected, actual, @show_common_queries).diff
end
def diff_query_group_message(query, suffixes)
@@ -141,7 +184,7 @@ module ExceedQueryLimitHelpers
def log_message
if expected.is_a?(ActiveRecord::QueryRecorder)
diff_counts = diff_query_counts(count_queries(expected), count_queries(@recorder))
- sections = diff_counts.map { |q, suffixes| diff_query_group_message(q, suffixes) }
+ sections = diff_counts.filter_map { |q, suffixes| diff_query_group_message(q, suffixes) }
<<~MSG
Query Diff:
@@ -323,7 +366,12 @@ RSpec::Matchers.define :exceed_query_limit do |expected|
include ExceedQueryLimitHelpers
match do |block|
- verify_count(&block)
+ if block.is_a?(ActiveRecord::QueryRecorder)
+ @recorder = block
+ verify_count
+ else
+ verify_count(&block)
+ end
end
failure_message_when_negated do |actual|
diff --git a/spec/support/shared_contexts/features/integrations/integrations_shared_context.rb b/spec/support/shared_contexts/features/integrations/integrations_shared_context.rb
index 3ea6658c0c1..3d3b8c2207d 100644
--- a/spec/support/shared_contexts/features/integrations/integrations_shared_context.rb
+++ b/spec/support/shared_contexts/features/integrations/integrations_shared_context.rb
@@ -2,14 +2,42 @@
Integration.available_integration_names.each do |integration|
RSpec.shared_context integration do
- include JiraServiceHelper if integration == 'jira'
+ include JiraIntegrationHelpers if integration == 'jira'
let(:dashed_integration) { integration.dasherize }
let(:integration_method) { Project.integration_association_name(integration) }
let(:integration_klass) { Integration.integration_name_to_model(integration) }
let(:integration_instance) { integration_klass.new }
- let(:integration_fields) { integration_instance.fields }
- let(:integration_attrs_list) { integration_fields.inject([]) {|arr, hash| arr << hash[:name].to_sym } }
+
+ # Build a list of all attributes that an integration supports.
+ let(:integration_attrs_list) do
+ integration_fields + integration_events + custom_attributes.fetch(integration.to_sym, [])
+ end
+
+ # Attributes defined as fields.
+ let(:integration_fields) do
+ integration_instance.fields.map { _1[:name].to_sym }
+ end
+
+ # Attributes for configurable event triggers.
+ let(:integration_events) do
+ integration_instance.configurable_events.map { IntegrationsHelper.integration_event_field_name(_1).to_sym }
+ end
+
+ # Other special cases, this list might be incomplete.
+ #
+ # Some of these won't be needed anymore after we've converted them to use the field DSL
+ # in https://gitlab.com/gitlab-org/gitlab/-/issues/354899.
+ #
+ # Others like `comment_on_event_disabled` are actual columns on `integrations`, maybe we should migrate
+ # these to fields as well.
+ let(:custom_attributes) do
+ {
+ jira: %i[comment_on_event_enabled jira_issue_transition_automatic jira_issue_transition_id project_key
+ issues_enabled vulnerabilities_enabled vulnerabilities_issuetype]
+ }
+ end
+
let(:integration_attrs) do
integration_attrs_list.inject({}) do |hash, k|
if k =~ /^(token*|.*_token|.*_key)/
@@ -32,9 +60,11 @@ Integration.available_integration_names.each do |integration|
hash.merge!(k => 1234)
elsif integration == 'jira' && k == :jira_issue_transition_id
hash.merge!(k => '1,2,3')
+ elsif integration == 'jira' && k == :jira_issue_transition_automatic
+ hash.merge!(k => true)
elsif integration == 'emails_on_push' && k == :recipients
hash.merge!(k => 'foo@bar.com')
- elsif integration == 'slack' || integration == 'mattermost' && k == :labels_to_be_notified_behavior
+ elsif (integration == 'slack' || integration == 'mattermost') && k == :labels_to_be_notified_behavior
hash.merge!(k => "match_any")
else
hash.merge!(k => "someword")
@@ -44,7 +74,7 @@ Integration.available_integration_names.each do |integration|
let(:licensed_features) do
{
- 'github' => :github_project_service_integration
+ 'github' => :github_integration
}
end
diff --git a/spec/support/shared_contexts/finders/issues_finder_shared_contexts.rb b/spec/support/shared_contexts/finders/issues_finder_shared_contexts.rb
index d9cbea58406..afb3976e3b8 100644
--- a/spec/support/shared_contexts/finders/issues_finder_shared_contexts.rb
+++ b/spec/support/shared_contexts/finders/issues_finder_shared_contexts.rb
@@ -12,7 +12,7 @@ RSpec.shared_context 'IssuesFinder context' do
let_it_be(:milestone) { create(:milestone, project: project1, releases: [release]) }
let_it_be(:label) { create(:label, project: project2) }
let_it_be(:label2) { create(:label, project: project2) }
- let_it_be(:issue1, reload: true) do
+ let_it_be(:item1, reload: true) do
create(:issue,
author: user,
assignees: [user],
@@ -23,7 +23,7 @@ RSpec.shared_context 'IssuesFinder context' do
updated_at: 1.week.ago)
end
- let_it_be(:issue2, reload: true) do
+ let_it_be(:item2, reload: true) do
create(:issue,
author: user,
assignees: [user],
@@ -33,7 +33,7 @@ RSpec.shared_context 'IssuesFinder context' do
updated_at: 1.week.from_now)
end
- let_it_be(:issue3, reload: true) do
+ let_it_be(:item3, reload: true) do
create(:issue,
author: user2,
assignees: [user2],
@@ -44,8 +44,8 @@ RSpec.shared_context 'IssuesFinder context' do
updated_at: 2.weeks.from_now)
end
- let_it_be(:issue4, reload: true) { create(:issue, project: project3) }
- let_it_be(:issue5, reload: true) do
+ let_it_be(:item4, reload: true) { create(:issue, project: project3) }
+ let_it_be(:item5, reload: true) do
create(:issue,
author: user,
assignees: [user],
@@ -55,18 +55,20 @@ RSpec.shared_context 'IssuesFinder context' do
updated_at: 3.days.ago)
end
- let_it_be(:award_emoji1) { create(:award_emoji, name: 'thumbsup', user: user, awardable: issue1) }
- let_it_be(:award_emoji2) { create(:award_emoji, name: 'thumbsup', user: user2, awardable: issue2) }
- let_it_be(:award_emoji3) { create(:award_emoji, name: 'thumbsdown', user: user, awardable: issue3) }
+ let_it_be(:award_emoji1) { create(:award_emoji, name: 'thumbsup', user: user, awardable: item1) }
+ let_it_be(:award_emoji2) { create(:award_emoji, name: 'thumbsup', user: user2, awardable: item2) }
+ let_it_be(:award_emoji3) { create(:award_emoji, name: 'thumbsdown', user: user, awardable: item3) }
+
+ let(:items_model) { Issue }
end
RSpec.shared_context 'IssuesFinder#execute context' do
- let!(:closed_issue) { create(:issue, author: user2, assignees: [user2], project: project2, state: 'closed') }
- let!(:label_link) { create(:label_link, label: label, target: issue2) }
- let!(:label_link2) { create(:label_link, label: label2, target: issue3) }
+ let!(:closed_item) { create(:issue, author: user2, assignees: [user2], project: project2, state: 'closed') }
+ let!(:label_link) { create(:label_link, label: label, target: item2) }
+ let!(:label_link2) { create(:label_link, label: label2, target: item3) }
let(:search_user) { user }
let(:params) { {} }
- let(:issues) { described_class.new(search_user, params.reverse_merge(scope: scope, state: 'opened')).execute }
+ let(:items) { described_class.new(search_user, params.reverse_merge(scope: scope, state: 'opened')).execute }
before_all do
project1.add_maintainer(user)
diff --git a/spec/support/shared_contexts/finders/work_items_finder_shared_contexts.rb b/spec/support/shared_contexts/finders/work_items_finder_shared_contexts.rb
new file mode 100644
index 00000000000..8c5bc339db5
--- /dev/null
+++ b/spec/support/shared_contexts/finders/work_items_finder_shared_contexts.rb
@@ -0,0 +1,79 @@
+# frozen_string_literal: true
+
+RSpec.shared_context 'WorkItemsFinder context' do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:user2) { create(:user) }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:subgroup) { create(:group, parent: group) }
+ let_it_be(:project1, reload: true) { create(:project, group: group) }
+ let_it_be(:project2, reload: true) { create(:project) }
+ let_it_be(:project3, reload: true) { create(:project, group: subgroup) }
+ let_it_be(:release) { create(:release, project: project1, tag: 'v1.0.0') }
+ let_it_be(:milestone) { create(:milestone, project: project1, releases: [release]) }
+ let_it_be(:label) { create(:label, project: project2) }
+ let_it_be(:label2) { create(:label, project: project2) }
+ let_it_be(:item1, reload: true) do
+ create(:work_item,
+ author: user,
+ assignees: [user],
+ project: project1,
+ milestone: milestone,
+ title: 'gitlab',
+ created_at: 1.week.ago,
+ updated_at: 1.week.ago)
+ end
+
+ let_it_be(:item2, reload: true) do
+ create(:work_item,
+ author: user,
+ assignees: [user],
+ project: project2,
+ description: 'gitlab',
+ created_at: 1.week.from_now,
+ updated_at: 1.week.from_now)
+ end
+
+ let_it_be(:item3, reload: true) do
+ create(:work_item,
+ author: user2,
+ assignees: [user2],
+ project: project2,
+ title: 'tanuki',
+ description: 'tanuki',
+ created_at: 2.weeks.from_now,
+ updated_at: 2.weeks.from_now)
+ end
+
+ let_it_be(:item4, reload: true) { create(:work_item, project: project3) }
+ let_it_be(:item5, reload: true) do
+ create(:work_item,
+ author: user,
+ assignees: [user],
+ project: project1,
+ title: 'wotnot',
+ created_at: 3.days.ago,
+ updated_at: 3.days.ago)
+ end
+
+ let_it_be(:award_emoji1) { create(:award_emoji, name: 'thumbsup', user: user, awardable: item1) }
+ let_it_be(:award_emoji2) { create(:award_emoji, name: 'thumbsup', user: user2, awardable: item2) }
+ let_it_be(:award_emoji3) { create(:award_emoji, name: 'thumbsdown', user: user, awardable: item3) }
+
+ let(:items_model) { WorkItem }
+end
+
+RSpec.shared_context 'WorkItemsFinder#execute context' do
+ let!(:closed_item) { create(:work_item, author: user2, assignees: [user2], project: project2, state: 'closed') }
+ let!(:label_link) { create(:label_link, label: label, target: item2) }
+ let!(:label_link2) { create(:label_link, label: label2, target: item3) }
+ let(:search_user) { user }
+ let(:params) { {} }
+ let(:items) { described_class.new(search_user, params.reverse_merge(scope: scope, state: 'opened')).execute }
+
+ before_all do
+ project1.add_maintainer(user)
+ project2.add_developer(user)
+ project2.add_developer(user2)
+ project3.add_developer(user)
+ end
+end
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 7d51c90522a..aa8bc6fa79f 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
@@ -18,7 +18,10 @@ RSpec.shared_context 'structured_logger' do
"correlation_id" => 'cid',
"error_message" => "wrong number of arguments (2 for 3)",
"error_class" => "ArgumentError",
- "error_backtrace" => []
+ "error_backtrace" => [],
+ "exception.message" => "wrong number of arguments (2 for 3)",
+ "exception.class" => "ArgumentError",
+ "exception.backtrace" => []
}
end
@@ -28,7 +31,10 @@ RSpec.shared_context 'structured_logger' do
let(:clock_thread_cputime_start) { 0.222222299 }
let(:clock_thread_cputime_end) { 1.333333799 }
let(:start_payload) do
- job.except('error_backtrace', 'error_class', 'error_message').merge(
+ job.except(
+ 'error_message', 'error_class', 'error_backtrace',
+ 'exception.backtrace', 'exception.class', 'exception.message'
+ ).merge(
'message' => 'TestWorker JID-da883554ee4fe414012f5f42: start',
'job_status' => 'start',
'pid' => Process.pid,
@@ -58,7 +64,8 @@ RSpec.shared_context 'structured_logger' do
'duration_s' => 0.0,
'completed_at' => timestamp.to_f,
'cpu_s' => 1.111112,
- 'rate_limiting_gates' => []
+ 'rate_limiting_gates' => [],
+ 'worker_id' => "process_#{Process.pid}"
)
end
@@ -68,7 +75,10 @@ RSpec.shared_context 'structured_logger' do
'job_status' => 'fail',
'error_class' => 'ArgumentError',
'error_message' => 'Something went wrong',
- 'error_backtrace' => be_a(Array).and(be_present)
+ 'error_backtrace' => be_a(Array).and(be_present),
+ 'exception.class' => 'ArgumentError',
+ 'exception.message' => 'Something went wrong',
+ 'exception.backtrace' => be_a(Array).and(be_present)
)
end
diff --git a/spec/support/shared_contexts/markdown_snapshot_shared_examples.rb b/spec/support/shared_contexts/markdown_snapshot_shared_examples.rb
new file mode 100644
index 00000000000..de52b58982e
--- /dev/null
+++ b/spec/support/shared_contexts/markdown_snapshot_shared_examples.rb
@@ -0,0 +1,64 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+# See https://docs.gitlab.com/ee/development/gitlab_flavored_markdown/specification_guide/#markdown-snapshot-testing
+# for documentation on this spec.
+# rubocop:disable Layout/LineLength
+RSpec.shared_context 'with API::Markdown Snapshot shared context' do |glfm_specification_dir, glfm_example_snapshots_dir|
+ # rubocop:enable Layout/LineLength
+ include ApiHelpers
+
+ markdown_examples, html_examples = %w[markdown.yml html.yml].map do |file_name|
+ yaml = File.read("#{glfm_example_snapshots_dir}/#{file_name}")
+ YAML.safe_load(yaml, symbolize_names: true, aliases: true)
+ end
+
+ normalizations_yaml = File.read(
+ "#{glfm_specification_dir}/input/gitlab_flavored_markdown/glfm_example_normalizations.yml")
+ normalizations_by_example_name = YAML.safe_load(normalizations_yaml, symbolize_names: true, aliases: true)
+
+ if (focused_markdown_examples_string = ENV['FOCUSED_MARKDOWN_EXAMPLES'])
+ focused_markdown_examples = focused_markdown_examples_string.split(',').map(&:strip).map(&:to_sym)
+ markdown_examples.select! { |example_name| focused_markdown_examples.include?(example_name) }
+ end
+
+ markdown_examples.each do |name, markdown|
+ context "for #{name}" do
+ let(:html) { html_examples.fetch(name).fetch(:static) }
+ let(:normalizations) { normalizations_by_example_name.dig(name, :html, :static, :snapshot) }
+
+ it "verifies conversion of GLFM to HTML", :unlimited_max_formatted_output_length do
+ api_url = api "/markdown"
+
+ # noinspection RubyResolve
+ normalized_html = normalize_html(html, normalizations)
+
+ post api_url, params: { text: markdown, gfm: true }
+ expect(response).to be_successful
+ response_body = Gitlab::Json.parse(response.body)
+ # Some requests have the HTML in the `html` key, others in the `body` key.
+ response_html = response_body['body'] ? response_body.fetch('body') : response_body.fetch('html')
+ # noinspection RubyResolve
+ normalized_response_html = normalize_html(response_html, normalizations)
+
+ expect(normalized_response_html).to eq(normalized_html)
+ end
+
+ def normalize_html(html, normalizations)
+ return html unless normalizations
+
+ normalized_html = html.dup
+ normalizations.each_value do |normalization_entry|
+ normalization_entry.each do |normalization|
+ regex = normalization.fetch(:regex)
+ replacement = normalization.fetch(:replacement)
+ normalized_html.gsub!(%r{#{regex}}, replacement)
+ end
+ end
+
+ normalized_html
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_contexts/navbar_structure_context.rb b/spec/support/shared_contexts/navbar_structure_context.rb
index ef6ff7be840..d277a45584d 100644
--- a/spec/support/shared_contexts/navbar_structure_context.rb
+++ b/spec/support/shared_contexts/navbar_structure_context.rb
@@ -134,13 +134,13 @@ RSpec.shared_context 'group navbar structure' do
nav_sub_items: [
_('General'),
_('Integrations'),
+ _('Webhooks'),
_('Access Tokens'),
_('Projects'),
_('Repository'),
_('CI/CD'),
_('Applications'),
- _('Packages & Registries'),
- _('Webhooks')
+ _('Packages & Registries')
]
}
end
diff --git a/spec/support/shared_contexts/policies/project_policy_shared_context.rb b/spec/support/shared_contexts/policies/project_policy_shared_context.rb
index e50083a10e7..7396643823c 100644
--- a/spec/support/shared_contexts/policies/project_policy_shared_context.rb
+++ b/spec/support/shared_contexts/policies/project_policy_shared_context.rb
@@ -75,7 +75,7 @@ RSpec.shared_context 'ProjectPolicy context' do
let(:base_owner_permissions) do
%i[
archive_project change_namespace change_visibility_level destroy_issue
- destroy_merge_request remove_fork_project remove_project rename_project
+ destroy_merge_request manage_owners remove_fork_project remove_project rename_project
set_issue_created_at set_issue_iid set_issue_updated_at
set_note_created_at
]
diff --git a/spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb b/spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb
index 15590fd10dc..0e6f6f12c3f 100644
--- a/spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb
+++ b/spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb
@@ -26,7 +26,7 @@ RSpec.shared_examples 'multiple issue boards' do
it 'switches current board' do
in_boards_switcher_dropdown do
- click_link board2.name
+ click_button board2.name
end
wait_for_requests
@@ -66,7 +66,7 @@ RSpec.shared_examples 'multiple issue boards' do
it 'adds a list to the none default board' do
in_boards_switcher_dropdown do
- click_link board2.name
+ click_button board2.name
end
wait_for_requests
@@ -88,7 +88,7 @@ RSpec.shared_examples 'multiple issue boards' do
expect(page).to have_selector('.board', count: 3)
in_boards_switcher_dropdown do
- click_link board.name
+ click_button board.name
end
wait_for_requests
@@ -100,7 +100,7 @@ RSpec.shared_examples 'multiple issue boards' do
assert_boards_nav_active
in_boards_switcher_dropdown do
- click_link board2.name
+ click_button board2.name
end
assert_boards_nav_active
@@ -108,7 +108,7 @@ RSpec.shared_examples 'multiple issue boards' do
it 'switches current board back' do
in_boards_switcher_dropdown do
- click_link board.name
+ click_button board.name
end
wait_for_requests
diff --git a/spec/support/shared_examples/components/pajamas_shared_examples.rb b/spec/support/shared_examples/components/pajamas_shared_examples.rb
new file mode 100644
index 00000000000..5c0ad1a1bc9
--- /dev/null
+++ b/spec/support/shared_examples/components/pajamas_shared_examples.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'it renders help text' do
+ it 'renders help text' do
+ expect(rendered_component).to have_selector('[data-testid="pajamas-component-help-text"]', text: help_text)
+ end
+end
+
+RSpec.shared_examples 'it does not render help text' do
+ it 'does not render help text' do
+ expect(rendered_component).not_to have_selector('[data-testid="pajamas-component-help-text"]')
+ 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 a79b94209f3..c6e880635aa 100644
--- a/spec/support/shared_examples/controllers/environments_controller_shared_examples.rb
+++ b/spec/support/shared_examples/controllers/environments_controller_shared_examples.rb
@@ -65,20 +65,3 @@ 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/githubish_import_controller_shared_examples.rb b/spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb
index 2ea98002de1..5faf462c23c 100644
--- a/spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb
+++ b/spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb
@@ -36,6 +36,19 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST personal_access_toke
expect(session[:"#{provider}_access_token"]).to eq(token)
expect(controller).to redirect_to(status_import_url)
end
+
+ it 'passes namespace_id param as query param if it was present' do
+ namespace_id = 5
+ status_import_url = public_send("status_import_#{provider}_url", { namespace_id: namespace_id })
+
+ allow_next_instance_of(Gitlab::LegacyGithubImport::Client) do |client|
+ allow(client).to receive(:user).and_return(true)
+ end
+
+ post :personal_access_token, params: { personal_access_token: 'some-token', namespace_id: 5 }
+
+ expect(controller).to redirect_to(status_import_url)
+ end
end
RSpec.shared_examples 'a GitHub-ish import controller: GET new' do
diff --git a/spec/support/shared_examples/features/2fa_shared_examples.rb b/spec/support/shared_examples/features/2fa_shared_examples.rb
index 94c91556ea7..44f30c32472 100644
--- a/spec/support/shared_examples/features/2fa_shared_examples.rb
+++ b/spec/support/shared_examples/features/2fa_shared_examples.rb
@@ -2,6 +2,7 @@
RSpec.shared_examples 'hardware device for 2fa' do |device_type|
include Spec::Support::Helpers::Features::TwoFactorHelpers
+ include Spec::Support::Helpers::ModalHelpers
def register_device(device_type, **kwargs)
case device_type.downcase
@@ -18,7 +19,6 @@ RSpec.shared_examples 'hardware device for 2fa' do |device_type|
let(:user) { create(:user) }
before do
- stub_feature_flags(bootstrap_confirmation_modals: false)
gitlab_sign_in(user)
user.update_attribute(:otp_required_for_login, true)
end
@@ -59,7 +59,7 @@ RSpec.shared_examples 'hardware device for 2fa' do |device_type|
expect(page).to have_content(first_device.name)
expect(page).to have_content(second_device.name)
- accept_confirm { click_on 'Delete', match: :first }
+ accept_gl_confirm(button_text: 'Delete') { click_on 'Delete', match: :first }
expect(page).to have_content('Successfully deleted')
expect(page.body).not_to have_content(first_device.name)
diff --git a/spec/support/shared_examples/features/access_tokens_shared_examples.rb b/spec/support/shared_examples/features/access_tokens_shared_examples.rb
index 215d9d3e5a8..c162ed36881 100644
--- a/spec/support/shared_examples/features/access_tokens_shared_examples.rb
+++ b/spec/support/shared_examples/features/access_tokens_shared_examples.rb
@@ -51,7 +51,7 @@ RSpec.shared_examples 'resource access tokens creation disallowed' do |error_mes
it 'does not show access token creation form' do
visit resource_settings_access_tokens_path
- expect(page).not_to have_selector('#new_resource_access_token')
+ expect(page).not_to have_selector('#js-new-access-token-form')
end
it 'shows access token creation disabled text' do
@@ -135,7 +135,7 @@ RSpec.shared_examples 'inactive resource access tokens' do |no_active_tokens_tex
it 'allows revocation of an active token' do
visit resource_settings_access_tokens_path
- accept_confirm { click_on 'Revoke' }
+ accept_gl_confirm(button_text: 'Revoke') { click_on 'Revoke' }
expect(page).to have_selector('.settings-message')
expect(no_resource_access_tokens_message).to have_text(no_active_tokens_text)
@@ -156,7 +156,7 @@ RSpec.shared_examples 'inactive resource access tokens' do |no_active_tokens_tex
it 'allows revocation of an active token' do
visit resource_settings_access_tokens_path
- accept_confirm { click_on 'Revoke' }
+ accept_gl_confirm(button_text: 'Revoke') { click_on 'Revoke' }
expect(page).to have_selector('.settings-message')
expect(no_resource_access_tokens_message).to have_text(no_active_tokens_text)
diff --git a/spec/support/shared_examples/features/cascading_settings_shared_examples.rb b/spec/support/shared_examples/features/cascading_settings_shared_examples.rb
index 395f4fc54e0..cb80751ff49 100644
--- a/spec/support/shared_examples/features/cascading_settings_shared_examples.rb
+++ b/spec/support/shared_examples/features/cascading_settings_shared_examples.rb
@@ -6,7 +6,8 @@ RSpec.shared_examples 'a cascading setting' do
visit group_path
page.within form_group_selector do
- find(setting_field_selector).check
+ enable_setting.call
+
find('[data-testid="enforce-for-all-subgroups-checkbox"]').check
end
diff --git a/spec/support/shared_examples/features/container_registry_shared_examples.rb b/spec/support/shared_examples/features/container_registry_shared_examples.rb
index 6aa7e6e6270..784f82fdda1 100644
--- a/spec/support/shared_examples/features/container_registry_shared_examples.rb
+++ b/spec/support/shared_examples/features/container_registry_shared_examples.rb
@@ -19,8 +19,7 @@ RSpec.shared_examples 'rejecting tags destruction for an importing repository on
expect(find('.modal .modal-title')).to have_content _('Remove tag')
find('.modal .modal-footer .btn-danger').click
- alert_body = find('.gl-alert-body')
- expect(alert_body).to have_content('Tags temporarily cannot be marked for deletion. Please try again in a few minutes.')
- expect(alert_body).to have_link('More details', href: help_page_path('user/packages/container_registry/index', anchor: 'tags-temporarily-cannot-be-marked-for-deletion'))
+ expect(page).to have_content('Tags temporarily cannot be marked for deletion. Please try again in a few minutes.')
+ expect(page).to have_link('More details', href: help_page_path('user/packages/container_registry/index', anchor: 'tags-temporarily-cannot-be-marked-for-deletion'))
end
end
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 c93d8e3d511..591f7973454 100644
--- a/spec/support/shared_examples/features/content_editor_shared_examples.rb
+++ b/spec/support/shared_examples/features/content_editor_shared_examples.rb
@@ -21,6 +21,31 @@ RSpec.shared_examples 'edits content using the content editor' do
end
end
+ describe 'code block' do
+ before do
+ visit(profile_preferences_path)
+
+ find('.syntax-theme').choose('Dark')
+
+ wait_for_requests
+
+ page.go_back
+ refresh
+
+ click_button 'Edit rich text'
+ end
+
+ it 'applies theme classes to code blocks' do
+ expect(page).not_to have_css('.content-editor-code-block.code.highlight.dark')
+
+ 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'
+
+ expect(page).to have_css('.content-editor-code-block.code.highlight.dark')
+ end
+ end
+
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]
@@ -51,4 +76,49 @@ RSpec.shared_examples 'edits content using the content editor' do
expect(find('[data-testid="code-block-bubble-menu"]')).to have_text('Custom (nomnoml)')
end
end
+
+ describe 'mermaid diagram' do
+ before do
+ find(content_editor_testid).send_keys [:enter, :enter]
+
+ find(content_editor_testid).send_keys '```mermaid '
+ find(content_editor_testid).send_keys ['graph TD;', :enter, ' JohnDoe12 --> HelloWorld34']
+ end
+
+ it 'renders and updates the diagram correctly in a sandboxed iframe' do
+ iframe = find(content_editor_testid).find('iframe')
+ expect(iframe['src']).to include('/-/sandbox/mermaid')
+
+ within_frame(iframe) do
+ expect(find('svg').text).to include('JohnDoe12')
+ expect(find('svg').text).to include('HelloWorld34')
+ end
+
+ expect(iframe['height'].to_i).to be > 100
+
+ find(content_editor_testid).send_keys [:enter, ' JaneDoe34 --> HelloWorld56']
+
+ within_frame(iframe) do
+ page.has_content?('JaneDoe34')
+
+ expect(find('svg').text).to include('JaneDoe34')
+ expect(find('svg').text).to include('HelloWorld56')
+ end
+ end
+
+ it 'toggles the diagram when preview button is clicked' do
+ find('[data-testid="preview-diagram"]').click
+
+ expect(find(content_editor_testid)).not_to have_selector('iframe')
+
+ find('[data-testid="preview-diagram"]').click
+
+ iframe = find(content_editor_testid).find('iframe')
+
+ within_frame(iframe) do
+ expect(find('svg').text).to include('JohnDoe12')
+ expect(find('svg').text).to include('HelloWorld34')
+ end
+ end
+ end
end
diff --git a/spec/support/shared_examples/features/issuables_user_dropdown_behaviors_shared_examples.rb b/spec/support/shared_examples/features/issuables_user_dropdown_behaviors_shared_examples.rb
deleted file mode 100644
index 1848b4fffd9..00000000000
--- a/spec/support/shared_examples/features/issuables_user_dropdown_behaviors_shared_examples.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-# frozen_string_literal: true
-
-RSpec.shared_examples 'issuable user dropdown behaviors' do
- include FilteredSearchHelpers
-
- before do
- issuable # ensure we have at least one issuable
- sign_in(user_in_dropdown)
- end
-
- %w[author assignee].each do |dropdown|
- describe "#{dropdown} dropdown", :js do
- it 'only includes members of the project/group' do
- visit issuables_path
-
- filtered_search.set("#{dropdown}:=")
-
- expect(find("#js-dropdown-#{dropdown} .filter-dropdown")).to have_content(user_in_dropdown.name)
- expect(find("#js-dropdown-#{dropdown} .filter-dropdown")).not_to have_content(user_not_in_dropdown.name)
- end
- end
- end
-end
diff --git a/spec/support/shared_examples/features/runners_shared_examples.rb b/spec/support/shared_examples/features/runners_shared_examples.rb
index d9460c7b8f1..52f3fd60c07 100644
--- a/spec/support/shared_examples/features/runners_shared_examples.rb
+++ b/spec/support/shared_examples/features/runners_shared_examples.rb
@@ -35,11 +35,11 @@ RSpec.shared_examples 'shows and resets runner registration token' do
it 'has a registration token' do
click_on 'Click to reveal'
- expect(page.find('[data-testid="token-value"] input').value).to have_content(registration_token)
+ expect(page.find_field('token-value').value).to have_content(registration_token)
end
describe 'reset registration token' do
- let!(:old_registration_token) { find('[data-testid="token-value"] input').value }
+ let!(:old_registration_token) { find_field('token-value').value }
before do
click_on 'Reset registration token'
@@ -62,7 +62,7 @@ RSpec.shared_examples 'shows and resets runner registration token' do
end
end
-RSpec.shared_examples 'shows no runners' do
+RSpec.shared_examples 'shows no runners registered' do
it 'shows counts with 0' do
expect(page).to have_text "Online runners 0"
expect(page).to have_text "Offline runners 0"
@@ -70,13 +70,19 @@ RSpec.shared_examples 'shows no runners' do
end
it 'shows "no runners" message' do
- expect(page).to have_text 'No runners found'
+ expect(page).to have_text s_('Runners|Get started with runners')
+ end
+end
+
+RSpec.shared_examples 'shows no runners found' do
+ it 'shows "no runners" message' do
+ expect(page).to have_text s_('Runners|No results found')
end
end
RSpec.shared_examples 'shows runner in list' do
it 'does not show empty state' do
- expect(page).not_to have_content 'No runners found'
+ expect(page).not_to have_content s_('Runners|Get started with runners')
end
it 'shows runner row' do
diff --git a/spec/support/shared_examples/features/sidebar_shared_examples.rb b/spec/support/shared_examples/features/sidebar_shared_examples.rb
index af3ea0600a2..77334db6a36 100644
--- a/spec/support/shared_examples/features/sidebar_shared_examples.rb
+++ b/spec/support/shared_examples/features/sidebar_shared_examples.rb
@@ -109,9 +109,8 @@ RSpec.shared_examples 'issue boards sidebar' do
wait_for_requests
expect(page).to have_content(
- _('Only project members with at least' \
- ' Reporter role can view or be' \
- ' notified about this issue.')
+ _('Only project members with at least the Reporter role, the author, and assignees' \
+ ' can view or be notified about this issue.')
)
end
end
diff --git a/spec/support/shared_examples/finders/issues_finder_shared_examples.rb b/spec/support/shared_examples/finders/issues_finder_shared_examples.rb
new file mode 100644
index 00000000000..622a88e8323
--- /dev/null
+++ b/spec/support/shared_examples/finders/issues_finder_shared_examples.rb
@@ -0,0 +1,1471 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.shared_examples 'issues or work items finder' do |factory, execute_context|
+ describe '#execute' do
+ include_context execute_context
+
+ context 'scope: all' do
+ let(:scope) { 'all' }
+
+ it 'returns all items' do
+ expect(items).to contain_exactly(item1, item2, item3, item4, item5)
+ end
+
+ context 'user does not have read permissions' do
+ let(:search_user) { user2 }
+
+ context 'when filtering by project id' do
+ let(:params) { { project_id: project1.id } }
+
+ it 'returns no items' do
+ expect(items).to be_empty
+ end
+ end
+
+ context 'when filtering by group id' do
+ let(:params) { { group_id: group.id } }
+
+ it 'returns no items' do
+ expect(items).to be_empty
+ end
+ end
+ end
+
+ context 'assignee filtering' do
+ let(:issuables) { items }
+
+ it_behaves_like 'assignee ID filter' do
+ let(:params) { { assignee_id: user.id } }
+ let(:expected_issuables) { [item1, item2, item5] }
+ end
+
+ it_behaves_like 'assignee NOT ID filter' do
+ let(:params) { { not: { assignee_id: user.id } } }
+ let(:expected_issuables) { [item3, item4] }
+ end
+
+ it_behaves_like 'assignee OR filter' do
+ let(:params) { { or: { assignee_id: [user.id, user2.id] } } }
+ let(:expected_issuables) { [item1, item2, item3, item5] }
+ end
+
+ context 'when assignee_id does not exist' do
+ it_behaves_like 'assignee NOT ID filter' do
+ let(:params) { { not: { assignee_id: -100 } } }
+ let(:expected_issuables) { [item1, item2, item3, item4, item5] }
+ end
+ end
+
+ context 'filter by username' do
+ let_it_be(:user3) { create(:user) }
+
+ before do
+ project2.add_developer(user3)
+ item2.assignees = [user2]
+ item3.assignees = [user3]
+ end
+
+ it_behaves_like 'assignee username filter' do
+ let(:params) { { assignee_username: [user2.username] } }
+ let(:expected_issuables) { [item2] }
+ end
+
+ it_behaves_like 'assignee NOT username filter' do
+ before do
+ item2.assignees = [user2]
+ end
+
+ let(:params) { { not: { assignee_username: [user.username, user2.username] } } }
+ let(:expected_issuables) { [item3, item4] }
+ end
+
+ it_behaves_like 'assignee OR filter' do
+ let(:params) { { or: { assignee_username: [user2.username, user3.username] } } }
+ let(:expected_issuables) { [item2, item3] }
+ end
+
+ context 'when assignee_username does not exist' do
+ it_behaves_like 'assignee NOT username filter' do
+ before do
+ item2.assignees = [user2]
+ end
+
+ let(:params) { { not: { assignee_username: 'non_existent_username' } } }
+ let(:expected_issuables) { [item1, item2, item3, item4, item5] }
+ end
+ end
+ end
+
+ it_behaves_like 'no assignee filter' do
+ let_it_be(:user3) { create(:user) }
+ let(:expected_issuables) { [item4] }
+ end
+
+ it_behaves_like 'any assignee filter' do
+ let(:expected_issuables) { [item1, item2, item3, item5] }
+ end
+ end
+
+ context 'filtering by release' do
+ context 'when the release tag is none' do
+ let(:params) { { release_tag: 'none' } }
+
+ it 'returns items without releases' do
+ expect(items).to contain_exactly(item2, item3, item4, item5)
+ end
+ end
+
+ context 'when the release tag exists' do
+ let(:params) { { project_id: project1.id, release_tag: release.tag } }
+
+ it 'returns the items associated with that release' do
+ expect(items).to contain_exactly(item1)
+ end
+ end
+ end
+
+ context 'filtering by projects' do
+ context 'when projects are passed in a list of ids' do
+ let(:params) { { projects: [project1.id] } }
+
+ it 'returns the item belonging to the projects' do
+ expect(items).to contain_exactly(item1, item5)
+ end
+ end
+
+ context 'when projects are passed in a subquery' do
+ let(:params) { { projects: Project.id_in(project1.id) } }
+
+ it 'returns the item belonging to the projects' do
+ expect(items).to contain_exactly(item1, item5)
+ end
+ end
+ end
+
+ context 'filtering by group_id' do
+ let(:params) { { group_id: group.id } }
+
+ context 'when include_subgroup param not set' do
+ it 'returns all group items' do
+ expect(items).to contain_exactly(item1, item5)
+ end
+
+ context 'when projects outside the group are passed' do
+ let(:params) { { group_id: group.id, projects: [project2.id] } }
+
+ it 'returns no items' do
+ expect(items).to be_empty
+ end
+ end
+
+ context 'when projects of the group are passed' do
+ let(:params) { { group_id: group.id, projects: [project1.id] } }
+
+ it 'returns the item within the group and projects' do
+ expect(items).to contain_exactly(item1, item5)
+ end
+ end
+
+ context 'when projects of the group are passed as a subquery' do
+ let(:params) { { group_id: group.id, projects: Project.id_in(project1.id) } }
+
+ it 'returns the item within the group and projects' do
+ expect(items).to contain_exactly(item1, item5)
+ end
+ end
+
+ context 'when release_tag is passed as a parameter' do
+ let(:params) { { group_id: group.id, release_tag: 'dne-release-tag' } }
+
+ it 'ignores the release_tag parameter' do
+ expect(items).to contain_exactly(item1, item5)
+ end
+ end
+ end
+
+ context 'when include_subgroup param is true' do
+ before do
+ params[:include_subgroups] = true
+ end
+
+ it 'returns all group and subgroup items' do
+ expect(items).to contain_exactly(item1, item4, item5)
+ end
+
+ context 'when mixed projects are passed' do
+ let(:params) { { group_id: group.id, projects: [project2.id, project3.id] } }
+
+ it 'returns the item within the group and projects' do
+ expect(items).to contain_exactly(item4)
+ end
+ end
+ end
+ end
+
+ context 'filtering by author' do
+ context 'by author ID' do
+ let(:params) { { author_id: user2.id } }
+
+ it 'returns items created by that user' do
+ expect(items).to contain_exactly(item3)
+ end
+ end
+
+ context 'using OR' do
+ let(:item6) { create(factory, project: project2) }
+ let(:params) { { or: { author_username: [item3.author.username, item6.author.username] } } }
+
+ it 'returns items created by any of the given users' do
+ expect(items).to contain_exactly(item3, item6)
+ end
+
+ context 'when feature flag is disabled' do
+ before do
+ stub_feature_flags(or_issuable_queries: false)
+ end
+
+ it 'does not add any filter' do
+ expect(items).to contain_exactly(item1, item2, item3, item4, item5, item6)
+ end
+ end
+ end
+
+ context 'filtering by NOT author ID' do
+ let(:params) { { not: { author_id: user2.id } } }
+
+ it 'returns items not created by that user' do
+ expect(items).to contain_exactly(item1, item2, item4, item5)
+ end
+ end
+
+ context 'filtering by nonexistent author ID and issue term using CTE for search' do
+ let(:params) do
+ {
+ author_id: 'does-not-exist',
+ search: 'git',
+ attempt_group_search_optimizations: true
+ }
+ end
+
+ it 'returns no results' do
+ expect(items).to be_empty
+ end
+ end
+ end
+
+ context 'filtering by milestone' do
+ let(:params) { { milestone_title: milestone.title } }
+
+ it 'returns items assigned to that milestone' do
+ expect(items).to contain_exactly(item1)
+ end
+ end
+
+ context 'filtering by not milestone' do
+ let(:params) { { not: { milestone_title: milestone.title } } }
+
+ it 'returns items not assigned to that milestone' do
+ expect(items).to contain_exactly(item2, item3, item4, item5)
+ end
+ end
+
+ context 'filtering by group milestone' do
+ let!(:group) { create(:group, :public) }
+ let(:group_milestone) { create(:milestone, group: group) }
+ let!(:group_member) { create(:group_member, group: group, user: user) }
+ let(:params) { { milestone_title: group_milestone.title } }
+
+ before do
+ project2.update!(namespace: group)
+ item2.update!(milestone: group_milestone)
+ item3.update!(milestone: group_milestone)
+ end
+
+ it 'returns items assigned to that group milestone' do
+ expect(items).to contain_exactly(item2, item3)
+ end
+
+ context 'using NOT' do
+ let(:params) { { not: { milestone_title: group_milestone.title } } }
+
+ it 'returns items not assigned to that group milestone' do
+ expect(items).to contain_exactly(item1, item4, item5)
+ end
+ end
+ end
+
+ context 'filtering by no milestone' do
+ let(:params) { { milestone_title: 'None' } }
+
+ it 'returns items with no milestone' do
+ expect(items).to contain_exactly(item2, item3, item4, item5)
+ end
+
+ it 'returns items with no milestone (deprecated)' do
+ params[:milestone_title] = Milestone::None.title
+
+ expect(items).to contain_exactly(item2, item3, item4, item5)
+ end
+ end
+
+ context 'filtering by any milestone' do
+ let(:params) { { milestone_title: 'Any' } }
+
+ it 'returns items with any assigned milestone' do
+ expect(items).to contain_exactly(item1)
+ end
+
+ it 'returns items with any assigned milestone (deprecated)' do
+ params[:milestone_title] = Milestone::Any.title
+
+ expect(items).to contain_exactly(item1)
+ end
+ end
+
+ context 'filtering by upcoming milestone' do
+ let(:params) { { milestone_title: Milestone::Upcoming.name } }
+
+ let!(:group) { create(:group, :public) }
+ let!(:group_member) { create(:group_member, group: group, user: user) }
+
+ let(:project_no_upcoming_milestones) { create(:project, :public) }
+ let(:project_next_1_1) { create(:project, :public) }
+ let(:project_next_8_8) { create(:project, :public) }
+ let(:project_in_group) { create(:project, :public, namespace: group) }
+
+ let(:yesterday) { Date.current - 1.day }
+ let(:tomorrow) { Date.current + 1.day }
+ let(:two_days_from_now) { Date.current + 2.days }
+ let(:ten_days_from_now) { Date.current + 10.days }
+
+ let(:milestones) do
+ [
+ create(:milestone, :closed, project: project_no_upcoming_milestones),
+ create(:milestone, project: project_next_1_1, title: '1.1', due_date: two_days_from_now),
+ create(:milestone, project: project_next_1_1, title: '8.9', due_date: ten_days_from_now),
+ create(:milestone, project: project_next_8_8, title: '1.2', due_date: yesterday),
+ create(:milestone, project: project_next_8_8, title: '8.8', due_date: tomorrow),
+ create(:milestone, group: group, title: '9.9', due_date: tomorrow)
+ ]
+ end
+
+ let!(:created_items) do
+ milestones.map do |milestone|
+ create(factory, project: milestone.project || project_in_group,
+ milestone: milestone, author: user, assignees: [user])
+ end
+ end
+
+ it 'returns items in the upcoming milestone for each project or group' do
+ expect(items.map { |item| item.milestone.title })
+ .to contain_exactly('1.1', '8.8', '9.9')
+ expect(items.map { |item| item.milestone.due_date })
+ .to contain_exactly(tomorrow, two_days_from_now, tomorrow)
+ end
+
+ context 'using NOT' do
+ let(:params) { { not: { milestone_title: Milestone::Upcoming.name } } }
+
+ it 'returns items not in upcoming milestones for each project or group, but must have a due date' do
+ target_items = created_items.select do |item|
+ item.milestone&.due_date && item.milestone.due_date <= Date.current
+ end
+
+ expect(items).to contain_exactly(*target_items)
+ end
+ end
+ end
+
+ context 'filtering by started milestone' do
+ let(:params) { { milestone_title: Milestone::Started.name } }
+
+ let(:project_no_started_milestones) { create(:project, :public) }
+ let(:project_started_1_and_2) { create(:project, :public) }
+ let(:project_started_8) { create(:project, :public) }
+
+ let(:yesterday) { Date.current - 1.day }
+ let(:tomorrow) { Date.current + 1.day }
+ let(:two_days_ago) { Date.current - 2.days }
+ let(:three_days_ago) { Date.current - 3.days }
+
+ let(:milestones) do
+ [
+ create(:milestone, project: project_no_started_milestones, start_date: tomorrow),
+ create(:milestone, project: project_started_1_and_2, title: '1.0', start_date: two_days_ago),
+ create(:milestone, project: project_started_1_and_2, title: '2.0', start_date: yesterday),
+ create(:milestone, project: project_started_1_and_2, title: '3.0', start_date: tomorrow),
+ create(:milestone, :closed, project: project_started_1_and_2, title: '4.0', start_date: three_days_ago),
+ create(:milestone, :closed, project: project_started_8, title: '6.0', start_date: three_days_ago),
+ create(:milestone, project: project_started_8, title: '7.0'),
+ create(:milestone, project: project_started_8, title: '8.0', start_date: yesterday),
+ create(:milestone, project: project_started_8, title: '9.0', start_date: tomorrow)
+ ]
+ end
+
+ before do
+ milestones.each do |milestone|
+ create(factory, project: milestone.project, milestone: milestone, author: user, assignees: [user])
+ end
+ end
+
+ it 'returns items in the started milestones for each project' do
+ expect(items.map { |item| item.milestone.title })
+ .to contain_exactly('1.0', '2.0', '8.0')
+ expect(items.map { |item| item.milestone.start_date })
+ .to contain_exactly(two_days_ago, yesterday, yesterday)
+ end
+
+ context 'using NOT' do
+ let(:params) { { not: { milestone_title: Milestone::Started.name } } }
+
+ it 'returns items not in the started milestones for each project' do
+ target_items = items_model.where(milestone: Milestone.not_started)
+
+ expect(items).to contain_exactly(*target_items)
+ end
+ end
+ end
+
+ context 'filtering by label' do
+ let(:params) { { label_name: label.title } }
+
+ it 'returns items with that label' do
+ expect(items).to contain_exactly(item2)
+ end
+
+ context 'using NOT' do
+ let(:params) { { not: { label_name: label.title } } }
+
+ it 'returns items that do not have that label' do
+ expect(items).to contain_exactly(item1, item3, item4, item5)
+ end
+
+ # IssuableFinder first filters using the outer params (the ones not inside the `not` key.)
+ # Afterwards, it applies the `not` params to that resultset. This means that things inside the `not` param
+ # do not take precedence over the outer params with the same name.
+ context 'shadowing the same outside param' do
+ let(:params) { { label_name: label2.title, not: { label_name: label.title } } }
+
+ it 'does not take precedence over labels outside NOT' do
+ expect(items).to contain_exactly(item3)
+ end
+ end
+
+ context 'further filtering outside params' do
+ let(:params) { { label_name: label2.title, not: { assignee_username: user2.username } } }
+
+ it 'further filters on the returned resultset' do
+ expect(items).to be_empty
+ end
+ end
+ end
+ end
+
+ context 'filtering by multiple labels' do
+ let(:params) { { label_name: [label.title, label2.title].join(',') } }
+ let(:label2) { create(:label, project: project2) }
+
+ before do
+ create(:label_link, label: label2, target: item2)
+ end
+
+ it 'returns the unique items with all those labels' do
+ expect(items).to contain_exactly(item2)
+ end
+
+ context 'using NOT' do
+ let(:params) { { not: { label_name: [label.title, label2.title].join(',') } } }
+
+ it 'returns items that do not have any of the labels provided' do
+ expect(items).to contain_exactly(item1, item4, item5)
+ end
+ end
+ end
+
+ context 'filtering by a label that includes any or none in the title' do
+ let(:params) { { label_name: [label.title, label2.title].join(',') } }
+ let(:label) { create(:label, title: 'any foo', project: project2) }
+ let(:label2) { create(:label, title: 'bar none', project: project2) }
+
+ before do
+ create(:label_link, label: label2, target: item2)
+ end
+
+ it 'returns the unique items with all those labels' do
+ expect(items).to contain_exactly(item2)
+ end
+
+ context 'using NOT' do
+ let(:params) { { not: { label_name: [label.title, label2.title].join(',') } } }
+
+ it 'returns items that do not have ANY ONE of the labels provided' do
+ expect(items).to contain_exactly(item1, item4, item5)
+ end
+ end
+ end
+
+ context 'filtering by no label' do
+ let(:params) { { label_name: described_class::Params::FILTER_NONE } }
+
+ it 'returns items with no labels' do
+ expect(items).to contain_exactly(item1, item4, item5)
+ end
+ end
+
+ context 'filtering by any label' do
+ let(:params) { { label_name: described_class::Params::FILTER_ANY } }
+
+ it 'returns items that have one or more label' do
+ create_list(:label_link, 2, label: create(:label, project: project2), target: item3)
+
+ expect(items).to contain_exactly(item2, item3)
+ end
+ end
+
+ context 'when the same label exists on project and group levels' do
+ let(:item1) { create(factory, project: project1) }
+ let(:item2) { create(factory, project: project1) }
+
+ # Skipping validation to reproduce a "real-word" scenario.
+ # We still have legacy labels on PRD that have the same title on the group and project levels, example: `bug`
+ let(:project_label) do
+ build(:label, title: 'somelabel', project: project1).tap { |r| r.save!(validate: false) }
+ end
+
+ let(:group_label) { create(:group_label, title: 'somelabel', group: project1.group) }
+
+ let(:params) { { label_name: 'somelabel' } }
+
+ before do
+ create(:label_link, label: group_label, target: item1)
+ create(:label_link, label: project_label, target: item2)
+ end
+
+ it 'finds both item records' do
+ expect(items).to contain_exactly(item1, item2)
+ end
+ end
+
+ context 'filtering by item term' do
+ let(:params) { { search: search_term } }
+
+ let_it_be(:english) { create(factory, project: project1, title: 'title', description: 'something english') }
+
+ let_it_be(:japanese) do
+ create(factory, project: project1, title: '日本語 title', description: 'another english description')
+ end
+
+ context 'with latin search term' do
+ let(:search_term) { 'title english' }
+
+ it 'returns matching items' do
+ expect(items).to contain_exactly(english, japanese)
+ end
+ end
+
+ context 'with non-latin search term' do
+ let(:search_term) { '日本語' }
+
+ it 'returns matching items' do
+ expect(items).to contain_exactly(japanese)
+ end
+ end
+
+ context 'when full-text search is disabled' do
+ let(:search_term) { 'somet' }
+
+ before do
+ stub_feature_flags(issues_full_text_search: false)
+ end
+
+ it 'allows partial word matches' do
+ expect(items).to contain_exactly(english)
+ end
+ end
+
+ context 'with anonymous user' do
+ let_it_be(:public_project) { create(:project, :public, group: subgroup) }
+ let_it_be(:item6) { create(factory, project: public_project, title: 'tanuki') }
+ let_it_be(:item7) { create(factory, project: public_project, title: 'ikunat') }
+
+ let(:search_user) { nil }
+ let(:params) { { search: 'tanuki' } }
+
+ context 'with disable_anonymous_search feature flag enabled' do
+ before do
+ stub_feature_flags(disable_anonymous_search: true)
+ end
+
+ it 'does not perform search' do
+ expect(items).to contain_exactly(item6, item7)
+ end
+ end
+
+ context 'with disable_anonymous_search feature flag disabled' do
+ before do
+ stub_feature_flags(disable_anonymous_search: false)
+ end
+
+ it 'finds one public item' do
+ expect(items).to contain_exactly(item6)
+ end
+ end
+ end
+ end
+
+ context 'filtering by item term in title' do
+ let(:params) { { search: 'git', in: 'title' } }
+
+ it 'returns items with title match for search term' do
+ expect(items).to contain_exactly(item1)
+ end
+ end
+
+ context 'filtering by items iids' do
+ let(:params) { { iids: [item3.iid] } }
+
+ it 'returns items where iids match' do
+ expect(items).to contain_exactly(item3, item5)
+ end
+
+ context 'using NOT' do
+ let(:params) { { not: { iids: [item3.iid] } } }
+
+ it 'returns items with no iids match' do
+ expect(items).to contain_exactly(item1, item2, item4)
+ end
+ end
+ end
+
+ context 'filtering by state' do
+ context 'with opened' do
+ let(:params) { { state: 'opened' } }
+
+ it 'returns only opened items' do
+ expect(items).to contain_exactly(item1, item2, item3, item4, item5)
+ end
+ end
+
+ context 'with closed' do
+ let(:params) { { state: 'closed' } }
+
+ it 'returns only closed items' do
+ expect(items).to contain_exactly(closed_item)
+ end
+ end
+
+ context 'with all' do
+ let(:params) { { state: 'all' } }
+
+ it 'returns all items' do
+ expect(items).to contain_exactly(item1, item2, item3, closed_item, item4, item5)
+ end
+ end
+
+ context 'with invalid state' do
+ let(:params) { { state: 'invalid_state' } }
+
+ it 'returns all items' do
+ expect(items).to contain_exactly(item1, item2, item3, closed_item, item4, item5)
+ end
+ end
+ end
+
+ context 'filtering by created_at' do
+ context 'through created_after' do
+ let(:params) { { created_after: item3.created_at } }
+
+ it 'returns items created on or after the given date' do
+ expect(items).to contain_exactly(item3)
+ end
+ end
+
+ context 'through created_before' do
+ let(:params) { { created_before: item1.created_at } }
+
+ it 'returns items created on or before the given date' do
+ expect(items).to contain_exactly(item1)
+ end
+ end
+
+ context 'through created_after and created_before' do
+ let(:params) { { created_after: item2.created_at, created_before: item3.created_at } }
+
+ it 'returns items created between the given dates' do
+ expect(items).to contain_exactly(item2, item3)
+ end
+ end
+ end
+
+ context 'filtering by updated_at' do
+ context 'through updated_after' do
+ let(:params) { { updated_after: item3.updated_at } }
+
+ it 'returns items updated on or after the given date' do
+ expect(items).to contain_exactly(item3)
+ end
+ end
+
+ context 'through updated_before' do
+ let(:params) { { updated_before: item1.updated_at } }
+
+ it 'returns items updated on or before the given date' do
+ expect(items).to contain_exactly(item1)
+ end
+ end
+
+ context 'through updated_after and updated_before' do
+ let(:params) { { updated_after: item2.updated_at, updated_before: item3.updated_at } }
+
+ it 'returns items updated between the given dates' do
+ expect(items).to contain_exactly(item2, item3)
+ end
+ end
+ end
+
+ context 'filtering by closed_at' do
+ let!(:closed_item1) { create(factory, project: project1, state: :closed, closed_at: 1.week.ago) }
+ let!(:closed_item2) { create(factory, project: project2, state: :closed, closed_at: 1.week.from_now) }
+ let!(:closed_item3) { create(factory, project: project2, state: :closed, closed_at: 2.weeks.from_now) }
+
+ context 'through closed_after' do
+ let(:params) { { state: :closed, closed_after: closed_item3.closed_at } }
+
+ it 'returns items closed on or after the given date' do
+ expect(items).to contain_exactly(closed_item3)
+ end
+ end
+
+ context 'through closed_before' do
+ let(:params) { { state: :closed, closed_before: closed_item1.closed_at } }
+
+ it 'returns items closed on or before the given date' do
+ expect(items).to contain_exactly(closed_item1)
+ end
+ end
+
+ context 'through closed_after and closed_before' do
+ let(:params) do
+ { state: :closed, closed_after: closed_item2.closed_at, closed_before: closed_item3.closed_at }
+ end
+
+ it 'returns items closed between the given dates' do
+ expect(items).to contain_exactly(closed_item2, closed_item3)
+ end
+ end
+ end
+
+ context 'filtering by reaction name' do
+ context 'user searches by no reaction' do
+ let(:params) { { my_reaction_emoji: 'None' } }
+
+ it 'returns items that the user did not react to' do
+ expect(items).to contain_exactly(item2, item4, item5)
+ end
+ end
+
+ context 'user searches by any reaction' do
+ let(:params) { { my_reaction_emoji: 'Any' } }
+
+ it 'returns items that the user reacted to' do
+ expect(items).to contain_exactly(item1, item3)
+ end
+ end
+
+ context 'user searches by "thumbsup" reaction' do
+ let(:params) { { my_reaction_emoji: 'thumbsup' } }
+
+ it 'returns items that the user thumbsup to' do
+ expect(items).to contain_exactly(item1)
+ end
+
+ context 'using NOT' do
+ let(:params) { { not: { my_reaction_emoji: 'thumbsup' } } }
+
+ it 'returns items that the user did not thumbsup to' do
+ expect(items).to contain_exactly(item2, item3, item4, item5)
+ end
+ end
+ end
+
+ context 'user2 searches by "thumbsup" reaction' do
+ let(:search_user) { user2 }
+
+ let(:params) { { my_reaction_emoji: 'thumbsup' } }
+
+ it 'returns items that the user2 thumbsup to' do
+ expect(items).to contain_exactly(item2)
+ end
+
+ context 'using NOT' do
+ let(:params) { { not: { my_reaction_emoji: 'thumbsup' } } }
+
+ it 'returns items that the user2 thumbsup to' do
+ expect(items).to contain_exactly(item3)
+ end
+ end
+ end
+
+ context 'user searches by "thumbsdown" reaction' do
+ let(:params) { { my_reaction_emoji: 'thumbsdown' } }
+
+ it 'returns items that the user thumbsdown to' do
+ expect(items).to contain_exactly(item3)
+ end
+
+ context 'using NOT' do
+ let(:params) { { not: { my_reaction_emoji: 'thumbsdown' } } }
+
+ it 'returns items that the user thumbsdown to' do
+ expect(items).to contain_exactly(item1, item2, item4, item5)
+ end
+ end
+ end
+ end
+
+ context 'filtering by confidential' do
+ let_it_be(:confidential_item) { create(factory, project: project1, confidential: true) }
+
+ context 'no filtering' do
+ it 'returns all items' do
+ expect(items).to contain_exactly(item1, item2, item3, item4, item5, confidential_item)
+ end
+ end
+
+ context 'user filters confidential items' do
+ let(:params) { { confidential: true } }
+
+ it 'returns only confidential items' do
+ expect(items).to contain_exactly(confidential_item)
+ end
+ end
+
+ context 'user filters only public items' do
+ let(:params) { { confidential: false } }
+
+ it 'returns only public items' do
+ expect(items).to contain_exactly(item1, item2, item3, item4, item5)
+ end
+ end
+ end
+
+ context 'filtering by item type' do
+ let_it_be(:incident_item) { create(factory, issue_type: :incident, project: project1) }
+
+ context 'no type given' do
+ let(:params) { { issue_types: [] } }
+
+ it 'returns all items' do
+ expect(items).to contain_exactly(incident_item, item1, item2, item3, item4, item5)
+ end
+ end
+
+ context 'incident type' do
+ let(:params) { { issue_types: ['incident'] } }
+
+ it 'returns incident items' do
+ expect(items).to contain_exactly(incident_item)
+ end
+ end
+
+ context 'item type' do
+ let(:params) { { issue_types: ['issue'] } }
+
+ it 'returns all items with type issue' do
+ expect(items).to contain_exactly(item1, item2, item3, item4, item5)
+ end
+ end
+
+ context 'multiple params' do
+ let(:params) { { issue_types: %w(issue incident) } }
+
+ it 'returns all items' do
+ expect(items).to contain_exactly(incident_item, item1, item2, item3, item4, item5)
+ end
+ end
+
+ context 'without array' do
+ let(:params) { { issue_types: 'incident' } }
+
+ it 'returns incident items' do
+ expect(items).to contain_exactly(incident_item)
+ end
+ end
+
+ context 'invalid params' do
+ let(:params) { { issue_types: ['nonsense'] } }
+
+ it 'returns no items' do
+ expect(items).to eq(items_model.none)
+ end
+ end
+ end
+
+ context 'filtering by crm contact' do
+ let_it_be(:contact1) { create(:contact, group: group) }
+ let_it_be(:contact2) { create(:contact, group: group) }
+
+ let_it_be(:contact1_item1) { create(factory, project: project1) }
+ let_it_be(:contact1_item2) { create(factory, project: project1) }
+ let_it_be(:contact2_item1) { create(factory, project: project1) }
+
+ let(:params) { { crm_contact_id: contact1.id } }
+
+ it 'returns for that contact' do
+ create(:issue_customer_relations_contact, issue: contact1_item1, contact: contact1)
+ create(:issue_customer_relations_contact, issue: contact1_item2, contact: contact1)
+ create(:issue_customer_relations_contact, issue: contact2_item1, contact: contact2)
+
+ expect(items).to contain_exactly(contact1_item1, contact1_item2)
+ end
+ end
+
+ context 'filtering by crm organization' do
+ let_it_be(:organization) { create(:organization, group: group) }
+ let_it_be(:contact1) { create(:contact, group: group, organization: organization) }
+ let_it_be(:contact2) { create(:contact, group: group, organization: organization) }
+
+ let_it_be(:contact1_item1) { create(factory, project: project1) }
+ let_it_be(:contact1_item2) { create(factory, project: project1) }
+ let_it_be(:contact2_item1) { create(factory, project: project1) }
+
+ let(:params) { { crm_organization_id: organization.id } }
+
+ it 'returns for that contact' do
+ create(:issue_customer_relations_contact, issue: contact1_item1, contact: contact1)
+ create(:issue_customer_relations_contact, issue: contact1_item2, contact: contact1)
+ create(:issue_customer_relations_contact, issue: contact2_item1, contact: contact2)
+
+ expect(items).to contain_exactly(contact1_item1, contact1_item2, contact2_item1)
+ end
+ end
+
+ context 'when the user is unauthorized' do
+ let(:search_user) { nil }
+
+ it 'returns no results' do
+ expect(items).to be_empty
+ end
+ end
+
+ context 'when the user can see some, but not all, items' do
+ let(:search_user) { user2 }
+
+ it 'returns only items they can see' do
+ expect(items).to contain_exactly(item2, item3)
+ end
+ end
+
+ it 'finds items user can access due to group' do
+ group = create(:group)
+ project = create(:project, group: group)
+ item = create(factory, project: project)
+ group.add_user(user, :owner)
+
+ expect(items).to include(item)
+ end
+ end
+
+ context 'personal scope' do
+ let(:scope) { 'assigned_to_me' }
+
+ it 'returns item assigned to the user' do
+ expect(items).to contain_exactly(item1, item2, item5)
+ end
+
+ context 'filtering by project' do
+ let(:params) { { project_id: project1.id } }
+
+ it 'returns items assigned to the user in that project' do
+ expect(items).to contain_exactly(item1, item5)
+ end
+ end
+ end
+
+ context 'when project restricts items' do
+ let(:scope) { nil }
+
+ it "doesn't return team-only items to non team members" do
+ project = create(:project, :public, :issues_private)
+ item = create(factory, project: project)
+
+ expect(items).not_to include(item)
+ end
+
+ it "doesn't return items if feature disabled" do
+ [project1, project2, project3].each do |project|
+ project.project_feature.update!(issues_access_level: ProjectFeature::DISABLED)
+ end
+
+ expect(items.count).to eq 0
+ end
+ end
+
+ context 'external authorization' do
+ it_behaves_like 'a finder with external authorization service' do
+ let!(:subject) { create(factory, project: project) }
+ let(:project_params) { { project_id: project.id } }
+ end
+ end
+
+ context 'filtering by due date' do
+ let_it_be(:item_due_today) { create(factory, project: project1, due_date: Date.current) }
+ let_it_be(:item_due_tomorrow) { create(factory, project: project1, due_date: 1.day.from_now) }
+ let_it_be(:item_overdue) { create(factory, project: project1, due_date: 2.days.ago) }
+ let_it_be(:item_due_soon) { create(factory, project: project1, due_date: 2.days.from_now) }
+
+ let(:scope) { 'all' }
+ let(:base_params) { { project_id: project1.id } }
+
+ context 'with param set to no due date' do
+ let(:params) { base_params.merge(due_date: items_model::NoDueDate.name) }
+
+ it 'returns items with no due date' do
+ expect(items).to contain_exactly(item1, item5)
+ end
+ end
+
+ context 'with param set to any due date' do
+ let(:params) { base_params.merge(due_date: items_model::AnyDueDate.name) }
+
+ it 'returns items with any due date' do
+ expect(items).to contain_exactly(item_due_today, item_due_tomorrow, item_overdue, item_due_soon)
+ end
+ end
+
+ context 'with param set to due today' do
+ let(:params) { base_params.merge(due_date: items_model::DueToday.name) }
+
+ it 'returns items due today' do
+ expect(items).to contain_exactly(item_due_today)
+ end
+ end
+
+ context 'with param set to due tomorrow' do
+ let(:params) { base_params.merge(due_date: items_model::DueTomorrow.name) }
+
+ it 'returns items due today' do
+ expect(items).to contain_exactly(item_due_tomorrow)
+ end
+ end
+
+ context 'with param set to overdue' do
+ let(:params) { base_params.merge(due_date: items_model::Overdue.name) }
+
+ it 'returns overdue items' do
+ expect(items).to contain_exactly(item_overdue)
+ end
+ end
+
+ context 'with param set to next month and previous two weeks' do
+ let(:params) { base_params.merge(due_date: items_model::DueNextMonthAndPreviousTwoWeeks.name) }
+
+ it 'returns items due in the previous two weeks and next month' do
+ expect(items).to contain_exactly(item_due_today, item_due_tomorrow, item_overdue, item_due_soon)
+ end
+ end
+
+ context 'with invalid param' do
+ let(:params) { base_params.merge(due_date: 'foo') }
+
+ it 'returns no items' do
+ expect(items).to be_empty
+ end
+ end
+ end
+ end
+
+ describe '#row_count', :request_store do
+ let_it_be(:admin) { create(:admin) }
+
+ context 'when admin mode is enabled', :enable_admin_mode do
+ it 'returns the number of rows for the default state' do
+ finder = described_class.new(admin)
+
+ expect(finder.row_count).to eq(5)
+ end
+
+ it 'returns the number of rows for a given state' do
+ finder = described_class.new(admin, state: 'closed')
+
+ expect(finder.row_count).to be_zero
+ end
+ end
+
+ context 'when admin mode is disabled' do
+ it 'returns no rows' do
+ finder = described_class.new(admin)
+
+ expect(finder.row_count).to be_zero
+ end
+ end
+
+ it 'returns -1 if the query times out' do
+ finder = described_class.new(admin)
+
+ expect_next_instance_of(described_class) do |subfinder|
+ expect(subfinder).to receive(:execute).and_raise(ActiveRecord::QueryCanceled)
+ end
+
+ expect(finder.row_count).to eq(-1)
+ end
+ end
+
+ describe '#with_confidentiality_access_check' do
+ let(:guest) { create(:user) }
+
+ let_it_be(:authorized_user) { create(:user) }
+ let_it_be(:banned_user) { create(:user, :banned) }
+ let_it_be(:project) { create(:project, namespace: authorized_user.namespace) }
+ let_it_be(:public_item) { create(factory, project: project) }
+ let_it_be(:confidential_item) { create(factory, project: project, confidential: true) }
+ let_it_be(:hidden_item) { create(factory, project: project, author: banned_user) }
+
+ shared_examples 'returns public, does not return hidden or confidential' do
+ it 'returns only public items' do
+ expect(subject).to include(public_item)
+ expect(subject).not_to include(confidential_item, hidden_item)
+ end
+ end
+
+ shared_examples 'returns public and confidential, does not return hidden' do
+ it 'returns only public and confidential items' do
+ expect(subject).to include(public_item, confidential_item)
+ expect(subject).not_to include(hidden_item)
+ end
+ end
+
+ shared_examples 'returns public and hidden, does not return confidential' do
+ it 'returns only public and hidden items' do
+ expect(subject).to include(public_item, hidden_item)
+ expect(subject).not_to include(confidential_item)
+ end
+ end
+
+ shared_examples 'returns public, confidential, and hidden' do
+ it 'returns all items' do
+ expect(subject).to include(public_item, confidential_item, hidden_item)
+ end
+ end
+
+ context 'when no project filter is given' do
+ let(:params) { {} }
+
+ context 'for an anonymous user' do
+ subject { described_class.new(nil, params).with_confidentiality_access_check }
+
+ it_behaves_like 'returns public, does not return hidden or confidential'
+
+ context 'when feature flag is disabled' do
+ before do
+ stub_feature_flags(ban_user_feature_flag: false)
+ end
+
+ it_behaves_like 'returns public and hidden, does not return confidential'
+ end
+ end
+
+ context 'for a user without project membership' do
+ subject { described_class.new(user, params).with_confidentiality_access_check }
+
+ it_behaves_like 'returns public, does not return hidden or confidential'
+
+ context 'when feature flag is disabled' do
+ before do
+ stub_feature_flags(ban_user_feature_flag: false)
+ end
+
+ it_behaves_like 'returns public and hidden, does not return confidential'
+ end
+ end
+
+ context 'for a guest user' do
+ subject { described_class.new(guest, params).with_confidentiality_access_check }
+
+ before do
+ project.add_guest(guest)
+ end
+
+ it_behaves_like 'returns public, does not return hidden or confidential'
+
+ context 'when feature flag is disabled' do
+ before do
+ stub_feature_flags(ban_user_feature_flag: false)
+ end
+
+ it_behaves_like 'returns public and hidden, does not return confidential'
+ end
+ end
+
+ context 'for a project member with access to view confidential items' do
+ subject { described_class.new(authorized_user, params).with_confidentiality_access_check }
+
+ it_behaves_like 'returns public and confidential, does not return hidden'
+
+ context 'when feature flag is disabled' do
+ before do
+ stub_feature_flags(ban_user_feature_flag: false)
+ end
+
+ it_behaves_like 'returns public, confidential, and hidden'
+ end
+ end
+
+ context 'for an admin' do
+ let(:admin_user) { create(:user, :admin) }
+
+ subject { described_class.new(admin_user, params).with_confidentiality_access_check }
+
+ context 'when admin mode is enabled', :enable_admin_mode do
+ it_behaves_like 'returns public, confidential, and hidden'
+
+ context 'when feature flag is disabled' do
+ before do
+ stub_feature_flags(ban_user_feature_flag: false)
+ end
+
+ it_behaves_like 'returns public, confidential, and hidden'
+ end
+ end
+
+ context 'when admin mode is disabled' do
+ it_behaves_like 'returns public, does not return hidden or confidential'
+
+ context 'when feature flag is disabled' do
+ before do
+ stub_feature_flags(ban_user_feature_flag: false)
+ end
+
+ it_behaves_like 'returns public and hidden, does not return confidential'
+ end
+ end
+ end
+ end
+
+ context 'when searching within a specific project' do
+ let(:params) { { project_id: project.id } }
+
+ context 'for an anonymous user' do
+ subject { described_class.new(nil, params).with_confidentiality_access_check }
+
+ it_behaves_like 'returns public, does not return hidden or confidential'
+
+ context 'when feature flag is disabled' do
+ before do
+ stub_feature_flags(ban_user_feature_flag: false)
+ end
+
+ it_behaves_like 'returns public and hidden, does not return confidential'
+ end
+
+ it 'does not filter by confidentiality' do
+ expect(items_model).not_to receive(:where).with(a_string_matching('confidential'), anything)
+ subject
+ end
+ end
+
+ context 'for a user without project membership' do
+ subject { described_class.new(user, params).with_confidentiality_access_check }
+
+ it_behaves_like 'returns public, does not return hidden or confidential'
+
+ context 'when feature flag is disabled' do
+ before do
+ stub_feature_flags(ban_user_feature_flag: false)
+ end
+
+ it_behaves_like 'returns public and hidden, does not return confidential'
+ end
+
+ it 'filters by confidentiality' do
+ expect(subject.to_sql).to match("issues.confidential")
+ end
+ end
+
+ context 'for a guest user' do
+ subject { described_class.new(guest, params).with_confidentiality_access_check }
+
+ before do
+ project.add_guest(guest)
+ end
+
+ it_behaves_like 'returns public, does not return hidden or confidential'
+
+ context 'when feature flag is disabled' do
+ before do
+ stub_feature_flags(ban_user_feature_flag: false)
+ end
+
+ it_behaves_like 'returns public and hidden, does not return confidential'
+ end
+
+ it 'filters by confidentiality' do
+ expect(subject.to_sql).to match("issues.confidential")
+ end
+ end
+
+ context 'for a project member with access to view confidential items' do
+ subject { described_class.new(authorized_user, params).with_confidentiality_access_check }
+
+ it_behaves_like 'returns public and confidential, does not return hidden'
+
+ context 'when feature flag is disabled' do
+ before do
+ stub_feature_flags(ban_user_feature_flag: false)
+ end
+
+ it_behaves_like 'returns public, confidential, and hidden'
+ end
+
+ it 'does not filter by confidentiality' do
+ expect(items_model).not_to receive(:where).with(a_string_matching('confidential'), anything)
+
+ subject
+ end
+ end
+
+ context 'for an admin' do
+ let(:admin_user) { create(:user, :admin) }
+
+ subject { described_class.new(admin_user, params).with_confidentiality_access_check }
+
+ context 'when admin mode is enabled', :enable_admin_mode do
+ it_behaves_like 'returns public, confidential, and hidden'
+
+ context 'when feature flag is disabled' do
+ before do
+ stub_feature_flags(ban_user_feature_flag: false)
+ end
+
+ it_behaves_like 'returns public, confidential, and hidden'
+ end
+
+ it 'does not filter by confidentiality' do
+ expect(items_model).not_to receive(:where).with(a_string_matching('confidential'), anything)
+
+ subject
+ end
+ end
+
+ context 'when admin mode is disabled' do
+ it_behaves_like 'returns public, does not return hidden or confidential'
+
+ context 'when feature flag is disabled' do
+ before do
+ stub_feature_flags(ban_user_feature_flag: false)
+ end
+
+ it_behaves_like 'returns public and hidden, does not return confidential'
+ end
+
+ it 'filters by confidentiality' do
+ expect(subject.to_sql).to match("issues.confidential")
+ end
+ end
+ end
+ end
+ end
+
+ describe '#use_cte_for_search?' do
+ let(:finder) { described_class.new(nil, params) }
+
+ context 'when there is no search param' do
+ let(:params) { { attempt_group_search_optimizations: true } }
+
+ it 'returns false' do
+ expect(finder.use_cte_for_search?).to be_falsey
+ end
+ end
+
+ context 'when the force_cte param is falsey' do
+ let(:params) { { search: '日本語' } }
+
+ it 'returns false' do
+ expect(finder.use_cte_for_search?).to be_falsey
+ end
+ end
+
+ context 'when a non-simple sort is given' do
+ let(:params) { { search: '日本語', attempt_project_search_optimizations: true, sort: 'popularity' } }
+
+ it 'returns false' do
+ expect(finder.use_cte_for_search?).to be_falsey
+ end
+ end
+
+ context 'when all conditions are met' do
+ context "uses group search optimization" do
+ let(:params) { { search: '日本語', attempt_group_search_optimizations: true } }
+
+ it 'returns true' do
+ expect(finder.use_cte_for_search?).to be_truthy
+ expect(finder.execute.to_sql)
+ .to match(/^WITH "issues" AS #{Gitlab::Database::AsWithMaterialized.materialized_if_supported}/)
+ end
+ end
+
+ context "uses project search optimization" do
+ let(:params) { { search: '日本語', attempt_project_search_optimizations: true } }
+
+ it 'returns true' do
+ expect(finder.use_cte_for_search?).to be_truthy
+ expect(finder.execute.to_sql)
+ .to match(/^WITH "issues" AS #{Gitlab::Database::AsWithMaterialized.materialized_if_supported}/)
+ end
+ end
+
+ context 'with simple sort' do
+ let(:params) { { search: '日本語', attempt_project_search_optimizations: true, sort: 'updated_desc' } }
+
+ it 'returns true' do
+ expect(finder.use_cte_for_search?).to be_truthy
+ expect(finder.execute.to_sql)
+ .to match(/^WITH "issues" AS #{Gitlab::Database::AsWithMaterialized.materialized_if_supported}/)
+ end
+ end
+
+ context 'with simple sort as a symbol' do
+ let(:params) { { search: '日本語', attempt_project_search_optimizations: true, sort: :updated_desc } }
+
+ it 'returns true' do
+ expect(finder.use_cte_for_search?).to be_truthy
+ expect(finder.execute.to_sql)
+ .to match(/^WITH "issues" AS #{Gitlab::Database::AsWithMaterialized.materialized_if_supported}/)
+ end
+ end
+ end
+ end
+
+ describe '#parent_param=' do
+ using RSpec::Parameterized::TableSyntax
+
+ let(:finder) { described_class.new(nil) }
+
+ subject { finder.parent_param = obj }
+
+ where(:klass, :param) do
+ :Project | :project_id
+ :Group | :group_id
+ end
+
+ with_them do
+ let(:obj) { Object.const_get(klass, false).new }
+
+ it 'sets the params' do
+ subject
+
+ expect(finder.params[param]).to eq(obj)
+ end
+ end
+
+ context 'unexpected parent' do
+ let(:obj) { MergeRequest.new }
+
+ it 'raises an error' do
+ expect { subject }.to raise_error('Unexpected parent: MergeRequest')
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/graphql/members_shared_examples.rb b/spec/support/shared_examples/graphql/members_shared_examples.rb
index 8e9e22f4359..110706c730b 100644
--- a/spec/support/shared_examples/graphql/members_shared_examples.rb
+++ b/spec/support/shared_examples/graphql/members_shared_examples.rb
@@ -39,7 +39,8 @@ RSpec.shared_examples 'querying members with a group' do
let(:base_args) { { relations: described_class.arguments['relations'].default_value } }
subject do
- resolve(described_class, obj: resource, args: base_args.merge(args), ctx: { current_user: user_4 })
+ resolve(described_class, obj: resource, args: base_args.merge(args),
+ ctx: { current_user: user_4 }, arg_style: :internal)
end
describe '#resolve' do
@@ -73,7 +74,8 @@ RSpec.shared_examples 'querying members with a group' do
let_it_be(:other_user) { create(:user) }
subject do
- resolve(described_class, obj: resource, args: base_args.merge(args), ctx: { current_user: other_user })
+ resolve(described_class, obj: resource, args: base_args.merge(args),
+ ctx: { current_user: other_user }, arg_style: :internal)
end
it 'generates an error' 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
index b989dbc6524..cd591248ff6 100644
--- 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
@@ -21,6 +21,7 @@ RSpec.shared_examples 'creating an incident timeline event' do
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)
+ expect(timeline_event.editable).to eq(expected_timeline_event.editable)
end
end
diff --git a/spec/support/shared_examples/graphql/mutations/security/ci_configuration_shared_examples.rb b/spec/support/shared_examples/graphql/mutations/security/ci_configuration_shared_examples.rb
index 21260e4d954..dfb8ce64391 100644
--- a/spec/support/shared_examples/graphql/mutations/security/ci_configuration_shared_examples.rb
+++ b/spec/support/shared_examples/graphql/mutations/security/ci_configuration_shared_examples.rb
@@ -71,7 +71,7 @@ RSpec.shared_examples_for 'graphql mutations security ci configuration' do
end
it 'returns an array of errors' do
- expect(result).to match(
+ expect(result).to include(
branch: be_nil,
success_path: be_nil,
errors: match_array([error_message])
@@ -92,7 +92,7 @@ RSpec.shared_examples_for 'graphql mutations security ci configuration' do
end
it 'returns a success path' do
- expect(result).to match(
+ expect(result).to include(
branch: branch,
success_path: success_path,
errors: []
@@ -108,7 +108,7 @@ RSpec.shared_examples_for 'graphql mutations security ci configuration' do
end
it 'returns an array of errors' do
- expect(result).to match(
+ expect(result).to include(
branch: be_nil,
success_path: be_nil,
errors: match_array([error])
diff --git a/spec/support/shared_examples/graphql/projects/merge_request_n_plus_one_query_examples.rb b/spec/support/shared_examples/graphql/n_plus_one_query_examples.rb
index 738edd43c92..738edd43c92 100644
--- a/spec/support/shared_examples/graphql/projects/merge_request_n_plus_one_query_examples.rb
+++ b/spec/support/shared_examples/graphql/n_plus_one_query_examples.rb
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 da8562161e7..3017f62a7c9 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
@@ -24,7 +24,7 @@ RSpec.shared_examples 'group and projects packages resolver' do
create(:maven_package, name: 'baz', project: project, created_at: 1.minute.ago, version: nil)
end
- [:created_desc, :name_desc, :version_desc, :type_asc].each do |order|
+ %w[CREATED_DESC NAME_DESC VERSION_DESC TYPE_ASC].each do |order|
context "#{order}" do
let(:args) { { sort: order } }
@@ -32,7 +32,7 @@ RSpec.shared_examples 'group and projects packages resolver' do
end
end
- [:created_asc, :name_asc, :version_asc, :type_desc].each do |order|
+ %w[CREATED_ASC NAME_ASC VERSION_ASC TYPE_DESC].each do |order|
context "#{order}" do
let(:args) { { sort: order } }
@@ -41,25 +41,25 @@ RSpec.shared_examples 'group and projects packages resolver' do
end
context 'filter by package_name' do
- let(:args) { { package_name: 'bar', sort: :created_desc } }
+ let(:args) { { package_name: 'bar', sort: 'CREATED_DESC' } }
it { is_expected.to eq([conan_package]) }
end
context 'filter by package_type' do
- let(:args) { { package_type: 'conan', sort: :created_desc } }
+ let(:args) { { package_type: 'conan', sort: 'CREATED_DESC' } }
it { is_expected.to eq([conan_package]) }
end
context 'filter by status' do
- let(:args) { { status: 'error', sort: :created_desc } }
+ let(:args) { { status: 'error', sort: 'CREATED_DESC' } }
it { is_expected.to eq([maven_package]) }
end
context 'include_versionless' do
- let(:args) { { include_versionless: true, sort: :created_desc } }
+ let(:args) { { include_versionless: true, sort: 'CREATED_DESC' } }
it { is_expected.to include(repository3) }
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 cf9c36fafe8..7fd54408b11 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
@@ -53,18 +53,20 @@ RSpec.shared_examples 'Gitlab-style deprecations' do
it 'adds information about the replacement if provided' do
deprecable = subject(deprecated: { milestone: '1.10', reason: :renamed, replacement: 'Foo.bar' })
- expect(deprecable.deprecation_reason).to include 'Please use `Foo.bar`'
+ expect(deprecable.deprecation_reason).to include('Please use `Foo.bar`')
end
it 'supports named reasons: renamed' do
deprecable = subject(deprecated: { milestone: '1.10', reason: :renamed })
- expect(deprecable.deprecation_reason).to include 'This was renamed.'
+ expect(deprecable.deprecation_reason).to eq('This was renamed. Deprecated in 1.10.')
end
it 'supports named reasons: alpha' do
deprecable = subject(deprecated: { milestone: '1.10', reason: :alpha })
- expect(deprecable.deprecation_reason).to include 'This feature is in Alpha'
+ expect(deprecable.deprecation_reason).to eq(
+ 'This feature is in Alpha. It can be changed or removed at any time. Introduced in 1.10.'
+ )
end
end
diff --git a/spec/support/shared_examples/integrations/integration_settings_form.rb b/spec/support/shared_examples/integrations/integration_settings_form.rb
index d8a46180796..dfe5a071f91 100644
--- a/spec/support/shared_examples/integrations/integration_settings_form.rb
+++ b/spec/support/shared_examples/integrations/integration_settings_form.rb
@@ -20,10 +20,18 @@ RSpec.shared_examples 'integration settings form' do
"#{integration.title} field #{field_name} not present"
end
+ sections = integration.sections
events = parse_json(trigger_events_for_integration(integration))
+
events.each do |trigger|
- expect(page).to have_field(trigger[:title], type: 'checkbox', wait: 0),
- "#{integration.title} field #{title} checkbox not present"
+ trigger_title = if sections.any? { |s| s[:type] == 'trigger' }
+ trigger_event_title(trigger[:name])
+ else
+ trigger[:title]
+ end
+
+ expect(page).to have_field(trigger_title, type: 'checkbox', wait: 0),
+ "#{integration.title} field #{trigger_title} checkbox not present"
end
end
end
@@ -35,4 +43,20 @@ RSpec.shared_examples 'integration settings form' do
def parse_json(json)
Gitlab::Json.parse(json, symbolize_names: true)
end
+
+ def trigger_event_title(name)
+ # Should match `integrationTriggerEventTitles` in app/assets/javascripts/integrations/constants.js
+ event_titles = {
+ push_events: s_('IntegrationEvents|A push is made to the repository'),
+ issues_events: s_('IntegrationEvents|IntegrationEvents|An issue is created, updated, or closed'),
+ confidential_issues_events: s_('IntegrationEvents|A confidential issue is created, updated, or closed'),
+ merge_requests_events: s_('IntegrationEvents|A merge request is created, updated, or merged'),
+ note_events: s_('IntegrationEvents|A comment is added on an issue'),
+ confidential_note_events: s_('IntegrationEvents|A comment is added on a confidential issue'),
+ tag_push_events: s_('IntegrationEvents|A tag is pushed to the repository'),
+ pipeline_events: s_('IntegrationEvents|A pipeline status changes'),
+ wiki_page_events: s_('IntegrationEvents|A wiki page is created or updated')
+ }.with_indifferent_access
+ event_titles[name]
+ end
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 e886ec65b02..284c129221b 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
@@ -834,8 +834,8 @@ RSpec.shared_examples 'trace with enabled live trace feature' do
end
end
- describe '#live_trace_exist?' do
- subject { trace.live_trace_exist? }
+ describe '#live?' do
+ subject { trace.live? }
context 'when trace does not exist' do
it { is_expected.to be_falsy }
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
index 67d739b79ab..d14216ec5ff 100644
--- 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
@@ -22,7 +22,7 @@ RSpec.shared_context 'reconfigures connection stack' do |db_config_name|
end
end
- def validate_connections!
+ def validate_connections_stack!
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
diff --git a/spec/support/shared_examples/lib/gitlab/event_store_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/event_store_shared_examples.rb
index 4fc15cacab4..db2f2f2d0f0 100644
--- a/spec/support/shared_examples/lib/gitlab/event_store_shared_examples.rb
+++ b/spec/support/shared_examples/lib/gitlab/event_store_shared_examples.rb
@@ -11,6 +11,8 @@ RSpec.shared_examples 'subscribes to event' do
::Gitlab::EventStore.publish(event)
end
+
+ it_behaves_like 'an idempotent worker'
end
def consume_event(subscriber:, event:)
diff --git a/spec/support/shared_examples/lib/gitlab/redis/multi_store_feature_flags_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/redis/multi_store_feature_flags_shared_examples.rb
new file mode 100644
index 00000000000..a5e4df1c272
--- /dev/null
+++ b/spec/support/shared_examples/lib/gitlab/redis/multi_store_feature_flags_shared_examples.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'multi store feature flags' do |use_primary_and_secondary_stores, use_primary_store_as_default|
+ context "with feature flag :#{use_primary_and_secondary_stores} is enabled" do
+ before do
+ stub_feature_flags(use_primary_and_secondary_stores => true)
+ end
+
+ it 'multi store is enabled' do
+ subject.with do |redis_instance|
+ expect(redis_instance.use_primary_and_secondary_stores?).to be true
+ end
+ end
+ end
+
+ context "with feature flag :#{use_primary_and_secondary_stores} is disabled" do
+ before do
+ stub_feature_flags(use_primary_and_secondary_stores => false)
+ end
+
+ it 'multi store is disabled' do
+ subject.with do |redis_instance|
+ expect(redis_instance.use_primary_and_secondary_stores?).to be false
+ end
+ end
+ end
+
+ context "with feature flag :#{use_primary_store_as_default} is enabled" do
+ before do
+ stub_feature_flags(use_primary_store_as_default => true)
+ end
+
+ it 'primary store is enabled' do
+ subject.with do |redis_instance|
+ expect(redis_instance.use_primary_store_as_default?).to be true
+ end
+ end
+ end
+
+ context "with feature flag :#{use_primary_store_as_default} is disabled" do
+ before do
+ stub_feature_flags(use_primary_store_as_default => false)
+ end
+
+ it 'primary store is disabled' do
+ subject.with do |redis_instance|
+ expect(redis_instance.use_primary_store_as_default?).to be false
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/models/application_setting_shared_examples.rb b/spec/support/shared_examples/models/application_setting_shared_examples.rb
index 74ec6474e80..6e7d04d3cba 100644
--- a/spec/support/shared_examples/models/application_setting_shared_examples.rb
+++ b/spec/support/shared_examples/models/application_setting_shared_examples.rb
@@ -238,8 +238,16 @@ RSpec.shared_examples 'application settings examples' do
end
describe '#allowed_key_types' do
- it 'includes all key types by default' do
- expect(setting.allowed_key_types).to contain_exactly(*Gitlab::SSHPublicKey.supported_types)
+ context 'in non-FIPS mode', fips_mode: false do
+ it 'includes all key types by default' do
+ expect(setting.allowed_key_types).to contain_exactly(*Gitlab::SSHPublicKey.supported_types)
+ end
+ end
+
+ context 'in FIPS mode', :fips_mode do
+ it 'excludes DSA from supported key types' do
+ expect(setting.allowed_key_types).to contain_exactly(*Gitlab::SSHPublicKey.supported_types - %i(dsa))
+ end
end
it 'excludes disabled key types' do
diff --git a/spec/support/shared_examples/models/commit_signature_shared_examples.rb b/spec/support/shared_examples/models/commit_signature_shared_examples.rb
new file mode 100644
index 00000000000..56d5c1da3af
--- /dev/null
+++ b/spec/support/shared_examples/models/commit_signature_shared_examples.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'commit signature' do
+ describe 'associations' do
+ it { is_expected.to belong_to(:project).required }
+ end
+
+ describe 'validation' do
+ subject { described_class.new }
+
+ it { is_expected.to validate_presence_of(:commit_sha) }
+ it { is_expected.to validate_presence_of(:project_id) }
+ end
+
+ describe '.safe_create!' do
+ it 'finds a signature by commit sha if it existed' do
+ signature
+
+ expect(described_class.safe_create!(commit_sha: commit_sha)).to eq(signature)
+ end
+
+ it 'creates a new signature if it was not found' do
+ expect { described_class.safe_create!(attributes) }.to change { described_class.count }.by(1)
+ end
+
+ it 'assigns the correct attributes when creating' do
+ signature = described_class.safe_create!(attributes)
+
+ expect(signature).to have_attributes(attributes)
+ end
+
+ it 'does not raise an error in case of a race condition' do
+ expect(described_class).to receive(:find_by).and_return(nil, instance_double(described_class, persisted?: true))
+
+ expect(described_class).to receive(:create).and_raise(ActiveRecord::RecordNotUnique)
+ allow(described_class).to receive(:create).and_call_original
+
+ described_class.safe_create!(attributes)
+ end
+ end
+
+ describe '#commit' do
+ it 'fetches the commit through the project' do
+ expect_next_instance_of(Project) do |instance|
+ expect(instance).to receive(:commit).with(commit_sha).and_return(commit)
+ end
+
+ signature.commit
+ end
+ end
+end
diff --git a/spec/support/shared_examples/models/concerns/limitable_shared_examples.rb b/spec/support/shared_examples/models/concerns/limitable_shared_examples.rb
index 0ff0895b861..3d393e6dcb5 100644
--- a/spec/support/shared_examples/models/concerns/limitable_shared_examples.rb
+++ b/spec/support/shared_examples/models/concerns/limitable_shared_examples.rb
@@ -1,6 +1,30 @@
# frozen_string_literal: true
RSpec.shared_examples 'includes Limitable concern' do
+ describe '#exceeds_limits?' do
+ let(:plan_limits) { create(:plan_limits, :default_plan) }
+
+ context 'without plan limits configured' do
+ it { expect(subject.exceeds_limits?).to eq false }
+ end
+
+ context 'without plan limits configured' do
+ before do
+ plan_limits.update!(subject.class.limit_name => 1)
+ end
+
+ it { expect(subject.exceeds_limits?).to eq false }
+
+ context 'with an existing model' do
+ before do
+ subject.clone.save!
+ end
+
+ it { expect(subject.exceeds_limits?).to eq true }
+ end
+ end
+ end
+
describe 'validations' do
let(:plan_limits) { create(:plan_limits, :default_plan) }
diff --git a/spec/support/shared_examples/models/integrations/base_data_fields_shared_examples.rb b/spec/support/shared_examples/models/integrations/base_data_fields_shared_examples.rb
new file mode 100644
index 00000000000..211beb5b32f
--- /dev/null
+++ b/spec/support/shared_examples/models/integrations/base_data_fields_shared_examples.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.shared_examples Integrations::BaseDataFields do
+ subject(:model) { described_class.new }
+
+ describe 'associations' do
+ it { is_expected.to belong_to :integration }
+ end
+
+ describe '#activated?' do
+ subject(:activated?) { model.activated? }
+
+ context 'with integration' do
+ let(:integration) { instance_spy(Integration, activated?: activated) }
+
+ before do
+ allow(model).to receive(:integration).and_return(integration)
+ end
+
+ context 'with value set to false' do
+ let(:activated) { false }
+
+ it { is_expected.to eq(false) }
+ end
+
+ context 'with value set to true' do
+ let(:activated) { true }
+
+ it { is_expected.to eq(true) }
+ end
+ end
+
+ context 'without integration' do
+ before do
+ allow(model).to receive(:integration).and_return(nil)
+ end
+
+ it { is_expected.to eq(false) }
+ end
+ end
+
+ describe '#to_database_hash' do
+ it 'does not include certain attributes' do
+ hash = model.to_database_hash
+
+ expect(hash.keys).not_to include('id', 'service_id', 'integration_id', 'created_at', 'updated_at')
+ end
+ 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 e293d10964b..75fff11cecd 100644
--- a/spec/support/shared_examples/models/member_shared_examples.rb
+++ b/spec/support/shared_examples/models/member_shared_examples.rb
@@ -80,7 +80,7 @@ RSpec.shared_examples_for "member creation" do
let_it_be(:admin) { create(:admin) }
it 'returns a Member object', :aggregate_failures do
- member = described_class.new(source, user, :maintainer).execute
+ member = described_class.add_user(source, user, :maintainer)
expect(member).to be_a member_type
expect(member).to be_persisted
@@ -99,7 +99,7 @@ RSpec.shared_examples_for "member creation" do
end
it 'does not update the member' do
- member = described_class.new(source, project_bot, :maintainer, current_user: user).execute
+ member = described_class.add_user(source, project_bot, :maintainer, current_user: user)
expect(source.users.reload).to include(project_bot)
expect(member).to be_persisted
@@ -110,7 +110,7 @@ RSpec.shared_examples_for "member creation" do
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
+ member = described_class.add_user(source, project_bot, :maintainer, current_user: user)
expect(source.users.reload).to include(project_bot)
expect(member).to be_persisted
@@ -120,7 +120,7 @@ RSpec.shared_examples_for "member creation" do
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
+ member = described_class.add_user(source, user, :maintainer, current_user: admin)
expect(member).to be_persisted
expect(source.users.reload).to include(user)
@@ -130,7 +130,7 @@ RSpec.shared_examples_for "member creation" do
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
+ member = described_class.add_user(source, user, :maintainer, current_user: admin)
expect(member).not_to be_persisted
expect(source.users.reload).not_to include(user)
@@ -139,7 +139,7 @@ RSpec.shared_examples_for "member creation" do
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
+ member = described_class.add_user(source, user, :maintainer, expires_at: Date.new(2016, 9, 22))
expect(member.expires_at).to eq(Date.new(2016, 9, 22))
end
@@ -148,7 +148,7 @@ RSpec.shared_examples_for "member creation" do
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
+ member = described_class.add_user(source, user.id, sym_key)
expect(member.access_level).to eq(int_access_level)
expect(source.users.reload).to include(user)
@@ -157,7 +157,7 @@ RSpec.shared_examples_for "member creation" do
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
+ member = described_class.add_user(source, user.id, int_access_level)
expect(member.access_level).to eq(int_access_level)
expect(source.users.reload).to include(user)
@@ -169,7 +169,7 @@ RSpec.shared_examples_for "member creation" do
it 'adds the user as a member' do
expect(source.users).not_to include(user)
- described_class.new(source, user.id, :maintainer).execute
+ described_class.add_user(source, user.id, :maintainer)
expect(source.users.reload).to include(user)
end
@@ -179,7 +179,7 @@ RSpec.shared_examples_for "member creation" do
it 'does not add 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.add_user(source, non_existing_record_id, :maintainer)
expect(source.users.reload).not_to include(user)
end
@@ -189,7 +189,7 @@ RSpec.shared_examples_for "member creation" do
it 'adds the user as a member' do
expect(source.users).not_to include(user)
- described_class.new(source, user, :maintainer).execute
+ described_class.add_user(source, user, :maintainer)
expect(source.users.reload).to include(user)
end
@@ -200,12 +200,12 @@ RSpec.shared_examples_for "member creation" do
source.request_access(user)
end
- it 'adds the requester as a member', :aggregate_failures do
+ it 'does not add the requester as a regular 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
+ described_class.add_user(source, user, :maintainer)
end.to raise_error(Gitlab::Access::AccessDeniedError)
expect(source.users.reload).not_to include(user)
@@ -217,7 +217,7 @@ RSpec.shared_examples_for "member creation" do
it 'adds the user as a member' do
expect(source.users).not_to include(user)
- described_class.new(source, user.email, :maintainer).execute
+ described_class.add_user(source, user.email, :maintainer)
expect(source.users.reload).to include(user)
end
@@ -227,7 +227,7 @@ RSpec.shared_examples_for "member creation" do
it 'creates an invited member' do
expect(source.users).not_to include(user)
- described_class.new(source, 'user@example.com', :maintainer).execute
+ described_class.add_user(source, 'user@example.com', :maintainer)
expect(source.members.invite.pluck(:invite_email)).to include('user@example.com')
end
@@ -237,7 +237,7 @@ RSpec.shared_examples_for "member creation" do
it 'creates an invited member', :aggregate_failures do
email_starting_with_number = "#{user.id}_email@example.com"
- described_class.new(source, email_starting_with_number, :maintainer).execute
+ described_class.add_user(source, email_starting_with_number, :maintainer)
expect(source.members.invite.pluck(:invite_email)).to include(email_starting_with_number)
expect(source.users.reload).not_to include(user)
@@ -249,7 +249,7 @@ RSpec.shared_examples_for "member creation" do
it 'creates the member' do
expect(source.users).not_to include(user)
- described_class.new(source, user, :maintainer, current_user: admin).execute
+ described_class.add_user(source, user, :maintainer, current_user: admin)
expect(source.users.reload).to include(user)
end
@@ -263,7 +263,7 @@ RSpec.shared_examples_for "member creation" 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
+ described_class.add_user(source, user, :maintainer, current_user: admin)
expect(source.users.reload).to include(user)
expect(source.requesters.reload.exists?(user_id: user)).to be_falsy
@@ -275,7 +275,7 @@ RSpec.shared_examples_for "member creation" do
it 'does not create the member', :aggregate_failures do
expect(source.users).not_to include(user)
- member = described_class.new(source, user, :maintainer, current_user: user).execute
+ member = described_class.add_user(source, user, :maintainer, current_user: user)
expect(source.users.reload).not_to include(user)
expect(member).not_to be_persisted
@@ -290,7 +290,7 @@ RSpec.shared_examples_for "member creation" do
expect(source.users).not_to include(user)
expect(source.requesters.exists?(user_id: user)).to be_truthy
- described_class.new(source, user, :maintainer, current_user: user).execute
+ described_class.add_user(source, user, :maintainer, current_user: user)
expect(source.users.reload).not_to include(user)
expect(source.requesters.exists?(user_id: user)).to be_truthy
@@ -307,7 +307,7 @@ RSpec.shared_examples_for "member creation" do
it 'updates the member' do
expect(source.users).to include(user)
- described_class.new(source, user, :maintainer).execute
+ described_class.add_user(source, user, :maintainer)
expect(source.members.find_by(user_id: user).access_level).to eq(Gitlab::Access::MAINTAINER)
end
@@ -317,7 +317,7 @@ RSpec.shared_examples_for "member creation" do
it 'updates the member' do
expect(source.users).to include(user)
- described_class.new(source, user, :maintainer, current_user: admin).execute
+ described_class.add_user(source, user, :maintainer, current_user: admin)
expect(source.members.find_by(user_id: user).access_level).to eq(Gitlab::Access::MAINTAINER)
end
@@ -327,221 +327,194 @@ RSpec.shared_examples_for "member creation" do
it 'does not update the member' do
expect(source.users).to include(user)
- described_class.new(source, user, :maintainer, current_user: user).execute
+ described_class.add_user(source, user, :maintainer, current_user: user)
expect(source.members.find_by(user_id: user).access_level).to eq(Gitlab::Access::DEVELOPER)
end
end
end
+end
- 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 }
-
- 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
+RSpec.shared_examples_for "bulk member creation" do
+ let_it_be(:admin) { create(:admin) }
+ let_it_be(:user1) { create(:user) }
+ let_it_be(:user2) { create(:user) }
- member = source.members.last
+ context 'when current user does not have permission' do
+ it 'does not succeed' do
+ # maintainers cannot add owners
+ source.add_maintainer(user)
- expect(member.tasks_to_be_done).to match_array([:ci, :code])
- expect(member.member_task.project).to eq(task_project)
+ expect(described_class.add_users(source, [user1, user2], :owner, current_user: user)).to be_empty
end
+ 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)
+ it 'returns Member objects' do
+ members = described_class.add_users(source, [user1, user2], :maintainer)
- member.reset
- expect(member.tasks_to_be_done).to match_array([:code, :ci])
- expect(member.member_task.project).to eq(task_project)
- end
+ expect(members.map(&:user)).to contain_exactly(user1, user2)
+ expect(members).to all(be_a(member_type))
+ expect(members).to all(be_persisted)
+ 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)
+ it 'returns an empty array' do
+ members = described_class.add_users(source, [], :maintainer)
- 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
+ expect(members).to be_a Array
+ expect(members).to be_empty
end
-end
-RSpec.shared_examples_for "bulk member creation" do
- let_it_be(:user) { create(:user) }
- let_it_be(:admin) { create(:admin) }
+ it 'supports different formats' do
+ list = ['joe@local.test', admin, user1.id, user2.id.to_s]
- describe '#execute' do
- it 'raises an error when exiting_members is not passed in the args hash' do
- expect do
- described_class.new(source, user, :maintainer, current_user: user).execute
- end.to raise_error(ArgumentError, 'existing_members must be included in the args hash')
- end
- end
+ members = described_class.add_users(source, list, :maintainer)
- describe '.add_users', :aggregate_failures do
- let_it_be(:user1) { create(:user) }
- let_it_be(:user2) { create(:user) }
+ expect(members.size).to eq(4)
+ expect(members.first).to be_invite
+ end
- it 'returns a Member objects' do
- members = described_class.add_users(source, [user1, user2], :maintainer)
+ context 'with de-duplication' do
+ it 'has the same user by id and user' do
+ members = described_class.add_users(source, [user1.id, user1, user1.id, user2, user2.id, user2], :maintainer)
expect(members.map(&:user)).to contain_exactly(user1, user2)
expect(members).to all(be_a(member_type))
expect(members).to all(be_persisted)
end
- it 'returns an empty array' do
- members = described_class.add_users(source, [], :maintainer)
+ it 'has the same user sent more than once' do
+ members = described_class.add_users(source, [user1, user1], :maintainer)
- expect(members).to be_a Array
- expect(members).to be_empty
+ expect(members.map(&:user)).to contain_exactly(user1)
+ expect(members).to all(be_a(member_type))
+ expect(members).to all(be_persisted)
end
+ end
- it 'supports different formats' do
- list = ['joe@local.test', admin, user1.id, user2.id.to_s]
+ it 'with the same user sent more than once by user and by email' do
+ members = described_class.add_users(source, [user1, user1.email], :maintainer)
- members = described_class.add_users(source, list, :maintainer)
+ expect(members.map(&:user)).to contain_exactly(user1)
+ expect(members).to all(be_a(member_type))
+ expect(members).to all(be_persisted)
+ end
- expect(members.size).to eq(4)
- expect(members.first).to be_invite
- end
+ it 'with the same user sent more than once by user id and by email' do
+ members = described_class.add_users(source, [user1.id, user1.email], :maintainer)
- context 'with de-duplication' do
- it 'has the same user by id and user' do
- members = described_class.add_users(source, [user1.id, user1, user1.id, user2, user2.id, user2], :maintainer)
+ expect(members.map(&:user)).to contain_exactly(user1)
+ expect(members).to all(be_a(member_type))
+ expect(members).to all(be_persisted)
+ end
+
+ context 'when a member already exists' do
+ before do
+ source.add_user(user1, :developer)
+ end
+ it 'has the same user sent more than once with the member already existing' do
+ expect do
+ members = described_class.add_users(source, [user1, user1, user2], :maintainer)
expect(members.map(&:user)).to contain_exactly(user1, user2)
expect(members).to all(be_a(member_type))
expect(members).to all(be_persisted)
- end
+ end.to change { Member.count }.by(1)
+ end
- it 'has the same user sent more than once' do
- members = described_class.add_users(source, [user1, user1], :maintainer)
+ it 'supports existing users as expected with user_ids passed' do
+ user3 = create(:user)
- expect(members.map(&:user)).to contain_exactly(user1)
+ expect do
+ members = described_class.add_users(source, [user1.id, user2, user3.id], :maintainer)
+ expect(members.map(&:user)).to contain_exactly(user1, user2, user3)
expect(members).to all(be_a(member_type))
expect(members).to all(be_persisted)
- end
+ end.to change { Member.count }.by(2)
end
- it 'with the same user sent more than once by user and by email' do
- members = described_class.add_users(source, [user1, user1.email], :maintainer)
+ it 'supports existing users as expected without user ids passed' do
+ user3 = create(:user)
- expect(members.map(&:user)).to contain_exactly(user1)
- expect(members).to all(be_a(member_type))
- expect(members).to all(be_persisted)
+ expect do
+ members = described_class.add_users(source, [user1, user2, user3], :maintainer)
+ expect(members.map(&:user)).to contain_exactly(user1, user2, user3)
+ expect(members).to all(be_a(member_type))
+ expect(members).to all(be_persisted)
+ end.to change { Member.count }.by(2)
end
+ end
- it 'with the same user sent more than once by user id and by email' do
- members = described_class.add_users(source, [user1.id, user1.email], :maintainer)
+ 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(members.map(&:user)).to contain_exactly(user1)
- expect(members).to all(be_a(member_type))
- expect(members).to all(be_persisted)
+ it 'creates a member_task with the correct attributes', :aggregate_failures do
+ members = described_class.add_users(source, [user1], :developer, tasks_to_be_done: %w(ci code), tasks_project_id: task_project.id)
+ member = members.last
+
+ expect(member.tasks_to_be_done).to match_array([:ci, :code])
+ expect(member.member_task.project).to eq(task_project)
end
- context 'when a member already exists' do
+ context 'with an already existing member' do
before do
source.add_user(user1, :developer)
end
- it 'has the same user sent more than once with the member already existing' do
- expect do
- members = described_class.add_users(source, [user1, user1, user2], :maintainer)
- expect(members.map(&:user)).to contain_exactly(user1, user2)
- expect(members).to all(be_a(member_type))
- expect(members).to all(be_persisted)
- end.to change { Member.count }.by(1)
- end
-
- it 'supports existing users as expected with user_ids passed' do
- user3 = create(:user)
+ it 'does not update tasks to be done if tasks already exist', :aggregate_failures do
+ member = source.members.find_by(user_id: user1.id)
+ create(:member_task, member: member, project: task_project, tasks_to_be_done: %w(code ci))
expect do
- members = described_class.add_users(source, [user1.id, user2, user3.id], :maintainer)
- expect(members.map(&:user)).to contain_exactly(user1, user2, user3)
- expect(members).to all(be_a(member_type))
- expect(members).to all(be_persisted)
- end.to change { Member.count }.by(2)
- end
+ described_class.add_users(source,
+ [user1.id],
+ :developer,
+ tasks_to_be_done: %w(issues),
+ tasks_project_id: task_project.id)
+ end.not_to change(MemberTask, :count)
- it 'supports existing users as expected without user ids passed' do
- user3 = create(:user)
+ 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
- members = described_class.add_users(source, [user1, user2, user3], :maintainer)
- expect(members.map(&:user)).to contain_exactly(user1, user2, user3)
- expect(members).to all(be_a(member_type))
- expect(members).to all(be_persisted)
- end.to change { Member.count }.by(2)
+ described_class.add_users(source,
+ [user1.id],
+ :developer,
+ tasks_to_be_done: %w(issues),
+ tasks_project_id: task_project.id)
+ end.to change(MemberTask, :count).by(1)
+
+ member = source.members.find_by(user_id: user1.id)
+ expect(member.tasks_to_be_done).to match_array([:issues])
+ expect(member.member_task.project).to eq(task_project)
end
end
+ end
+end
+
+RSpec.shared_examples 'owner management' do
+ describe '.cannot_manage_owners?' do
+ subject { described_class.cannot_manage_owners?(source, 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 }
+ context 'when maintainer' do
+ before do
+ source.add_maintainer(user)
+ end
- it 'creates a member_task with the correct attributes', :aggregate_failures do
- members = described_class.add_users(source, [user1], :developer, tasks_to_be_done: %w(ci code), tasks_project_id: task_project.id)
- member = members.last
+ it 'cannot manage owners' do
+ expect(subject).to be_truthy
+ end
+ end
- expect(member.tasks_to_be_done).to match_array([:ci, :code])
- expect(member.member_task.project).to eq(task_project)
+ context 'when owner' do
+ before do
+ source.add_owner(user)
end
- context 'with an already existing member' do
- before do
- source.add_user(user1, :developer)
- end
-
- it 'does not update tasks to be done if tasks already exist', :aggregate_failures do
- member = source.members.find_by(user_id: user1.id)
- create(:member_task, member: member, project: task_project, tasks_to_be_done: %w(code ci))
-
- expect do
- described_class.add_users(source,
- [user1.id],
- :developer,
- tasks_to_be_done: %w(issues),
- tasks_project_id: task_project.id)
- 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.add_users(source,
- [user1.id],
- :developer,
- tasks_to_be_done: %w(issues),
- tasks_project_id: task_project.id)
- end.to change(MemberTask, :count).by(1)
-
- member = source.members.find_by(user_id: user1.id)
- expect(member.tasks_to_be_done).to match_array([:issues])
- expect(member.member_task.project).to eq(task_project)
- end
+ it 'can manage owners' do
+ expect(subject).to be_falsey
end
end
end
diff --git a/spec/support/shared_examples/models/members_notifications_shared_example.rb b/spec/support/shared_examples/models/members_notifications_shared_example.rb
index 04af3935d15..75eed0203a7 100644
--- a/spec/support/shared_examples/models/members_notifications_shared_example.rb
+++ b/spec/support/shared_examples/models/members_notifications_shared_example.rb
@@ -33,6 +33,18 @@ RSpec.shared_examples 'members notifications' do |entity_type|
end
end
+ describe '#after_commit' do
+ context 'on creation of a member requesting access' do
+ let(:member) { build(:"#{entity_type}_member", :access_request) }
+
+ it "calls NotificationService.new_access_request" do
+ expect(notification_service).to receive(:new_access_request).with(member)
+
+ member.save!
+ end
+ end
+ end
+
describe '#accept_request' do
let(:member) { create(:"#{entity_type}_member", :access_request) }
diff --git a/spec/support/shared_examples/models/wiki_shared_examples.rb b/spec/support/shared_examples/models/wiki_shared_examples.rb
index 6f17231a040..604c57768fe 100644
--- a/spec/support/shared_examples/models/wiki_shared_examples.rb
+++ b/spec/support/shared_examples/models/wiki_shared_examples.rb
@@ -540,14 +540,6 @@ RSpec.shared_examples 'wiki model' do
end
end
end
-
- context 'when feature flag :gitaly_replace_wiki_create_page is disabled' do
- before do
- stub_feature_flags(gitaly_replace_wiki_create_page: false)
- end
-
- it_behaves_like 'create_page tests'
- end
end
describe '#update_page' do
diff --git a/spec/support/shared_examples/namespaces/traversal_scope_examples.rb b/spec/support/shared_examples/namespaces/traversal_scope_examples.rb
index f1ace9878e9..45da1d382c1 100644
--- a/spec/support/shared_examples/namespaces/traversal_scope_examples.rb
+++ b/spec/support/shared_examples/namespaces/traversal_scope_examples.rb
@@ -238,6 +238,12 @@ RSpec.shared_examples 'namespace traversal scopes' do
subject { described_class.where(id: [nested_group_1, nested_group_2]).self_and_descendants(include_self: false) }
it { is_expected.to contain_exactly(deep_nested_group_1, deep_nested_group_2) }
+
+ context 'with duplicate descendants' do
+ subject { described_class.where(id: [group_1, nested_group_1]).self_and_descendants(include_self: false) }
+
+ it { is_expected.to contain_exactly(nested_group_1, deep_nested_group_1) }
+ end
end
context 'with offset and limit' do
@@ -267,6 +273,14 @@ RSpec.shared_examples 'namespace traversal scopes' do
include_examples '.self_and_descendants'
end
+
+ context 'with linear_scopes_superset feature flag disabled' do
+ before do
+ stub_feature_flags(linear_scopes_superset: false)
+ end
+
+ include_examples '.self_and_descendants'
+ end
end
shared_examples '.self_and_descendant_ids' do
@@ -310,6 +324,14 @@ RSpec.shared_examples 'namespace traversal scopes' do
include_examples '.self_and_descendant_ids'
end
+
+ context 'with linear_scopes_superset feature flag disabled' do
+ before do
+ stub_feature_flags(linear_scopes_superset: false)
+ end
+
+ include_examples '.self_and_descendant_ids'
+ end
end
shared_examples '.self_and_hierarchy' do
diff --git a/spec/support/shared_examples/requests/api/project_statistics_refresh_conflicts_shared_examples.rb b/spec/support/shared_examples/requests/api/project_statistics_refresh_conflicts_shared_examples.rb
new file mode 100644
index 00000000000..7c3f4781472
--- /dev/null
+++ b/spec/support/shared_examples/requests/api/project_statistics_refresh_conflicts_shared_examples.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'preventing request because of ongoing project stats refresh' do |entrypoint|
+ before do
+ create(:project_build_artifacts_size_refresh, :pending, project: project)
+ end
+
+ it 'logs about the rejected request' do
+ expect(Gitlab::ProjectStatsRefreshConflictsLogger)
+ .to receive(:warn_request_rejected_during_stats_refresh)
+ .with(project.id)
+
+ make_request
+ end
+
+ it 'returns 409 error' do
+ make_request
+
+ expect(response).to have_gitlab_http_status(:conflict)
+ end
+end
diff --git a/spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb
index aff086d1ba3..795545e4ad1 100644
--- a/spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb
@@ -124,6 +124,23 @@ RSpec.shared_examples 'PyPI package versions' do |user_type, status, add_member
end
end
+RSpec.shared_examples 'PyPI package index' do |user_type, status, add_member = true|
+ context "for user type #{user_type}" do
+ before do
+ project.send("add_#{user_type}", user) if add_member && user_type != :anonymous
+ group.send("add_#{user_type}", user) if add_member && user_type != :anonymous
+ end
+
+ it 'returns the package index' do
+ subject
+
+ expect(response.body).to match(package.name)
+ end
+
+ it_behaves_like 'returning response status', status
+ end
+end
+
RSpec.shared_examples 'PyPI package download' do |user_type, status, add_member = true|
context "for user type #{user_type}" do
before do
@@ -259,6 +276,45 @@ RSpec.shared_examples 'pypi simple API endpoint' do
end
end
+RSpec.shared_examples 'pypi simple index API endpoint' do
+ using RSpec::Parameterized::TableSyntax
+
+ context 'with valid project' do
+ where(:visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do
+ :public | :developer | true | true | 'PyPI package index' | :success
+ :public | :guest | true | true | 'PyPI package index' | :success
+ :public | :developer | true | false | 'PyPI package index' | :success
+ :public | :guest | true | false | 'PyPI package index' | :success
+ :public | :developer | false | true | 'PyPI package index' | :success
+ :public | :guest | false | true | 'PyPI package index' | :success
+ :public | :developer | false | false | 'PyPI package index' | :success
+ :public | :guest | false | false | 'PyPI package index' | :success
+ :public | :anonymous | false | true | 'PyPI package index' | :success
+ :private | :developer | true | true | 'PyPI package index' | :success
+ :private | :guest | true | true | 'process PyPI api request' | :forbidden
+ :private | :developer | true | false | 'process PyPI api request' | :unauthorized
+ :private | :guest | true | false | 'process PyPI api request' | :unauthorized
+ :private | :developer | false | true | 'process PyPI api request' | :not_found
+ :private | :guest | false | true | 'process PyPI api request' | :not_found
+ :private | :developer | false | false | 'process PyPI api request' | :unauthorized
+ :private | :guest | false | false | 'process PyPI api request' | :unauthorized
+ :private | :anonymous | false | true | 'process PyPI api request' | :unauthorized
+ end
+
+ with_them do
+ let(:token) { user_token ? personal_access_token.token : 'wrong' }
+ let(:headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) }
+
+ before do
+ project.update_column(:visibility_level, Gitlab::VisibilityLevel.level_value(visibility_level.to_s))
+ group.update_column(:visibility_level, Gitlab::VisibilityLevel.level_value(visibility_level.to_s))
+ end
+
+ it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member]
+ end
+ end
+end
+
RSpec.shared_examples 'pypi file download endpoint' do
using RSpec::Parameterized::TableSyntax
diff --git a/spec/support/shared_examples/requests/projects/environments_controller_spec_shared_examples.rb b/spec/support/shared_examples/requests/projects/environments_controller_spec_shared_examples.rb
new file mode 100644
index 00000000000..31218b104bd
--- /dev/null
+++ b/spec/support/shared_examples/requests/projects/environments_controller_spec_shared_examples.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'avoids N+1 queries on environment detail page' do
+ it 'avoids N+1 queries', :use_sql_query_cache do
+ create_deployment_with_associations(commit_depth: 19)
+
+ control = ActiveRecord::QueryRecorder.new(skip_cached: false) do
+ get project_environment_path(project, environment), params: environment_params
+ end
+
+ 18.downto(0).each { |n| create_deployment_with_associations(commit_depth: n) }
+
+ # N+1s exist for loading commit emails and users
+ expect do
+ get project_environment_path(project, environment), params: environment_params
+ end.not_to exceed_all_query_limit(control).with_threshold(9)
+ end
+end
diff --git a/spec/support/shared_examples/serializers/environment_serializer_shared_examples.rb b/spec/support/shared_examples/serializers/environment_serializer_shared_examples.rb
index e1baa594f3c..6d59943d91c 100644
--- a/spec/support/shared_examples/serializers/environment_serializer_shared_examples.rb
+++ b/spec/support/shared_examples/serializers/environment_serializer_shared_examples.rb
@@ -8,9 +8,8 @@ RSpec.shared_examples 'avoid N+1 on environments serialization' do
create_environment_with_associations(project)
create_environment_with_associations(project)
- # Fix N+1 queries introduced by multi stop_actions for environment.
- # Tracked in https://gitlab.com/gitlab-org/gitlab/-/issues/358780
- relax_count = 14
+ # See issue: https://gitlab.com/gitlab-org/gitlab/-/issues/363317
+ relax_count = 1
expect { serialize(grouping: true) }.not_to exceed_query_limit(control.count + relax_count)
end
@@ -23,9 +22,8 @@ RSpec.shared_examples 'avoid N+1 on environments serialization' do
create_environment_with_associations(project)
create_environment_with_associations(project)
- # Fix N+1 queries introduced by multi stop_actions for environment.
- # Tracked in https://gitlab.com/gitlab-org/gitlab/-/issues/358780
- relax_count = 14
+ # See issue: https://gitlab.com/gitlab-org/gitlab/-/issues/363317
+ relax_count = 1
expect { serialize(grouping: false) }.not_to exceed_query_limit(control.count + relax_count)
end
diff --git a/spec/support/shared_examples/services/alert_management_shared_examples.rb b/spec/support/shared_examples/services/alert_management_shared_examples.rb
index 23aee912d2d..f644f1a1687 100644
--- a/spec/support/shared_examples/services/alert_management_shared_examples.rb
+++ b/spec/support/shared_examples/services/alert_management_shared_examples.rb
@@ -32,7 +32,7 @@ RSpec.shared_context 'incident management settings enabled' do
end
before do
- allow(ProjectServiceWorker).to receive(:perform_async)
+ allow(Integrations::ExecuteWorker).to receive(:perform_async)
allow(service)
.to receive(:incident_management_setting)
.and_return(incident_management_setting)
diff --git a/spec/support/shared_examples/services/boards/items_list_service_shared_examples.rb b/spec/support/shared_examples/services/boards/items_list_service_shared_examples.rb
index 9a3a0cc9cc8..ed05a150f8b 100644
--- a/spec/support/shared_examples/services/boards/items_list_service_shared_examples.rb
+++ b/spec/support/shared_examples/services/boards/items_list_service_shared_examples.rb
@@ -3,17 +3,17 @@
RSpec.shared_examples 'items list service' do
it 'avoids N+1' do
params = { board_id: board.id }
- control = ActiveRecord::QueryRecorder.new { described_class.new(parent, user, params).execute }
+ control = ActiveRecord::QueryRecorder.new { list_service(params).execute }
new_list
- expect { described_class.new(parent, user, params).execute }.not_to exceed_query_limit(control)
+ expect { list_service(params).execute }.not_to exceed_query_limit(control)
end
- it 'returns opened items when list_id is missing' do
+ it 'returns opened items when list_id and list are missing' do
params = { board_id: board.id }
- items = described_class.new(parent, user, params).execute
+ items = list_service(params).execute
expect(items).to match_array(backlog_items)
end
@@ -21,7 +21,7 @@ RSpec.shared_examples 'items list service' do
it 'returns opened items when listing items from Backlog' do
params = { board_id: board.id, id: backlog.id }
- items = described_class.new(parent, user, params).execute
+ items = list_service(params).execute
expect(items).to match_array(backlog_items)
end
@@ -29,7 +29,7 @@ RSpec.shared_examples 'items list service' do
it 'returns opened items that have label list applied when listing items from a label list' do
params = { board_id: board.id, id: list1.id }
- items = described_class.new(parent, user, params).execute
+ items = list_service(params).execute
expect(items).to match_array(list1_items)
end
@@ -37,20 +37,24 @@ RSpec.shared_examples 'items list service' do
it 'returns closed items when listing items from Closed sorted by closed_at in descending order' do
params = { board_id: board.id, id: closed.id }
- items = described_class.new(parent, user, params).execute
+ items = list_service(params).execute
expect(items).to eq(closed_items)
end
it 'raises an error if the list does not belong to the board' do
list = create(list_factory) # rubocop:disable Rails/SaveBang
- service = described_class.new(parent, user, board_id: board.id, id: list.id)
+ params = { board_id: board.id, id: list.id }
+
+ service = list_service(params)
expect { service.execute }.to raise_error(ActiveRecord::RecordNotFound)
end
- it 'raises an error if list id is invalid' do
- service = described_class.new(parent, user, board_id: board.id, id: nil)
+ it 'raises an error if list and list id are invalid or missing' do
+ params = { board_id: board.id, id: nil, list: nil }
+
+ service = list_service(params)
expect { service.execute }.to raise_error(ActiveRecord::RecordNotFound)
end
@@ -58,8 +62,22 @@ RSpec.shared_examples 'items list service' do
it 'returns items from all lists if :all_list is used' do
params = { board_id: board.id, all_lists: true }
- items = described_class.new(parent, user, params).execute
+ items = list_service(params).execute
expect(items).to match_array(all_items)
end
+
+ it 'returns opened items that have label list applied when using list param' do
+ params = { board_id: board.id, list: list1 }
+
+ items = list_service(params).execute
+
+ expect(items).to match_array(list1_items)
+ end
+
+ def list_service(params)
+ args = [parent, user].push(params)
+
+ described_class.new(*args)
+ end
end
diff --git a/spec/support/shared_examples/views/pagination_shared_examples.rb b/spec/support/shared_examples/views/pagination_shared_examples.rb
new file mode 100644
index 00000000000..3932f320859
--- /dev/null
+++ b/spec/support/shared_examples/views/pagination_shared_examples.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'correct pagination' do
+ it 'paginates correctly to page 3 and back' do
+ expect(page).to have_selector(item_selector, count: per_page)
+ page1_item_text = page.find(item_selector).text
+ click_next_page(next_button_selector)
+
+ expect(page).to have_selector(item_selector, count: per_page)
+ page2_item_text = page.find(item_selector).text
+ click_next_page(next_button_selector)
+
+ expect(page).to have_selector(item_selector, count: per_page)
+ page3_item_text = page.find(item_selector).text
+ click_prev_page(prev_button_selector)
+
+ expect(page3_item_text).not_to eql(page2_item_text)
+ expect(page.find(item_selector).text).to eql(page2_item_text)
+
+ click_prev_page(prev_button_selector)
+
+ expect(page.find(item_selector).text).to eql(page1_item_text)
+ expect(page).to have_selector(item_selector, count: per_page)
+ end
+
+ def click_next_page(next_button_selector)
+ page.find(next_button_selector).click
+ wait_for_requests
+ end
+
+ def click_prev_page(prev_button_selector)
+ page.find(prev_button_selector).click
+ wait_for_requests
+ end
+end
diff --git a/spec/support/shared_examples/workers/background_migration_worker_shared_examples.rb b/spec/support/shared_examples/workers/background_migration_worker_shared_examples.rb
index 7fdf049a823..8ecb04bfdd6 100644
--- a/spec/support/shared_examples/workers/background_migration_worker_shared_examples.rb
+++ b/spec/support/shared_examples/workers/background_migration_worker_shared_examples.rb
@@ -42,159 +42,195 @@ RSpec.shared_examples 'it runs background migration jobs' do |tracking_database|
describe '#perform' do
let(:worker) { described_class.new }
- before do
- allow(worker).to receive(:jid).and_return(1)
- allow(worker).to receive(:always_perform?).and_return(false)
+ context 'when execute_background_migrations feature flag is disabled' do
+ before do
+ stub_feature_flags(execute_background_migrations: false)
+ end
- allow(Postgresql::ReplicationSlot).to receive(:lag_too_great?).and_return(false)
- end
+ it 'does not perform the job, reschedules it in the future, and logs a message' do
+ expect(worker).not_to receive(:perform_with_connection)
- it 'performs jobs using the coordinator for the worker' do
- expect_next_instance_of(Gitlab::BackgroundMigration::JobCoordinator) do |coordinator|
- allow(coordinator).to receive(:with_shared_connection).and_yield
+ expect(Sidekiq.logger).to receive(:info) do |payload|
+ expect(payload[:class]).to eq(described_class.name)
+ expect(payload[:database]).to eq(tracking_database)
+ expect(payload[:message]).to match(/skipping execution, migration rescheduled/)
+ end
- expect(coordinator.worker_class).to eq(described_class)
- expect(coordinator).to receive(:perform).with('Foo', [10, 20])
- end
+ lease_attempts = 3
+ delay = described_class::BACKGROUND_MIGRATIONS_DELAY
+ job_args = [10, 20]
- worker.perform('Foo', [10, 20])
- end
+ freeze_time do
+ worker.perform('Foo', job_args, lease_attempts)
- context 'when lease can be obtained' do
- let(:coordinator) { double('job coordinator') }
+ job = described_class.jobs.find { |job| job['args'] == ['Foo', job_args, lease_attempts] }
+ expect(job).to be, "Expected the job to be rescheduled with (#{job_args}, #{lease_attempts}), but it was not."
+ expected_time = delay.to_i + Time.now.to_i
+ expect(job['at']).to eq(expected_time),
+ "Expected the job to be rescheduled in #{expected_time} seconds, " \
+ "but it was rescheduled in #{job['at']} seconds."
+ end
+ end
+ end
+
+ context 'when execute_background_migrations feature flag is enabled' do
before do
- allow(Gitlab::BackgroundMigration).to receive(:coordinator_for_database)
- .with(tracking_database)
- .and_return(coordinator)
+ stub_feature_flags(execute_background_migrations: true)
- allow(coordinator).to receive(:with_shared_connection).and_yield
+ allow(worker).to receive(:jid).and_return(1)
+ allow(worker).to receive(:always_perform?).and_return(false)
+
+ allow(Postgresql::ReplicationSlot).to receive(:lag_too_great?).and_return(false)
end
- it 'sets up the shared connection before checking replication' do
- expect(coordinator).to receive(:with_shared_connection).and_yield.ordered
- expect(Postgresql::ReplicationSlot).to receive(:lag_too_great?).and_return(false).ordered
+ it 'performs jobs using the coordinator for the worker' do
+ expect_next_instance_of(Gitlab::BackgroundMigration::JobCoordinator) do |coordinator|
+ allow(coordinator).to receive(:with_shared_connection).and_yield
- expect(coordinator).to receive(:perform).with('Foo', [10, 20])
+ expect(coordinator.worker_class).to eq(described_class)
+ expect(coordinator).to receive(:perform).with('Foo', [10, 20])
+ end
worker.perform('Foo', [10, 20])
end
- it 'performs a background migration' do
- expect(coordinator).to receive(:perform).with('Foo', [10, 20])
+ context 'when lease can be obtained' do
+ let(:coordinator) { double('job coordinator') }
- worker.perform('Foo', [10, 20])
- end
+ before do
+ allow(Gitlab::BackgroundMigration).to receive(:coordinator_for_database)
+ .with(tracking_database)
+ .and_return(coordinator)
+
+ allow(coordinator).to receive(:with_shared_connection).and_yield
+ end
+
+ it 'sets up the shared connection before checking replication' do
+ expect(coordinator).to receive(:with_shared_connection).and_yield.ordered
+ expect(Postgresql::ReplicationSlot).to receive(:lag_too_great?).and_return(false).ordered
- context 'when lease_attempts is 1' do
- it 'performs a background migration' do
expect(coordinator).to receive(:perform).with('Foo', [10, 20])
- worker.perform('Foo', [10, 20], 1)
+ worker.perform('Foo', [10, 20])
end
- end
- it 'can run scheduled job and retried job concurrently' do
- expect(coordinator)
- .to receive(:perform)
- .with('Foo', [10, 20])
- .exactly(2).time
-
- worker.perform('Foo', [10, 20])
- worker.perform('Foo', [10, 20], described_class::MAX_LEASE_ATTEMPTS - 1)
- end
+ it 'performs a background migration' do
+ expect(coordinator).to receive(:perform).with('Foo', [10, 20])
- it 'sets the class that will be executed as the caller_id' do
- expect(coordinator).to receive(:perform) do
- expect(Gitlab::ApplicationContext.current).to include('meta.caller_id' => 'Foo')
+ worker.perform('Foo', [10, 20])
end
- worker.perform('Foo', [10, 20])
- end
- end
+ context 'when lease_attempts is 1' do
+ it 'performs a background migration' do
+ expect(coordinator).to receive(:perform).with('Foo', [10, 20])
- context 'when lease not obtained (migration of same class was performed recently)' do
- let(:timeout) { described_class.minimum_interval }
- let(:lease_key) { "#{described_class.name}:Foo" }
- let(:coordinator) { double('job coordinator') }
+ worker.perform('Foo', [10, 20], 1)
+ end
+ end
- before do
- allow(Gitlab::BackgroundMigration).to receive(:coordinator_for_database)
- .with(tracking_database)
- .and_return(coordinator)
+ it 'can run scheduled job and retried job concurrently' do
+ expect(coordinator)
+ .to receive(:perform)
+ .with('Foo', [10, 20])
+ .exactly(2).time
- allow(coordinator).to receive(:with_shared_connection).and_yield
+ worker.perform('Foo', [10, 20])
+ worker.perform('Foo', [10, 20], described_class::MAX_LEASE_ATTEMPTS - 1)
+ end
- expect(coordinator).not_to receive(:perform)
+ it 'sets the class that will be executed as the caller_id' do
+ expect(coordinator).to receive(:perform) do
+ expect(Gitlab::ApplicationContext.current).to include('meta.caller_id' => 'Foo')
+ end
- Gitlab::ExclusiveLease.new(lease_key, timeout: timeout).try_obtain
+ worker.perform('Foo', [10, 20])
+ end
end
- it 'reschedules the migration and decrements the lease_attempts' do
- expect(described_class)
- .to receive(:perform_in)
- .with(a_kind_of(Numeric), 'Foo', [10, 20], 4)
+ context 'when lease not obtained (migration of same class was performed recently)' do
+ let(:timeout) { described_class.minimum_interval }
+ let(:lease_key) { "#{described_class.name}:Foo" }
+ let(:coordinator) { double('job coordinator') }
- worker.perform('Foo', [10, 20], 5)
- end
+ before do
+ allow(Gitlab::BackgroundMigration).to receive(:coordinator_for_database)
+ .with(tracking_database)
+ .and_return(coordinator)
- context 'when lease_attempts is 1' do
- let(:lease_key) { "#{described_class.name}:Foo:retried" }
+ allow(coordinator).to receive(:with_shared_connection).and_yield
+
+ expect(coordinator).not_to receive(:perform)
+
+ Gitlab::ExclusiveLease.new(lease_key, timeout: timeout).try_obtain
+ end
it 'reschedules the migration and decrements the lease_attempts' do
expect(described_class)
.to receive(:perform_in)
- .with(a_kind_of(Numeric), 'Foo', [10, 20], 0)
+ .with(a_kind_of(Numeric), 'Foo', [10, 20], 4)
- worker.perform('Foo', [10, 20], 1)
+ worker.perform('Foo', [10, 20], 5)
end
- end
- context 'when lease_attempts is 0' do
- let(:lease_key) { "#{described_class.name}:Foo:retried" }
+ context 'when lease_attempts is 1' do
+ let(:lease_key) { "#{described_class.name}:Foo:retried" }
- it 'gives up performing the migration' do
- expect(described_class).not_to receive(:perform_in)
- expect(Sidekiq.logger).to receive(:warn).with(
- class: 'Foo',
- message: 'Job could not get an exclusive lease after several tries. Giving up.',
- job_id: 1)
+ it 'reschedules the migration and decrements the lease_attempts' do
+ expect(described_class)
+ .to receive(:perform_in)
+ .with(a_kind_of(Numeric), 'Foo', [10, 20], 0)
- worker.perform('Foo', [10, 20], 0)
+ worker.perform('Foo', [10, 20], 1)
+ end
end
- end
- end
- context 'when database is not healthy' do
- before do
- expect(Postgresql::ReplicationSlot).to receive(:lag_too_great?).and_return(true)
- end
+ context 'when lease_attempts is 0' do
+ let(:lease_key) { "#{described_class.name}:Foo:retried" }
- it 'reschedules a migration if the database is not healthy' do
- expect(described_class)
- .to receive(:perform_in)
- .with(a_kind_of(Numeric), 'Foo', [10, 20], 4)
+ it 'gives up performing the migration' do
+ expect(described_class).not_to receive(:perform_in)
+ expect(Sidekiq.logger).to receive(:warn).with(
+ class: 'Foo',
+ message: 'Job could not get an exclusive lease after several tries. Giving up.',
+ job_id: 1)
- worker.perform('Foo', [10, 20])
+ worker.perform('Foo', [10, 20], 0)
+ end
+ end
end
- it 'increments the unhealthy counter' do
- counter = Gitlab::Metrics.counter(:background_migration_database_health_reschedules, 'msg')
+ context 'when database is not healthy' do
+ before do
+ expect(Postgresql::ReplicationSlot).to receive(:lag_too_great?).and_return(true)
+ end
- expect(described_class).to receive(:perform_in)
+ it 'reschedules a migration if the database is not healthy' do
+ expect(described_class)
+ .to receive(:perform_in)
+ .with(a_kind_of(Numeric), 'Foo', [10, 20], 4)
- expect { worker.perform('Foo', [10, 20]) }.to change { counter.get(db_config_name: tracking_database) }.by(1)
- end
+ worker.perform('Foo', [10, 20])
+ end
+
+ it 'increments the unhealthy counter' do
+ counter = Gitlab::Metrics.counter(:background_migration_database_health_reschedules, 'msg')
+
+ expect(described_class).to receive(:perform_in)
+
+ expect { worker.perform('Foo', [10, 20]) }.to change { counter.get(db_config_name: tracking_database) }.by(1)
+ end
- context 'when lease_attempts is 0' do
- it 'gives up performing the migration' do
- expect(described_class).not_to receive(:perform_in)
- expect(Sidekiq.logger).to receive(:warn).with(
- class: 'Foo',
- message: 'Database was unhealthy after several tries. Giving up.',
- job_id: 1)
+ context 'when lease_attempts is 0' do
+ it 'gives up performing the migration' do
+ expect(described_class).not_to receive(:perform_in)
+ expect(Sidekiq.logger).to receive(:warn).with(
+ class: 'Foo',
+ message: 'Database was unhealthy after several tries. Giving up.',
+ job_id: 1)
- worker.perform('Foo', [10, 20], 0)
+ worker.perform('Foo', [10, 20], 0)
+ end
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 3d4e840fe2d..54962eac100 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
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-RSpec.shared_examples 'it runs batched background migration jobs' do |tracking_database, feature_flag:|
+RSpec.shared_examples 'it runs batched background migration jobs' do |tracking_database|
include ExclusiveLeaseHelpers
describe 'defining the job attributes' do
@@ -40,13 +40,17 @@ RSpec.shared_examples 'it runs batched background migration jobs' do |tracking_d
end
describe '.enabled?' do
- it 'does not raise an error' do
- expect { described_class.enabled? }.not_to raise_error
- end
+ it 'returns true when execute_batched_migrations_on_schedule feature flag is enabled' do
+ stub_feature_flags(execute_batched_migrations_on_schedule: true)
- it 'returns true' do
expect(described_class.enabled?).to be_truthy
end
+
+ it 'returns false when execute_batched_migrations_on_schedule feature flag is disabled' do
+ stub_feature_flags(execute_batched_migrations_on_schedule: false)
+
+ expect(described_class.enabled?).to be_falsey
+ end
end
describe '#perform' do
@@ -86,7 +90,7 @@ RSpec.shared_examples 'it runs batched background migration jobs' do |tracking_d
context 'when the feature flag is disabled' do
before do
- stub_feature_flags(feature_flag => false)
+ stub_feature_flags(execute_batched_migrations_on_schedule: false)
end
it 'does nothing' do
@@ -98,10 +102,26 @@ RSpec.shared_examples 'it runs batched background migration jobs' do |tracking_d
end
context 'when the feature flag is enabled' do
+ let(:base_model) { Gitlab::Database.database_base_models[tracking_database] }
+
before do
- stub_feature_flags(feature_flag => true)
+ stub_feature_flags(execute_batched_migrations_on_schedule: true)
- allow(Gitlab::Database::BackgroundMigration::BatchedMigration).to receive(:active_migration).and_return(nil)
+ allow(Gitlab::Database::BackgroundMigration::BatchedMigration).to receive(:active_migration)
+ .with(connection: base_model.connection)
+ .and_return(nil)
+ end
+
+ context 'when database config is shared' do
+ it 'does nothing' do
+ expect(Gitlab::Database).to receive(:db_config_share_with)
+ .with(base_model.connection_db_config).and_return('main')
+
+ expect(worker).not_to receive(:active_migration)
+ expect(worker).not_to receive(:run_active_migration)
+
+ worker.perform
+ end
end
context 'when no active migrations exist' do
@@ -121,6 +141,7 @@ RSpec.shared_examples 'it runs batched background migration jobs' do |tracking_d
before do
allow(Gitlab::Database::BackgroundMigration::BatchedMigration).to receive(:active_migration)
+ .with(connection: base_model.connection)
.and_return(migration)
allow(migration).to receive(:interval_elapsed?).with(variance: interval_variance).and_return(true)
@@ -222,6 +243,7 @@ RSpec.shared_examples 'it runs batched background migration jobs' do |tracking_d
end
end
+ let(:gitlab_schema) { "gitlab_#{tracking_database}" }
let!(:migration) do
create(
:batched_background_migration,
@@ -232,10 +254,12 @@ RSpec.shared_examples 'it runs batched background migration jobs' do |tracking_d
batch_size: batch_size,
sub_batch_size: sub_batch_size,
job_class_name: 'ExampleDataMigration',
- job_arguments: [1]
+ job_arguments: [1],
+ gitlab_schema: gitlab_schema
)
end
+ let(:base_model) { Gitlab::Database.database_base_models[tracking_database] }
let(:table_name) { 'example_data' }
let(:batch_size) { 5 }
let(:sub_batch_size) { 2 }
diff --git a/spec/support/shared_examples/workers/idempotency_shared_examples.rb b/spec/support/shared_examples/workers/idempotency_shared_examples.rb
index 9d9b371d61a..be43ea7d5f0 100644
--- a/spec/support/shared_examples/workers/idempotency_shared_examples.rb
+++ b/spec/support/shared_examples/workers/idempotency_shared_examples.rb
@@ -20,7 +20,11 @@ RSpec.shared_examples 'an idempotent worker' do
# Avoid stubbing calls for a more accurate run.
subject do
- defined?(job_args) ? perform_multiple(job_args) : perform_multiple
+ if described_class.include?(::Gitlab::EventStore::Subscriber)
+ event_worker
+ else
+ standard_worker
+ end
end
it 'is labeled as idempotent' do
@@ -30,4 +34,12 @@ RSpec.shared_examples 'an idempotent worker' do
it 'performs multiple times sequentially without raising an exception' do
expect { subject }.not_to raise_error
end
+
+ def event_worker
+ consume_event(subscriber: described_class, event: event)
+ end
+
+ def standard_worker
+ defined?(job_args) ? perform_multiple(job_args) : perform_multiple
+ end
end