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:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-12-17 14:59:07 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2020-12-17 14:59:07 +0300
commit8b573c94895dc0ac0e1d9d59cf3e8745e8b539ca (patch)
tree544930fb309b30317ae9797a9683768705d664c4 /spec/support
parent4b1de649d0168371549608993deac953eb692019 (diff)
Add latest changes from gitlab-org/gitlab@13-7-stable-eev13.7.0-rc42
Diffstat (limited to 'spec/support')
-rw-r--r--spec/support/atlassian/jira_connect/schemata.rb83
-rw-r--r--spec/support/caching.rb2
-rw-r--r--spec/support/gitlab_experiment.rb4
-rw-r--r--spec/support/gitlab_stubs/gitlab_ci_includes.yml19
-rw-r--r--spec/support/graphql/arguments.rb70
-rw-r--r--spec/support/graphql/field_inspection.rb35
-rw-r--r--spec/support/graphql/field_selection.rb73
-rw-r--r--spec/support/graphql/var.rb59
-rw-r--r--spec/support/helpers/after_next_helpers.rb53
-rw-r--r--spec/support/helpers/database_helpers.rb13
-rw-r--r--spec/support/helpers/dependency_proxy_helpers.rb18
-rw-r--r--spec/support/helpers/features/merge_request_helpers.rb25
-rw-r--r--spec/support/helpers/features/web_ide_spec_helpers.rb35
-rw-r--r--spec/support/helpers/file_read_helpers.rb48
-rw-r--r--spec/support/helpers/filtered_search_helpers.rb4
-rw-r--r--spec/support/helpers/git_http_helpers.rb34
-rw-r--r--spec/support/helpers/gitlab_verify_helpers.rb2
-rw-r--r--spec/support/helpers/gpg_helpers.rb139
-rw-r--r--spec/support/helpers/graphql_helpers.rb206
-rw-r--r--spec/support/helpers/login_helpers.rb16
-rw-r--r--spec/support/helpers/multipart_helpers.rb2
-rw-r--r--spec/support/helpers/next_instance_of.rb27
-rw-r--r--spec/support/helpers/services_helper.rb11
-rw-r--r--spec/support/helpers/snowplow_helpers.rb26
-rw-r--r--spec/support/helpers/stub_experiments.rb16
-rw-r--r--spec/support/helpers/stub_object_storage.rb17
-rw-r--r--spec/support/helpers/test_env.rb48
-rw-r--r--spec/support/helpers/usage_data_helpers.rb2
-rw-r--r--spec/support/helpers/workhorse_helpers.rb6
-rw-r--r--spec/support/import_export/common_util.rb5
-rw-r--r--spec/support/matchers/access_matchers.rb4
-rw-r--r--spec/support/matchers/be_valid_json.rb32
-rw-r--r--spec/support/matchers/exceed_query_limit.rb121
-rw-r--r--spec/support/services/issuable_import_csv_service_shared_examples.rb32
-rw-r--r--spec/support/shared_contexts/finders/group_projects_finder_shared_contexts.rb14
-rw-r--r--spec/support/shared_contexts/finders/merge_requests_finder_shared_contexts.rb6
-rw-r--r--spec/support/shared_contexts/issuable/merge_request_shared_context.rb44
-rw-r--r--spec/support/shared_contexts/lib/gitlab/middleware/with_a_mocked_gitlab_instance_shared_context.rb2
-rw-r--r--spec/support/shared_contexts/navbar_structure_context.rb26
-rw-r--r--spec/support/shared_contexts/policies/group_policy_shared_context.rb1
-rw-r--r--spec/support/shared_contexts/services_shared_context.rb5
-rw-r--r--spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb42
-rw-r--r--spec/support/shared_examples/controllers/repositories/git_http_controller_shared_examples.rb117
-rw-r--r--spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb68
-rw-r--r--spec/support/shared_examples/features/issuable_invite_members_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/features/master_manages_access_requests_shared_example.rb1
-rw-r--r--spec/support/shared_examples/features/reportable_note_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/features/wiki/user_git_access_wiki_page_shared_examples.rb27
-rw-r--r--spec/support/shared_examples/features/wiki/user_uses_wiki_shortcuts_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/graphql/connection_redaction_shared_examples.rb54
-rw-r--r--spec/support/shared_examples/graphql/connection_shared_examples.rb9
-rw-r--r--spec/support/shared_examples/graphql/design_fields_shared_examples.rb30
-rw-r--r--spec/support/shared_examples/graphql/jira_import/jira_import_resolver_shared_examples.rb15
-rw-r--r--spec/support/shared_examples/graphql/members_shared_examples.rb5
-rw-r--r--spec/support/shared_examples/graphql/mutation_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/graphql/mutations/boards_create_shared_examples.rb24
-rw-r--r--spec/support/shared_examples/graphql/mutations/spammable_mutation_fields_examples.rb7
-rw-r--r--spec/support/shared_examples/graphql/projects/services_resolver_shared_examples.rb6
-rw-r--r--spec/support/shared_examples/graphql/sorted_paginated_query_shared_examples.rb93
-rw-r--r--spec/support/shared_examples/graphql/types/gitlab_style_deprecations_shared_examples.rb6
-rw-r--r--spec/support/shared_examples/lib/gitlab/diff_file_collections_shared_examples.rb52
-rw-r--r--spec/support/shared_examples/lib/gitlab/import_export/import_failure_service_shared_examples.rb8
-rw-r--r--spec/support/shared_examples/lib/gitlab/middleware/read_only_gitlab_instance_shared_examples.rb3
-rw-r--r--spec/support/shared_examples/lib/gitlab/sidekiq_middleware/strategy_shared_examples.rb10
-rw-r--r--spec/support/shared_examples/models/concerns/can_move_repository_storage_shared_examples.rb55
-rw-r--r--spec/support/shared_examples/models/concerns/repository_storage_movable_shared_examples.rb115
-rw-r--r--spec/support/shared_examples/models/concerns/shardable_shared_examples.rb6
-rw-r--r--spec/support/shared_examples/models/mentionable_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/models/resource_timebox_event_shared_examples.rb10
-rw-r--r--spec/support/shared_examples/quick_actions/issue/clone_quick_action_shared_examples.rb187
-rw-r--r--spec/support/shared_examples/requests/api/conan_packages_shared_examples.rb16
-rw-r--r--spec/support/shared_examples/requests/api/graphql/group_and_project_boards_query_shared_examples.rb16
-rw-r--r--spec/support/shared_examples/requests/api/nuget_endpoints_shared_examples.rb265
-rw-r--r--spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb10
-rw-r--r--spec/support/shared_examples/requests/graphql_shared_examples.rb5
-rw-r--r--spec/support/shared_examples/requests/lfs_http_shared_examples.rb13
-rw-r--r--spec/support/shared_examples/requests/rack_attack_shared_examples.rb114
-rw-r--r--spec/support/shared_examples/requests/self_monitoring_shared_examples.rb12
-rw-r--r--spec/support/shared_examples/requests/uploads_auhorize_shared_examples.rb61
-rw-r--r--spec/support/shared_examples/routing/git_http_routing_shared_examples.rb43
-rw-r--r--spec/support/shared_examples/services/alert_management_shared_examples.rb32
-rw-r--r--spec/support/shared_examples/services/container_registry_auth_service_shared_examples.rb1006
-rw-r--r--spec/support/shared_examples/services/incident_shared_examples.rb6
-rw-r--r--spec/support/shared_examples/services/metrics/dashboard_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/services/packages_shared_examples.rb18
-rw-r--r--spec/support/shared_examples/workers/concerns/reenqueuer_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/workers/project_export_shared_examples.rb82
-rw-r--r--spec/support/snowplow.rb3
89 files changed, 3682 insertions, 379 deletions
diff --git a/spec/support/atlassian/jira_connect/schemata.rb b/spec/support/atlassian/jira_connect/schemata.rb
new file mode 100644
index 00000000000..91f8fe0bb41
--- /dev/null
+++ b/spec/support/atlassian/jira_connect/schemata.rb
@@ -0,0 +1,83 @@
+# frozen_string_literal: true
+
+module Atlassian
+ module Schemata
+ def self.build_info
+ {
+ 'type' => 'object',
+ 'required' => %w(schemaVersion pipelineId buildNumber updateSequenceNumber displayName url state issueKeys testInfo references),
+ 'properties' => {
+ 'schemaVersion' => { 'type' => 'string', 'pattern' => '1.0' },
+ 'pipelineId' => { 'type' => 'string' },
+ 'buildNumber' => { 'type' => 'integer' },
+ 'updateSequenceNumber' => { 'type' => 'integer' },
+ 'displayName' => { 'type' => 'string' },
+ 'url' => { 'type' => 'string' },
+ 'state' => {
+ 'type' => 'string',
+ 'pattern' => '(pending|in_progress|successful|failed|cancelled)'
+ },
+ 'issueKeys' => {
+ 'type' => 'array',
+ 'items' => { 'type' => 'string' },
+ 'minItems' => 1
+ },
+ 'testInfo' => {
+ 'type' => 'object',
+ 'required' => %w(totalNumber numberPassed numberFailed numberSkipped),
+ 'properties' => {
+ 'totalNumber' => { 'type' => 'integer' },
+ 'numberFailed' => { 'type' => 'integer' },
+ 'numberPassed' => { 'type' => 'integer' },
+ 'numberSkipped' => { 'type' => 'integer' }
+ }
+ },
+ 'references' => {
+ 'type' => 'array',
+ 'items' => {
+ 'type' => 'object',
+ 'required' => %w(commit ref),
+ 'properties' => {
+ 'commit' => {
+ 'type' => 'object',
+ 'required' => %w(id repositoryUri),
+ 'properties' => {
+ 'id' => { 'type' => 'string' },
+ 'repositoryUri' => { 'type' => 'string' }
+ }
+ },
+ 'ref' => {
+ 'type' => 'object',
+ 'required' => %w(name uri),
+ 'properties' => {
+ 'name' => { 'type' => 'string' },
+ 'uri' => { 'type' => 'string' }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ end
+
+ def self.build_info_payload
+ {
+ 'type' => 'object',
+ 'required' => %w(providerMetadata builds),
+ 'properties' => {
+ 'providerMetadata' => provider_metadata,
+ 'builds' => { 'type' => 'array', 'items' => build_info }
+ }
+ }
+ end
+
+ def self.provider_metadata
+ {
+ 'type' => 'object',
+ 'required' => %w(product),
+ 'properties' => { 'product' => { 'type' => 'string' } }
+ }
+ end
+ end
+end
diff --git a/spec/support/caching.rb b/spec/support/caching.rb
index 883d531550a..11e4f534971 100644
--- a/spec/support/caching.rb
+++ b/spec/support/caching.rb
@@ -25,7 +25,7 @@ RSpec.configure do |config|
original_null_store = Rails.cache
caching_config_hash = Gitlab::Redis::Cache.params
caching_config_hash[:namespace] = Gitlab::Redis::Cache::CACHE_NAMESPACE
- Rails.cache = ActiveSupport::Cache::RedisCacheStore.new(caching_config_hash)
+ Rails.cache = ActiveSupport::Cache::RedisCacheStore.new(**caching_config_hash)
redis_cache_cleanup!
diff --git a/spec/support/gitlab_experiment.rb b/spec/support/gitlab_experiment.rb
new file mode 100644
index 00000000000..1f283e4f06c
--- /dev/null
+++ b/spec/support/gitlab_experiment.rb
@@ -0,0 +1,4 @@
+# frozen_string_literal: true
+
+# Disable all caching for experiments in tests.
+Gitlab::Experiment::Configuration.cache = nil
diff --git a/spec/support/gitlab_stubs/gitlab_ci_includes.yml b/spec/support/gitlab_stubs/gitlab_ci_includes.yml
new file mode 100644
index 00000000000..e74773ce23e
--- /dev/null
+++ b/spec/support/gitlab_stubs/gitlab_ci_includes.yml
@@ -0,0 +1,19 @@
+rspec 0 1:
+ stage: build
+ script: 'rake spec'
+ needs: []
+
+rspec 0 2:
+ stage: build
+ script: 'rake spec'
+ needs: []
+
+spinach:
+ stage: build
+ script: 'rake spinach'
+ needs: []
+
+docker:
+ stage: test
+ script: 'curl http://dockerhub/URL'
+ needs: [spinach, rspec 0 1]
diff --git a/spec/support/graphql/arguments.rb b/spec/support/graphql/arguments.rb
new file mode 100644
index 00000000000..d8c334c2ca4
--- /dev/null
+++ b/spec/support/graphql/arguments.rb
@@ -0,0 +1,70 @@
+# frozen_string_literal: true
+
+module Graphql
+ class Arguments
+ delegate :blank?, :empty?, to: :to_h
+
+ def initialize(values)
+ @values = values.compact
+ end
+
+ def to_h
+ @values
+ end
+
+ def ==(other)
+ to_h == other&.to_h
+ end
+
+ alias_method :eql, :==
+
+ def to_s
+ return '' if empty?
+
+ @values.map do |name, value|
+ value_str = as_graphql_literal(value)
+
+ "#{GraphqlHelpers.fieldnamerize(name.to_s)}: #{value_str}"
+ end.join(", ")
+ end
+
+ def as_graphql_literal(value)
+ self.class.as_graphql_literal(value)
+ end
+
+ # Transform values to GraphQL literal arguments.
+ # Use symbol for Enum values
+ def self.as_graphql_literal(value)
+ case value
+ when ::Graphql::Arguments then "{#{value}}"
+ when Array then "[#{value.map { |v| as_graphql_literal(v) }.join(',')}]"
+ when Hash then "{#{new(value)}}"
+ when Integer, Float, Symbol then value.to_s
+ when String then "\"#{value.gsub(/"/, '\\"')}\""
+ when nil then 'null'
+ when true then 'true'
+ when false then 'false'
+ else
+ value.to_graphql_value
+ end
+ rescue NoMethodError
+ raise ArgumentError, "Cannot represent #{value} as GraphQL literal"
+ end
+
+ def merge(other)
+ self.class.new(@values.merge(other.to_h))
+ end
+
+ def +(other)
+ if blank?
+ other
+ elsif other.blank?
+ self
+ elsif other.is_a?(String)
+ [to_s, other].compact.join(', ')
+ else
+ merge(other)
+ end
+ end
+ end
+end
diff --git a/spec/support/graphql/field_inspection.rb b/spec/support/graphql/field_inspection.rb
new file mode 100644
index 00000000000..f39ba751141
--- /dev/null
+++ b/spec/support/graphql/field_inspection.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+module Graphql
+ class FieldInspection
+ def initialize(field)
+ @field = field
+ end
+
+ def nested_fields?
+ !scalar? && !enum?
+ end
+
+ def scalar?
+ type.kind.scalar?
+ end
+
+ def enum?
+ type.kind.enum?
+ end
+
+ def type
+ @type ||= begin
+ field_type = @field.type.respond_to?(:to_graphql) ? @field.type.to_graphql : @field.type
+
+ # The type could be nested. For example `[GraphQL::STRING_TYPE]`:
+ # - List
+ # - String!
+ # - String
+ field_type = field_type.of_type while field_type.respond_to?(:of_type)
+
+ field_type
+ end
+ end
+ end
+end
diff --git a/spec/support/graphql/field_selection.rb b/spec/support/graphql/field_selection.rb
new file mode 100644
index 00000000000..e2a3334acac
--- /dev/null
+++ b/spec/support/graphql/field_selection.rb
@@ -0,0 +1,73 @@
+# frozen_string_literal: true
+
+module Graphql
+ class FieldSelection
+ delegate :empty?, :blank?, :to_h, to: :selection
+ delegate :size, to: :paths
+
+ attr_reader :selection
+
+ def initialize(selection = {})
+ @selection = selection.to_h
+ end
+
+ def to_s
+ serialize_field_selection(selection)
+ end
+
+ def paths
+ selection.flat_map do |field, subselection|
+ paths_in([field], subselection)
+ end
+ end
+
+ private
+
+ def paths_in(path, leaves)
+ return [path] if leaves.nil?
+
+ leaves.to_a.flat_map do |k, v|
+ paths_in([k], v).map { |tail| path + tail }
+ end
+ end
+
+ def serialize_field_selection(hash, level = 0)
+ indent = ' ' * level
+
+ hash.map do |field, subselection|
+ if subselection.nil?
+ "#{indent}#{field}"
+ else
+ subfields = serialize_field_selection(subselection, level + 1)
+ "#{indent}#{field} {\n#{subfields}\n#{indent}}"
+ end
+ end.join("\n")
+ end
+
+ NO_SKIP = ->(_name, _field) { false }
+
+ def self.select_fields(type, skip = NO_SKIP, parent_types = Set.new, max_depth = 3)
+ return new if max_depth <= 0
+
+ new(type.fields.flat_map do |name, field|
+ next [] if skip[name, field]
+
+ 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)
+ next [] if subselection.empty?
+
+ [[name, subselection.to_h]]
+ else
+ [[name, nil]]
+ end
+ end)
+ end
+ end
+end
diff --git a/spec/support/graphql/var.rb b/spec/support/graphql/var.rb
new file mode 100644
index 00000000000..4f2c774e898
--- /dev/null
+++ b/spec/support/graphql/var.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+module Graphql
+ # Helper to pass variables around generated queries.
+ #
+ # e.g.:
+ # first = var('Int')
+ # after = var('String')
+ #
+ # query = with_signature(
+ # [first, after],
+ # query_graphql_path([
+ # [:project, { full_path: project.full_path }],
+ # [:issues, { after: after, first: first }]
+ # :nodes
+ # ], all_graphql_fields_for('Issue'))
+ # )
+ #
+ # post_graphql(query, variables: [first.with(2), after.with(some_cursor)])
+ #
+ class Var
+ attr_reader :name, :type
+ attr_accessor :value
+
+ def initialize(name, type)
+ @name = name
+ @type = type
+ end
+
+ def sig
+ "#{to_graphql_value}: #{type}"
+ end
+
+ def to_graphql_value
+ "$#{name}"
+ end
+
+ # We return a new object so that running the same query twice with
+ # different values does not risk re-using the value
+ #
+ # e.g.
+ #
+ # x = var('Int')
+ # expect { post_graphql(query, variables: x) }
+ # .to issue_same_number_of_queries_as { post_graphql(query, variables: x.with(1)) }
+ #
+ # Here we post the `x` variable once with the value set to 1, and once with
+ # the value set to `nil`.
+ def with(value)
+ copy = Var.new(name, type)
+ copy.value = value
+ copy
+ end
+
+ def to_h
+ { name => value }
+ end
+ end
+end
diff --git a/spec/support/helpers/after_next_helpers.rb b/spec/support/helpers/after_next_helpers.rb
new file mode 100644
index 00000000000..0a7844fdd8f
--- /dev/null
+++ b/spec/support/helpers/after_next_helpers.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+require_relative './next_instance_of'
+
+module AfterNextHelpers
+ class DeferredExpectation
+ include ::NextInstanceOf
+ include ::RSpec::Matchers
+ include ::RSpec::Mocks::ExampleMethods
+
+ def initialize(klass, args, level:)
+ @klass = klass
+ @args = args
+ @level = level.to_sym
+ end
+
+ def to(condition)
+ run_condition(condition, asserted: true)
+ end
+
+ def not_to(condition)
+ run_condition(condition, asserted: false)
+ end
+
+ private
+
+ attr_reader :klass, :args, :level
+
+ def run_condition(condition, asserted:)
+ msg = asserted ? :to : :not_to
+ case level
+ when :expect
+ if asserted
+ expect_next_instance_of(klass, *args) { |instance| expect(instance).send(msg, condition) }
+ else
+ allow_next_instance_of(klass, *args) { |instance| expect(instance).send(msg, condition) }
+ end
+ when :allow
+ allow_next_instance_of(klass, *args) { |instance| allow(instance).send(msg, condition) }
+ else
+ raise "Unknown level: #{level}"
+ end
+ end
+ end
+
+ def allow_next(klass, *args)
+ DeferredExpectation.new(klass, args, level: :allow)
+ end
+
+ def expect_next(klass, *args)
+ DeferredExpectation.new(klass, args, level: :expect)
+ end
+end
diff --git a/spec/support/helpers/database_helpers.rb b/spec/support/helpers/database_helpers.rb
new file mode 100644
index 00000000000..e9f0a74a8d1
--- /dev/null
+++ b/spec/support/helpers/database_helpers.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module DatabaseHelpers
+ # In order to directly work with views using factories,
+ # we can swapout the view for a table of identical structure.
+ def swapout_view_for_table(view)
+ ActiveRecord::Base.connection.execute(<<~SQL)
+ CREATE TABLE #{view}_copy (LIKE #{view});
+ DROP VIEW #{view};
+ ALTER TABLE #{view}_copy RENAME TO #{view};
+ SQL
+ end
+end
diff --git a/spec/support/helpers/dependency_proxy_helpers.rb b/spec/support/helpers/dependency_proxy_helpers.rb
index 545b9d1f4d0..ebb849628bf 100644
--- a/spec/support/helpers/dependency_proxy_helpers.rb
+++ b/spec/support/helpers/dependency_proxy_helpers.rb
@@ -11,11 +11,18 @@ module DependencyProxyHelpers
.to_return(status: status, body: body || auth_body)
end
- def stub_manifest_download(image, tag, status = 200, body = nil)
+ def stub_manifest_download(image, tag, status: 200, body: nil, headers: {})
manifest_url = registry.manifest_url(image, tag)
stub_full_request(manifest_url)
- .to_return(status: status, body: body || manifest)
+ .to_return(status: status, body: body || manifest, headers: headers)
+ end
+
+ def stub_manifest_head(image, tag, status: 200, body: nil, digest: '123456')
+ manifest_url = registry.manifest_url(image, tag)
+
+ stub_full_request(manifest_url, method: :head)
+ .to_return(status: status, body: body, headers: { 'docker-content-digest' => digest } )
end
def stub_blob_download(image, blob_sha, status = 200, body = '123456')
@@ -25,6 +32,13 @@ module DependencyProxyHelpers
.to_return(status: status, body: body)
end
+ def build_jwt(user = nil, expire_time: nil)
+ JSONWebToken::HMACToken.new(::Auth::DependencyProxyAuthenticationService.secret).tap do |jwt|
+ jwt['user_id'] = user.id if user
+ jwt.expire_time = expire_time || jwt.issued_at + 1.minute
+ end
+ end
+
private
def registry
diff --git a/spec/support/helpers/features/merge_request_helpers.rb b/spec/support/helpers/features/merge_request_helpers.rb
new file mode 100644
index 00000000000..53896e1fe12
--- /dev/null
+++ b/spec/support/helpers/features/merge_request_helpers.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Spec
+ module Support
+ module Helpers
+ module Features
+ module MergeRequestHelpers
+ def preload_view_requirements(merge_request, note)
+ # This will load the status fields of the author of the note and merge request
+ # to avoid queries when rendering the view being tested.
+ #
+ merge_request.author.status
+ note.author.status
+ end
+
+ def serialize_issuable_sidebar(user, project, merge_request)
+ MergeRequestSerializer
+ .new(current_user: user, project: project)
+ .represent(merge_request, serializer: 'sidebar')
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/support/helpers/features/web_ide_spec_helpers.rb b/spec/support/helpers/features/web_ide_spec_helpers.rb
index 12d3cecd052..358bfacce05 100644
--- a/spec/support/helpers/features/web_ide_spec_helpers.rb
+++ b/spec/support/helpers/features/web_ide_spec_helpers.rb
@@ -22,8 +22,6 @@ module WebIdeSpecHelpers
click_link('Web IDE')
wait_for_requests
-
- save_monaco_editor_reference
end
def ide_tree_body
@@ -65,17 +63,6 @@ module WebIdeSpecHelpers
ide_set_editor_value(content)
end
- def ide_rename_file(path, new_path)
- container = ide_traverse_to_file(path)
-
- click_file_action(container, 'Rename/Move')
-
- within '#ide-new-entry' do
- find('input').fill_in(with: new_path)
- click_button('Rename file')
- end
- end
-
# Deletes a file by traversing to `path`
# then clicking the 'Delete' action.
#
@@ -103,20 +90,6 @@ module WebIdeSpecHelpers
container
end
- def ide_close_file(name)
- within page.find('.multi-file-tabs') do
- click_button("Close #{name}")
- end
- end
-
- def ide_open_file(path)
- row = ide_traverse_to_file(path)
-
- ide_open_file_row(row)
-
- wait_for_requests
- end
-
def ide_open_file_row(row)
return if ide_folder_row_open?(row)
@@ -130,10 +103,6 @@ module WebIdeSpecHelpers
execute_script("monaco.editor.getModel('#{uri}').setValue('#{escape_javascript(value)}')")
end
- def ide_set_editor_position(line, col)
- execute_script("TEST_EDITOR.setPosition(#{{ lineNumber: line, column: col }.to_json})")
- end
-
def ide_editor_value
editor = find('.monaco-editor')
uri = editor['data-uri']
@@ -180,8 +149,4 @@ module WebIdeSpecHelpers
wait_for_requests
end
end
-
- def save_monaco_editor_reference
- evaluate_script("monaco.editor.onDidCreateEditor(editor => { window.TEST_EDITOR = editor; })")
- end
end
diff --git a/spec/support/helpers/file_read_helpers.rb b/spec/support/helpers/file_read_helpers.rb
new file mode 100644
index 00000000000..c30a9e6466f
--- /dev/null
+++ b/spec/support/helpers/file_read_helpers.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+module FileReadHelpers
+ def stub_file_read(file, content: nil, error: nil)
+ allow_original_file_read
+
+ expectation = allow(File).to receive(:read).with(file)
+
+ if error
+ expectation.and_raise(error)
+ elsif content
+ expectation.and_return(content)
+ else
+ expectation
+ end
+ end
+
+ def expect_file_read(file, content: nil, error: nil)
+ allow_original_file_read
+
+ expectation = expect(File).to receive(:read).with(file)
+
+ if error
+ expectation.and_raise(error)
+ elsif content
+ expectation.and_return(content)
+ else
+ expectation
+ end
+ end
+
+ def expect_file_not_to_read(file)
+ allow_original_file_read
+
+ expect(File).not_to receive(:read).with(file)
+ end
+
+ private
+
+ def allow_original_file_read
+ # Don't set this mock twice, otherwise subsequent calls will clobber
+ # previous mocks
+ return if @allow_original_file_read
+
+ @allow_original_file_read = true
+ allow(File).to receive(:read).and_call_original
+ end
+end
diff --git a/spec/support/helpers/filtered_search_helpers.rb b/spec/support/helpers/filtered_search_helpers.rb
index d203ff60cc9..10068b9c508 100644
--- a/spec/support/helpers/filtered_search_helpers.rb
+++ b/spec/support/helpers/filtered_search_helpers.rb
@@ -113,6 +113,10 @@ module FilteredSearchHelpers
create_token('Assignee', assignee_name)
end
+ def reviewer_token(reviewer_name = nil)
+ create_token('Reviewer', reviewer_name)
+ end
+
def milestone_token(milestone_name = nil, has_symbol = true, operator = '=')
symbol = has_symbol ? '%' : nil
create_token('Milestone', milestone_name, symbol, operator)
diff --git a/spec/support/helpers/git_http_helpers.rb b/spec/support/helpers/git_http_helpers.rb
index c9c1c4dcfc9..c9b4e06f067 100644
--- a/spec/support/helpers/git_http_helpers.rb
+++ b/spec/support/helpers/git_http_helpers.rb
@@ -5,45 +5,45 @@ require_relative 'workhorse_helpers'
module GitHttpHelpers
include WorkhorseHelpers
- def clone_get(project, **options)
- get "/#{project}/info/refs", params: { service: 'git-upload-pack' }, headers: auth_env(*options.values_at(:user, :password, :spnego_request_token))
+ def clone_get(repository_path, **options)
+ get "/#{repository_path}/info/refs", params: { service: 'git-upload-pack' }, headers: auth_env(*options.values_at(:user, :password, :spnego_request_token))
end
- def clone_post(project, **options)
- post "/#{project}/git-upload-pack", headers: auth_env(*options.values_at(:user, :password, :spnego_request_token))
+ def clone_post(repository_path, **options)
+ post "/#{repository_path}/git-upload-pack", headers: auth_env(*options.values_at(:user, :password, :spnego_request_token))
end
- def push_get(project, **options)
- get "/#{project}/info/refs", params: { service: 'git-receive-pack' }, headers: auth_env(*options.values_at(:user, :password, :spnego_request_token))
+ def push_get(repository_path, **options)
+ get "/#{repository_path}/info/refs", params: { service: 'git-receive-pack' }, headers: auth_env(*options.values_at(:user, :password, :spnego_request_token))
end
- def push_post(project, **options)
- post "/#{project}/git-receive-pack", headers: auth_env(*options.values_at(:user, :password, :spnego_request_token))
+ def push_post(repository_path, **options)
+ post "/#{repository_path}/git-receive-pack", headers: auth_env(*options.values_at(:user, :password, :spnego_request_token))
end
- def download(project, user: nil, password: nil, spnego_request_token: nil)
+ def download(repository_path, user: nil, password: nil, spnego_request_token: nil)
args = { user: user, password: password, spnego_request_token: spnego_request_token }
- clone_get(project, **args)
+ clone_get(repository_path, **args)
yield response
- clone_post(project, **args)
+ clone_post(repository_path, **args)
yield response
end
- def upload(project, user: nil, password: nil, spnego_request_token: nil)
+ def upload(repository_path, user: nil, password: nil, spnego_request_token: nil)
args = { user: user, password: password, spnego_request_token: spnego_request_token }
- push_get(project, **args)
+ push_get(repository_path, **args)
yield response
- push_post(project, **args)
+ push_post(repository_path, **args)
yield response
end
- def download_or_upload(project, **args, &block)
- download(project, **args, &block)
- upload(project, **args, &block)
+ def download_or_upload(repository_path, **args, &block)
+ download(repository_path, **args, &block)
+ upload(repository_path, **args, &block)
end
def auth_env(user, password, spnego_request_token)
diff --git a/spec/support/helpers/gitlab_verify_helpers.rb b/spec/support/helpers/gitlab_verify_helpers.rb
index 9901ce374ed..2a6ba8aaff4 100644
--- a/spec/support/helpers/gitlab_verify_helpers.rb
+++ b/spec/support/helpers/gitlab_verify_helpers.rb
@@ -2,7 +2,7 @@
module GitlabVerifyHelpers
def collect_ranges(args = {})
- verifier = described_class.new(args.merge(batch_size: 1))
+ verifier = described_class.new(**args.merge(batch_size: 1))
collect_results(verifier).map { |range, _| range }
end
diff --git a/spec/support/helpers/gpg_helpers.rb b/spec/support/helpers/gpg_helpers.rb
index f4df1cf601c..389e5818dbe 100644
--- a/spec/support/helpers/gpg_helpers.rb
+++ b/spec/support/helpers/gpg_helpers.rb
@@ -144,6 +144,145 @@ module GpgHelpers
'5F7EA3981A5845B141ABD522CCFBE19F00AC8B1D'
end
+ def secret_key2
+ <<~KEY.strip
+ -----BEGIN PGP PRIVATE KEY BLOCK-----
+
+ lQWGBF+7O0oBDADvRto4K9PT83Lbyp/qaMPIzBbXHB6ljdDoyb+Pn2UrHk9MhB5v
+ bTgBv+rctOabmimPPalcyaxOQ1GtrYizo1l33YQZupSvaOoStVLWqnBx8eKKcUv8
+ QucS3S2qFhj9G0tdHW7RW2BGrSwEM09d2xFsFKKAj/4RTTU5idYWrvB24DNcrBh+
+ iKsoa+rmJf1bwL6Mn9f9NwzundG16qibY/UwMlltQriWaVMn2AKVuu6HrX9pe3g5
+ Er2Szjc7DZitt6eAy3PmuWHXzDCCvsO7iPxXlywY49hLhDen3/Warwn1pSbp+im4
+ /0oJExLZBSS1xHbRSQoR6matF0+V/6TQz8Yo3g8z9HgyEtn1V7QJo3PoNrnEl73e
+ 9yslTqVtzba0Q132oRoO7eEYf82KrPOmVGj6Q9LpSXFLfsl3GlPgoBxRZXpT62CV
+ 3rGalIa2yKmcBQtyICjR1+PTIAJcVIPyr92xTo4RfLwVFW0czX7LM2H0FT2Ksj7L
+ U450ewBz8N6bFDMAEQEAAf4HAwIkqHaeA9ofAv9oQj+upbqfdEmXd0krBv5R1Q3u
+ VZwtCdnf0KGtueJ7SpPHVbNB0gCYnYdgf59MF9HHuVjHTWCOBwBJ3hmc7Yt2NcZy
+ ow15C+2xy+6/ChIYz3K7cr3jFR17M8Rz430YpCeGdYq5CfNQvNlzHDjO7PClLOek
+ jqy7V0ME0j6Q5+gHKqz6ragrUkfQBK863T4/4IUE+oCcDkuPaQUJQcYbI81R60Tl
+ 4Rasi6njwj9MZlt9k8wfXmMInWAl7aLaEzTpwVFG8xZ5IHExWGHO9mS+DNqBRVd9
+ oDQoYoLFW6w0wPIkcn1uoUJaDZoRFzy2AzFInS8oLPAYWg/Wg8TLyyTIHYq9Zn+B
+ 1mXeBHqx+TOCFq8P1wk9/A4MIl8cJmsEYrd2u0xdbVUQxCDzqrjqVmU4oamY6N6s
+ JPSp/hhBJB97CbCIoACB3aaH1CFDyXvyiqjobD5daKz8FlDzm4yze5n5b7CLwAWB
+ IA7nbNsGnLZiKQs+jmA6VcAax3nlulhG0YnzNLlwX4PgWjwjtd79rEmSdN9LsZE3
+ R26377QFE6G5NLDiKg/96NsRYA1BsDnAWKpm64ZVHHbBxz/HiAP1Zncw3Ij5p8F1
+ mtHK++qNF1P2OkAP01KaE2v6T+d3lCQzlPwnQIojW/NGvBZXarjV3916fN7rJamf
+ gs6Q72XKuXCOVJxGvknVGjXS97AIWbllLcCG5nYZx5BYaehMWOjrB9abD3h3lRXt
+ lT43gOFI53XY/vTw+jsPeT125QjjB3Kih5Ch5b6tXMj7X1Lkd9yTOIU0LVF5e9St
+ 1mvVl+pPwWafq60vlCtEnluwcEmH6XDiIABHDchgBdk+qsvc215bspyPRy4CRVAg
+ V3eaFFKgFrF/qDtzLgYVopcij1ovGmmox+m3mua4wSAs5Bm2UotEZfGscN6sCSfR
+ KAk83bV00rfjC/Zrgx3zn6PUqit5KcpLkQIo/CzUr9UCRC3tMIzFARbmjTE7f471
+ +kUuJGxMONiRQC3ejLDZ/+B7WvZm44KffyKVlOSfG0MDUZzsINNY3jUskF2pfuq2
+ acXqcVi16grRjyIsoRtZFM5/yu7ED7j4yZRRnBjD+E03uui5Rv3uiHcddE8nwwU+
+ Tctvua+0QtS5NzFL6pM8tYdgRTXYekaoZf6N8sE3kgOlanvyXwxguNA7Y5Ns1mFC
+ JqIwOVwQbi8bk9I2PY9ER/nK6HRx2LpM466wRp7Bn9WAY8k/5gjzZrqVDCZJjuTO
+ mmhvGcm9wvsXxfb1NQdhc7ZHvCTj+Gf5hmdpzJnX0Cm83BqEEpmKk0HAXNCmMxQp
+ 3twrjrj/RahXVpnUgQR8PKAn7HjVFs/YvbQtTmFubmllIEJlcm5oYXJkIDxuYW5u
+ aWUuYmVybmhhcmRAZXhhbXBsZS5jb20+iQHUBBMBCgA+FiEExEem9r/Zzvj7NxeF
+ VxYlqTAkEXkFAl+7O0oCGwMFCQPCZwAFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AA
+ CgkQVxYlqTAkEXk9xwv/WlJJGJ+QyGeJAhySG3z3bQnFwb2CusF2LbwcAETDgbkf
+ opkkf34Vbb9A7kM7peZ7Va0Edsg09XdkBUAdaqKQn78HiZJC5n0grXcj1c67Adss
+ Ym9TGVM6AC3K3Vm3wVV0X+ng31rdDpjfIqfYDAvwhMc8H/MHs/dCRSIxEGWK8UKh
+ WLUrX+wN+HNMVbzWPGwoTMWiDa/ofA9INhqN+u+mJkTaP+a4R3LTgL5hp+kUDOaB
+ Nc0rqH7vgj+037NTL8vox18J4qgNbRIsywclMYBJDwfA4w1phtsMu1BKPiOu2kue
+ 18fyGDtboXUPFOJjf5OEwJsu+MFogWeAVuHN/eeiqOAFCYW+TT6Ehc6BnJ8vWCMS
+ Dgs3t6i94gNZtvEty2EAheHEBD1alU4c6S3VENdh5q2KkWIVFxgNtungo03eAVfj
+ UhMjrrEu0LC/Rizo7Me0kG7rfdn9oIwp4MTn7Cst1wGEWdi9UO4NJf1C+P9rFQuG
+ hMaj+8gb1uBdjPG8WOOanQWGBF+7O0oBDADhzNAvjiphKHsa4O5s3BePLQ+DJz+K
+ rS8f9mb66to/w9BlUtnm/L4gVgiIYqGhH7TSDaGhvIDMf3iKKBnKrWeBe0W8cdq3
+ FlzWC/AHUahEFxFm0l6nq0pOIiAVQ58IPaB/0a5YCY7tU2yfw8llZUN8dWJ7cSsB
+ Gpa6Q9/9y4x5/9VPDPduXRv22KCfDbHXuFS79ubmueFfrOa1CLXRhCy3dUXCyePU
+ YuwxixXJRTJQJm+A6c8TFIL+cji7IEzzDAiNexfGzEfu+Qj1/9PzX8aIn6C5Tf4q
+ B1pcGa4uYr8K1aCENcVt6+GA5gMdcplYXmtA212RyPqQmnJIjxDdS7AJYcivqG2q
+ F5CvqzKY5/A+e9+GLyRM36P8LpB8+XHMoYNMNmOl5KX6WZ1tRw/xxgv1iKX3Pcqd
+ noFwsOCNVpTWlxvjsyve8VQUplORSakIhfKh1VWu7j8AKXWe9S3zMYQDq5G8VrTO
+ Vb1pPvPgiNxo9u1OXi2H9UTXhCWYZ6FIe2UAEQEAAf4HAwIlxJFDCl1eRf+8ne6l
+ KpsQfPjhCNnaXE1Q1izRVNGn0gojZkHTRzBF6ZOaPMNSWOri22JoaACI2txuQLyu
+ fHdO+ROr2Pnp17zeXbrm9Tk0PpugPwW/+AkvLPtcSOoCLEzkoKnwKmpC224Ed2Zb
+ Ma5ApPp3HNGkZgPVw5Mvj8R/n8MbKr7/TC7PV9WInranisZqH9fzvA3KEpaDwSr0
+ vBtn6nXzSQKhmwCGRLCUuA+HG2gXIlYuNi7lPpu+Tivz+FnIaTVtrhG5b6Az30QP
+ C0cLe539X9HgryP6M9kzLSYnfpGQMqSqOUYZfhQW6xtSWr7/iWdnYF7S1YouWPLs
+ vuN+xFFKv3eVtErk4UOgAp9it4/i41QuMNwCWCt71278Ugwqygexw/XMi+Rs2Z6C
+ 2ESu1dJnOhYF4eL7ymSKxwBitA+qETQBsjxjegNls/poFjREIhOOwM0w9mn+GptC
+ RVmFdcTlXMGJIGPxTFZQzIitCVoTURrkzBvqUvKFft8GcEBr2izoIqOZU3Npya7c
+ kKHyVMY0n7xjH3Hs4C3A4tBtkbDpwxz+hc9xh5/E/EKKlvZLfIKuuTP4eJap8KEN
+ vvbDPolF3TveTvNLIe86GTSU+wi67PM1PBHKhLSP2aYvS503Z29OLD6Rd6p6jI8u
+ MC8ueF719oH5uG5Sbs3OGmX+UF1aaproLhnGpTwrLyEX7tMebb/JM22Qasj9H9to
+ PNAgEfhlNdhJ+IULkx0My2e55+BIskhsWJpkAhpD2dOyiDBsXZvT3x3dbMKWi1sS
+ +nbKzhMjmUoQ++Vh2uZ9Zi93H3+gsge6e1duRSLNEFrrOk9c6cVPsmle7HoZSzNw
+ qYVCb3npMo+43IgyaK48eGS757ZGsgTEQdicoqVann+wHbAOlWwUFSPTGpqTMMvD
+ 17PVFQB4ADb5J3IAy7kJsVUwoqYI8VrdfiJJUeQikePOi760TCUTJ3PlMUNqngMn
+ ItzNidE8A0RvzFW6DNcPHJVpdGRk36GtWooBhxRwelchAgTSB6gVueF9KTW+EZU2
+ evdAwuTfwvTguOuJ3yJ6g+vFiHYrsczHJXq7QaJbpmJLlavvA2yFPDmlSDMSMKFo
+ t13RwYZ+mPLS5QLK52vbCmDKiQI7Z7zLXIcQ2RXXHQN4OYYLbDXeIMO2BwXAsGJf
+ LC3W64gMUSRKB07UXmDdu4U3US0sqMsxUNWqLFC8PRVR68NAxF+8zS1xKLCUPRWS
+ ELivIY0m4ybzITM6xHBCOSFRph5+LKQVehEo1qM7aoRtS+5SHjdtOeyPEQwSTsWj
+ IWlumHJAXFUmBqc+bVi1m661c5O56VCm7PP61oQQxsB3J0E5OsQUA4kBvAQYAQoA
+ JhYhBMRHpva/2c74+zcXhVcWJakwJBF5BQJfuztKAhsMBQkDwmcAAAoJEFcWJakw
+ JBF5T/ML/3Ml7+493hQuoC9O3HOANkimc0pGxILVeJmJmnfbMDJ71fU84h2+xAyk
+ 2PZc48wVYKju9THJzdRk+XBPO+G6mSBupSt53JIYb5NijotNTmJmHYpG1yb+9FjD
+ EFWTlxK1mr5wjSUxlGWa/O46XjxzCSEUP1SknLWbTOucV8KOmPWL3DupvGINIIQx
+ e5eJ9SMjlHvUn4rq8sd11FT2bQrd+xMx8gP5cearPqB7qVRlHjtOKn29gTV90kIw
+ amRke8KxSoJh+xT057aKI2+MCu7RC8TgThmUVCWgwUzXlsw1Qe8ySc6CmjIBftfo
+ lQYPDSq1u8RSBAB+t2Xwprvdedr9SQihzBk5GCGBJ/npEcgF2jk26sJqoXYbvyQG
+ tqSDQ925oP7OstyOE4FTH7sQmBvP01Ikdgwkm0cthLSpWY4QI+09Aeg+rZ80Etfv
+ vAKquDGA33no8YGnn+epeLqyscIh4WG3bIoHk9JlFCcwIp9U65IfR1fTcvlTdzZN
+ 4f6xMfFu2A==
+ =3YL6
+ -----END PGP PRIVATE KEY BLOCK-----
+ KEY
+ end
+
+ def public_key2
+ <<~KEY.strip
+ -----BEGIN PGP PUBLIC KEY BLOCK-----
+
+ mQGNBF+7O0oBDADvRto4K9PT83Lbyp/qaMPIzBbXHB6ljdDoyb+Pn2UrHk9MhB5v
+ bTgBv+rctOabmimPPalcyaxOQ1GtrYizo1l33YQZupSvaOoStVLWqnBx8eKKcUv8
+ QucS3S2qFhj9G0tdHW7RW2BGrSwEM09d2xFsFKKAj/4RTTU5idYWrvB24DNcrBh+
+ iKsoa+rmJf1bwL6Mn9f9NwzundG16qibY/UwMlltQriWaVMn2AKVuu6HrX9pe3g5
+ Er2Szjc7DZitt6eAy3PmuWHXzDCCvsO7iPxXlywY49hLhDen3/Warwn1pSbp+im4
+ /0oJExLZBSS1xHbRSQoR6matF0+V/6TQz8Yo3g8z9HgyEtn1V7QJo3PoNrnEl73e
+ 9yslTqVtzba0Q132oRoO7eEYf82KrPOmVGj6Q9LpSXFLfsl3GlPgoBxRZXpT62CV
+ 3rGalIa2yKmcBQtyICjR1+PTIAJcVIPyr92xTo4RfLwVFW0czX7LM2H0FT2Ksj7L
+ U450ewBz8N6bFDMAEQEAAbQtTmFubmllIEJlcm5oYXJkIDxuYW5uaWUuYmVybmhh
+ cmRAZXhhbXBsZS5jb20+iQHUBBMBCgA+FiEExEem9r/Zzvj7NxeFVxYlqTAkEXkF
+ Al+7O0oCGwMFCQPCZwAFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AACgkQVxYlqTAk
+ EXk9xwv/WlJJGJ+QyGeJAhySG3z3bQnFwb2CusF2LbwcAETDgbkfopkkf34Vbb9A
+ 7kM7peZ7Va0Edsg09XdkBUAdaqKQn78HiZJC5n0grXcj1c67AdssYm9TGVM6AC3K
+ 3Vm3wVV0X+ng31rdDpjfIqfYDAvwhMc8H/MHs/dCRSIxEGWK8UKhWLUrX+wN+HNM
+ VbzWPGwoTMWiDa/ofA9INhqN+u+mJkTaP+a4R3LTgL5hp+kUDOaBNc0rqH7vgj+0
+ 37NTL8vox18J4qgNbRIsywclMYBJDwfA4w1phtsMu1BKPiOu2kue18fyGDtboXUP
+ FOJjf5OEwJsu+MFogWeAVuHN/eeiqOAFCYW+TT6Ehc6BnJ8vWCMSDgs3t6i94gNZ
+ tvEty2EAheHEBD1alU4c6S3VENdh5q2KkWIVFxgNtungo03eAVfjUhMjrrEu0LC/
+ Rizo7Me0kG7rfdn9oIwp4MTn7Cst1wGEWdi9UO4NJf1C+P9rFQuGhMaj+8gb1uBd
+ jPG8WOOauQGNBF+7O0oBDADhzNAvjiphKHsa4O5s3BePLQ+DJz+KrS8f9mb66to/
+ w9BlUtnm/L4gVgiIYqGhH7TSDaGhvIDMf3iKKBnKrWeBe0W8cdq3FlzWC/AHUahE
+ FxFm0l6nq0pOIiAVQ58IPaB/0a5YCY7tU2yfw8llZUN8dWJ7cSsBGpa6Q9/9y4x5
+ /9VPDPduXRv22KCfDbHXuFS79ubmueFfrOa1CLXRhCy3dUXCyePUYuwxixXJRTJQ
+ Jm+A6c8TFIL+cji7IEzzDAiNexfGzEfu+Qj1/9PzX8aIn6C5Tf4qB1pcGa4uYr8K
+ 1aCENcVt6+GA5gMdcplYXmtA212RyPqQmnJIjxDdS7AJYcivqG2qF5CvqzKY5/A+
+ e9+GLyRM36P8LpB8+XHMoYNMNmOl5KX6WZ1tRw/xxgv1iKX3PcqdnoFwsOCNVpTW
+ lxvjsyve8VQUplORSakIhfKh1VWu7j8AKXWe9S3zMYQDq5G8VrTOVb1pPvPgiNxo
+ 9u1OXi2H9UTXhCWYZ6FIe2UAEQEAAYkBvAQYAQoAJhYhBMRHpva/2c74+zcXhVcW
+ JakwJBF5BQJfuztKAhsMBQkDwmcAAAoJEFcWJakwJBF5T/ML/3Ml7+493hQuoC9O
+ 3HOANkimc0pGxILVeJmJmnfbMDJ71fU84h2+xAyk2PZc48wVYKju9THJzdRk+XBP
+ O+G6mSBupSt53JIYb5NijotNTmJmHYpG1yb+9FjDEFWTlxK1mr5wjSUxlGWa/O46
+ XjxzCSEUP1SknLWbTOucV8KOmPWL3DupvGINIIQxe5eJ9SMjlHvUn4rq8sd11FT2
+ bQrd+xMx8gP5cearPqB7qVRlHjtOKn29gTV90kIwamRke8KxSoJh+xT057aKI2+M
+ Cu7RC8TgThmUVCWgwUzXlsw1Qe8ySc6CmjIBftfolQYPDSq1u8RSBAB+t2Xwprvd
+ edr9SQihzBk5GCGBJ/npEcgF2jk26sJqoXYbvyQGtqSDQ925oP7OstyOE4FTH7sQ
+ mBvP01Ikdgwkm0cthLSpWY4QI+09Aeg+rZ80EtfvvAKquDGA33no8YGnn+epeLqy
+ scIh4WG3bIoHk9JlFCcwIp9U65IfR1fTcvlTdzZN4f6xMfFu2A==
+ =RAwd
+ -----END PGP PUBLIC KEY BLOCK-----
+ KEY
+ end
+
+ def fingerprint2
+ 'C447A6F6BFD9CEF8FB371785571625A930241179'
+ end
+
def names
['Nannie Bernhard']
end
diff --git a/spec/support/helpers/graphql_helpers.rb b/spec/support/helpers/graphql_helpers.rb
index a1b4e6eee92..b20801bd3c4 100644
--- a/spec/support/helpers/graphql_helpers.rb
+++ b/spec/support/helpers/graphql_helpers.rb
@@ -4,6 +4,11 @@ module GraphqlHelpers
MutationDefinition = Struct.new(:query, :variables)
NoData = Class.new(StandardError)
+ UnauthorizedObject = Class.new(StandardError)
+
+ def graphql_args(**values)
+ ::Graphql::Arguments.new(values)
+ end
# makes an underscored string look like a fieldname
# "merge_request" => "mergeRequest"
@@ -17,7 +22,10 @@ module GraphqlHelpers
# ready, then the early return is returned instead.
#
# Then the resolve method is called.
- def resolve(resolver_class, args: {}, **resolver_args)
+ def resolve(resolver_class, args: {}, lookahead: :not_given, parent: :not_given, **resolver_args)
+ args = aliased_args(resolver_class, args)
+ args[:parent] = parent unless parent == :not_given
+ args[:lookahead] = lookahead unless lookahead == :not_given
resolver = resolver_instance(resolver_class, **resolver_args)
ready, early_return = sync_all { resolver.ready?(**args) }
@@ -26,6 +34,16 @@ module GraphqlHelpers
resolver.resolve(**args)
end
+ # TODO: Remove this method entirely when GraphqlHelpers uses real resolve_field
+ # see: https://gitlab.com/gitlab-org/gitlab/-/issues/287791
+ def aliased_args(resolver, args)
+ definitions = resolver.arguments
+
+ args.transform_keys do |k|
+ definitions[GraphqlHelpers.fieldnamerize(k)]&.keyword || k
+ end
+ end
+
def resolver_instance(resolver_class, obj: nil, ctx: {}, field: nil, schema: GitlabSchema)
if ctx.is_a?(Hash)
q = double('Query', schema: schema)
@@ -111,24 +129,33 @@ module GraphqlHelpers
def variables_for_mutation(name, input)
graphql_input = prepare_input_for_mutation(input)
- result = { input_variable_name_for_mutation(name) => graphql_input }
+ { input_variable_name_for_mutation(name) => graphql_input }
+ end
- # Avoid trying to serialize multipart data into JSON
- if graphql_input.values.none? { |value| io_value?(value) }
- result.to_json
- else
- result
- end
+ def serialize_variables(variables)
+ return unless variables
+ return variables if variables.is_a?(String)
+
+ ::Gitlab::Utils::MergeHash.merge(Array.wrap(variables).map(&:to_h)).to_json
end
- def resolve_field(name, object, args = {})
- context = double("Context",
- schema: GitlabSchema,
- query: GraphQL::Query.new(GitlabSchema),
- parent: nil)
- field = described_class.fields[name]
+ def resolve_field(name, object, args = {}, current_user: nil)
+ q = GraphQL::Query.new(GitlabSchema)
+ context = GraphQL::Query::Context.new(query: q, object: object, values: { current_user: current_user })
+ allow(context).to receive(:parent).and_return(nil)
+ field = described_class.fields.fetch(GraphqlHelpers.fieldnamerize(name))
instance = described_class.authorized_new(object, context)
- field.resolve_field(instance, {}, context)
+ raise UnauthorizedObject unless instance
+
+ field.resolve_field(instance, args, context)
+ end
+
+ def simple_resolver(resolved_value = 'Resolved value')
+ Class.new(Resolvers::BaseResolver) do
+ define_method :resolve do |**_args|
+ resolved_value
+ end
+ end
end
# Recursively convert a Hash with Ruby-style keys to GraphQL fieldname-style keys
@@ -165,10 +192,32 @@ module GraphqlHelpers
end
def query_graphql_field(name, attributes = {}, fields = nil)
- <<~QUERY
- #{field_with_params(name, attributes)}
- #{wrap_fields(fields || all_graphql_fields_for(name.to_s.classify))}
- QUERY
+ attributes, fields = [nil, attributes] if fields.nil? && !attributes.is_a?(Hash)
+
+ field = field_with_params(name, attributes)
+
+ field + wrap_fields(fields || all_graphql_fields_for(name.to_s.classify)).to_s
+ end
+
+ def page_info_selection
+ "pageInfo { hasNextPage hasPreviousPage endCursor startCursor }"
+ end
+
+ def query_nodes(name, fields = nil, args: nil, of: name, include_pagination_info: false, max_depth: 1)
+ fields ||= all_graphql_fields_for(of.to_s.classify, max_depth: max_depth)
+ node_selection = include_pagination_info ? "#{page_info_selection} nodes" : :nodes
+ query_graphql_path([[name, args], node_selection], fields)
+ end
+
+ # e.g:
+ # query_graphql_path(%i[foo bar baz], all_graphql_fields_for('Baz'))
+ # => foo { bar { baz { x y z } } }
+ def query_graphql_path(segments, fields = nil)
+ # we really want foldr here...
+ segments.reverse.reduce(fields) do |tail, segment|
+ name, args = Array.wrap(segment)
+ query_graphql_field(name, args, tail)
+ end
end
def wrap_fields(fields)
@@ -203,50 +252,22 @@ module GraphqlHelpers
type = GitlabSchema.types[class_name.to_s]
return "" unless type
- type.fields.map do |name, field|
- # We can't guess arguments, so skip fields that require them
- next if required_arguments?(field)
- next if excluded.include?(name)
-
- singular_field_type = field_type(field)
-
- # 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)
+ # We can't guess arguments, so skip fields that require them
+ skip = ->(name, field) { excluded.include?(name) || required_arguments?(field) }
- if nested_fields?(field)
- fields =
- all_graphql_fields_for(singular_field_type, parent_types | [type], max_depth: max_depth - 1)
-
- "#{name} { #{fields} }" unless fields.blank?
- else
- name
- end
- end.compact.join("\n")
+ ::Graphql::FieldSelection.select_fields(type, skip, parent_types, max_depth)
end
- def attributes_to_graphql(attributes)
- attributes.map do |name, value|
- value_str = as_graphql_literal(value)
+ def with_signature(variables, query)
+ %Q[query(#{variables.map(&:sig).join(', ')}) #{query}]
+ end
- "#{GraphqlHelpers.fieldnamerize(name.to_s)}: #{value_str}"
- end.join(", ")
+ def var(type)
+ ::Graphql::Var.new(generate(:variable), type)
end
- # Fairly dumb Ruby => GraphQL rendering function. Only suitable for testing.
- # Use symbol for Enum values
- def as_graphql_literal(value)
- case value
- when Array then "[#{value.map { |v| as_graphql_literal(v) }.join(',')}]"
- when Hash then "{#{attributes_to_graphql(value)}}"
- when Integer, Float then value.to_s
- when String then "\"#{value.gsub(/"/, '\\"')}\""
- when Symbol then value
- when nil then 'null'
- when true then 'true'
- when false then 'false'
- else raise ArgumentError, "Cannot represent #{value} as GraphQL literal"
- end
+ def attributes_to_graphql(arguments)
+ ::Graphql::Arguments.new(arguments).to_s
end
def post_multiplex(queries, current_user: nil, headers: {})
@@ -254,7 +275,7 @@ module GraphqlHelpers
end
def post_graphql(query, current_user: nil, variables: nil, headers: {})
- params = { query: query, variables: variables&.to_json }
+ params = { query: query, variables: serialize_variables(variables) }
post api('/', current_user, version: 'graphql'), params: params, headers: headers
end
@@ -320,36 +341,47 @@ module GraphqlHelpers
{ operations: operations.to_json, map: map.to_json }.merge(extracted_files)
end
+ def fresh_response_data
+ Gitlab::Json.parse(response.body)
+ end
+
# Raises an error if no data is found
- def graphql_data
+ def graphql_data(body = json_response)
# Note that `json_response` is defined as `let(:json_response)` and
# therefore, in a spec with multiple queries, will only contain data
# from the _first_ query, not subsequent ones
- json_response['data'] || (raise NoData, graphql_errors)
+ body['data'] || (raise NoData, graphql_errors(body))
end
def graphql_data_at(*path)
graphql_dig_at(graphql_data, *path)
end
+ # Slightly more powerful than just `dig`:
+ # - also supports implicit flat-mapping (.e.g. :foo :nodes :bar :nodes)
def graphql_dig_at(data, *path)
keys = path.map { |segment| segment.is_a?(Integer) ? segment : GraphqlHelpers.fieldnamerize(segment) }
# Allows for array indexing, like this
# ['project', 'boards', 'edges', 0, 'node', 'lists']
keys.reduce(data) do |memo, key|
- memo.is_a?(Array) ? memo[key] : memo&.dig(key)
+ if memo.is_a?(Array)
+ key.is_a?(Integer) ? memo[key] : memo.flat_map { |e| Array.wrap(e[key]) }
+ else
+ memo&.dig(key)
+ end
end
end
- def graphql_errors
- case json_response
+ # See note at graphql_data about memoization and multiple requests
+ def graphql_errors(body = json_response)
+ case body
when Hash # regular query
- json_response['errors']
+ body['errors']
when Array # multiplexed queries
- json_response.map { |response| response['errors'] }
+ body.map { |response| response['errors'] }
else
- raise "Unknown GraphQL response type #{json_response.class}"
+ raise "Unknown GraphQL response type #{body.class}"
end
end
@@ -392,19 +424,29 @@ module GraphqlHelpers
end
def nested_fields?(field)
- !scalar?(field) && !enum?(field)
+ ::Graphql::FieldInspection.new(field).nested_fields?
end
def scalar?(field)
- field_type(field).kind.scalar?
+ ::Graphql::FieldInspection.new(field).scalar?
end
def enum?(field)
- field_type(field).kind.enum?
+ ::Graphql::FieldInspection.new(field).enum?
end
+ # There are a few non BaseField fields in our schema (pageInfo for one).
+ # None of them require arguments.
def required_arguments?(field)
- field.arguments.values.any? { |argument| argument.type.non_null? }
+ return field.requires_argument? if field.is_a?(::Types::BaseField)
+
+ if (meta = field.try(:metadata)) && meta[:type_class]
+ required_arguments?(meta[:type_class])
+ elsif args = field.try(:arguments)
+ args.values.any? { |argument| argument.type.non_null? }
+ else
+ false
+ end
end
def io_value?(value)
@@ -412,15 +454,7 @@ module GraphqlHelpers
end
def field_type(field)
- field_type = field.type.respond_to?(:to_graphql) ? field.type.to_graphql : field.type
-
- # The type could be nested. For example `[GraphQL::STRING_TYPE]`:
- # - List
- # - String!
- # - String
- field_type = field_type.of_type while field_type.respond_to?(:of_type)
-
- field_type
+ ::Graphql::FieldInspection.new(field).type
end
# for most tests, we want to allow unlimited complexity
@@ -498,6 +532,20 @@ module GraphqlHelpers
variables: {}
)
end
+
+ # A lookahead that selects everything
+ def positive_lookahead
+ double(selects?: true).tap do |selection|
+ allow(selection).to receive(:selection).and_return(selection)
+ end
+ end
+
+ # A lookahead that selects nothing
+ def negative_lookahead
+ double(selects?: false).tap do |selection|
+ allow(selection).to receive(:selection).and_return(selection)
+ end
+ end
end
# This warms our schema, doing this as part of loading the helpers to avoid
diff --git a/spec/support/helpers/login_helpers.rb b/spec/support/helpers/login_helpers.rb
index e21d4497cda..86022e16d71 100644
--- a/spec/support/helpers/login_helpers.rb
+++ b/spec/support/helpers/login_helpers.rb
@@ -138,13 +138,15 @@ module LoginHelpers
secret: 'mock_secret'
},
extra: {
- raw_info: {
- info: {
- name: 'mockuser',
- email: email,
- image: 'mock_user_thumbnail_url'
+ raw_info: OneLogin::RubySaml::Attributes.new(
+ {
+ info: {
+ name: 'mockuser',
+ email: email,
+ image: 'mock_user_thumbnail_url'
+ }
}
- },
+ ),
response_object: response_object
}
}).merge(additional_info) { |_, old_hash, new_hash| old_hash.merge(new_hash) }
@@ -198,7 +200,7 @@ module LoginHelpers
env['omniauth.error.strategy'] = strategy
end
- def stub_omniauth_saml_config(messages, context: Rails.application)
+ def stub_omniauth_saml_config(context: Rails.application, **messages)
set_devise_mapping(context: context)
routes = Rails.application.routes
routes.disable_clear_and_finalize = true
diff --git a/spec/support/helpers/multipart_helpers.rb b/spec/support/helpers/multipart_helpers.rb
index 2e8db0e9e42..bcb184f84c5 100644
--- a/spec/support/helpers/multipart_helpers.rb
+++ b/spec/support/helpers/multipart_helpers.rb
@@ -37,7 +37,7 @@ module MultipartHelpers
# *without* the "#{key}." prefix
result.deep_transform_keys! { |k| k.remove("#{key}.") }
{
- "#{key}.gitlab-workhorse-upload" => jwt_token('upload' => result)
+ "#{key}.gitlab-workhorse-upload" => jwt_token(data: { 'upload' => result })
}
end
diff --git a/spec/support/helpers/next_instance_of.rb b/spec/support/helpers/next_instance_of.rb
index 83c788c3d38..a8e9ab2bafe 100644
--- a/spec/support/helpers/next_instance_of.rb
+++ b/spec/support/helpers/next_instance_of.rb
@@ -1,28 +1,31 @@
# frozen_string_literal: true
module NextInstanceOf
- def expect_next_instance_of(klass, *new_args)
- stub_new(expect(klass), *new_args) do |expectation|
- yield(expectation)
- end
+ def expect_next_instance_of(klass, *new_args, &blk)
+ stub_new(expect(klass), nil, *new_args, &blk)
end
- def allow_next_instance_of(klass, *new_args)
- stub_new(allow(klass), *new_args) do |allowance|
- yield(allowance)
- end
+ def expect_next_instances_of(klass, number, *new_args, &blk)
+ stub_new(expect(klass), number, *new_args, &blk)
+ end
+
+ def allow_next_instance_of(klass, *new_args, &blk)
+ stub_new(allow(klass), nil, *new_args, &blk)
+ end
+
+ def allow_next_instances_of(klass, number, *new_args, &blk)
+ stub_new(allow(klass), number, *new_args, &blk)
end
private
- def stub_new(target, *new_args)
+ def stub_new(target, number, *new_args, &blk)
receive_new = receive(:new)
+ receive_new.exactly(number).times if number
receive_new.with(*new_args) if new_args.any?
target.to receive_new.and_wrap_original do |method, *original_args|
- method.call(*original_args).tap do |instance|
- yield(instance)
- end
+ method.call(*original_args).tap(&blk)
end
end
end
diff --git a/spec/support/helpers/services_helper.rb b/spec/support/helpers/services_helper.rb
new file mode 100644
index 00000000000..bf007815551
--- /dev/null
+++ b/spec/support/helpers/services_helper.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+require_relative './after_next_helpers'
+
+module ServicesHelper
+ include AfterNextHelpers
+
+ def expect_execution_of(service_class, *args)
+ expect_next(service_class, *args).to receive(:execute)
+ end
+end
diff --git a/spec/support/helpers/snowplow_helpers.rb b/spec/support/helpers/snowplow_helpers.rb
index 15eac1b24fc..70a4eadd8de 100644
--- a/spec/support/helpers/snowplow_helpers.rb
+++ b/spec/support/helpers/snowplow_helpers.rb
@@ -31,7 +31,31 @@ module SnowplowHelpers
# )
# end
# end
- def expect_snowplow_event(category:, action:, **kwargs)
+ #
+ # Passing context:
+ #
+ # Simply provide a hash that has the schema and data expected.
+ #
+ # expect_snowplow_event(
+ # category: 'Experiment',
+ # action: 'created',
+ # context: [
+ # {
+ # schema: 'iglu:com.gitlab/.../0-3-0',
+ # data: { key: 'value' }
+ # }
+ # ]
+ # )
+ def expect_snowplow_event(category:, action:, context: nil, **kwargs)
+ if context
+ kwargs[:context] = []
+ context.each do |c|
+ expect(SnowplowTracker::SelfDescribingJson).to have_received(:new)
+ .with(c[:schema], c[:data]).at_least(:once)
+ kwargs[:context] << an_instance_of(SnowplowTracker::SelfDescribingJson)
+ end
+ end
+
expect(Gitlab::Tracking).to have_received(:event) # rubocop:disable RSpec/ExpectGitlabTracking
.with(category, action, **kwargs).at_least(:once)
end
diff --git a/spec/support/helpers/stub_experiments.rb b/spec/support/helpers/stub_experiments.rb
index 7a6154d5ef9..247692d83ee 100644
--- a/spec/support/helpers/stub_experiments.rb
+++ b/spec/support/helpers/stub_experiments.rb
@@ -3,15 +3,15 @@
module StubExperiments
# Stub Experiment with `key: true/false`
#
- # @param [Hash] experiment where key is feature name and value is boolean whether enabled or not.
+ # @param [Hash] experiment where key is feature name and value is boolean whether active or not.
#
# Examples
- # - `stub_experiment(signup_flow: false)` ... Disable `signup_flow` experiment globally.
+ # - `stub_experiment(signup_flow: false)` ... Disables `signup_flow` experiment.
def stub_experiment(experiments)
- allow(Gitlab::Experimentation).to receive(:enabled?).and_call_original
+ allow(Gitlab::Experimentation).to receive(:active?).and_call_original
experiments.each do |experiment_key, enabled|
- allow(Gitlab::Experimentation).to receive(:enabled?).with(experiment_key) { enabled }
+ allow(Gitlab::Experimentation).to receive(:active?).with(experiment_key) { enabled }
end
end
@@ -20,12 +20,12 @@ module StubExperiments
# @param [Hash] experiment where key is feature name and value is boolean whether enabled or not.
#
# Examples
- # - `stub_experiment_for_user(signup_flow: false)` ... Disable `signup_flow` experiment for user.
- def stub_experiment_for_user(experiments)
- allow(Gitlab::Experimentation).to receive(:enabled_for_value?).and_call_original
+ # - `stub_experiment_for_subject(signup_flow: false)` ... Disable `signup_flow` experiment for user.
+ def stub_experiment_for_subject(experiments)
+ allow(Gitlab::Experimentation).to receive(:in_experiment_group?).and_call_original
experiments.each do |experiment_key, enabled|
- allow(Gitlab::Experimentation).to receive(:enabled_for_value?).with(experiment_key, anything) { enabled }
+ allow(Gitlab::Experimentation).to receive(:in_experiment_group?).with(experiment_key, anything) { enabled }
end
end
end
diff --git a/spec/support/helpers/stub_object_storage.rb b/spec/support/helpers/stub_object_storage.rb
index dba3d2b137e..dc54a21d0fa 100644
--- a/spec/support/helpers/stub_object_storage.rb
+++ b/spec/support/helpers/stub_object_storage.rb
@@ -22,6 +22,16 @@ module StubObjectStorage
background_upload: false,
direct_upload: false
)
+ new_config = config.to_h.deep_symbolize_keys.merge({
+ enabled: enabled,
+ proxy_download: proxy_download,
+ background_upload: background_upload,
+ direct_upload: direct_upload
+ })
+
+ # Needed for ObjectStorage::Config compatibility
+ allow(config).to receive(:to_hash).and_return(new_config)
+ allow(config).to receive(:to_h).and_return(new_config)
allow(config).to receive(:enabled) { enabled }
allow(config).to receive(:proxy_download) { proxy_download }
allow(config).to receive(:background_upload) { background_upload }
@@ -84,13 +94,6 @@ module StubObjectStorage
def stub_terraform_state_object_storage(**params)
stub_object_storage_uploader(config: Gitlab.config.terraform_state.object_store,
- uploader: Terraform::VersionedStateUploader,
- remote_directory: 'terraform',
- **params)
- end
-
- def stub_terraform_state_version_object_storage(**params)
- stub_object_storage_uploader(config: Gitlab.config.terraform_state.object_store,
uploader: Terraform::StateUploader,
remote_directory: 'terraform',
**params)
diff --git a/spec/support/helpers/test_env.rb b/spec/support/helpers/test_env.rb
index 4c78ca0117c..01571277a1d 100644
--- a/spec/support/helpers/test_env.rb
+++ b/spec/support/helpers/test_env.rb
@@ -168,6 +168,11 @@ module TestEnv
version: Gitlab::GitalyClient.expected_server_version,
task: "gitlab:gitaly:install[#{install_gitaly_args}]") do
Gitlab::SetupHelper::Gitaly.create_configuration(gitaly_dir, { 'default' => repos_path }, force: true)
+ Gitlab::SetupHelper::Gitaly.create_configuration(
+ gitaly_dir,
+ { 'default' => repos_path }, force: true,
+ options: { gitaly_socket: "gitaly2.socket", config_filename: "gitaly2.config.toml" }
+ )
Gitlab::SetupHelper::Praefect.create_configuration(gitaly_dir, { 'praefect' => repos_path }, force: true)
end
@@ -241,15 +246,38 @@ module TestEnv
end
def setup_workhorse
- install_workhorse_args = [workhorse_dir, workhorse_url].compact.join(',')
-
- component_timed_setup(
- 'GitLab Workhorse',
- install_dir: workhorse_dir,
- version: Gitlab::Workhorse.version,
- task: "gitlab:workhorse:install[#{install_workhorse_args}]") do
- Gitlab::SetupHelper::Workhorse.create_configuration(workhorse_dir, nil)
- end
+ start = Time.now
+ return if skip_compile_workhorse?
+
+ puts "\n==> Setting up GitLab Workhorse..."
+
+ FileUtils.rm_rf(workhorse_dir)
+ Gitlab::SetupHelper::Workhorse.compile_into(workhorse_dir)
+ Gitlab::SetupHelper::Workhorse.create_configuration(workhorse_dir, nil)
+
+ File.write(workhorse_tree_file, workhorse_tree) if workhorse_source_clean?
+
+ puts " GitLab Workhorse set up in #{Time.now - start} seconds...\n"
+ end
+
+ def skip_compile_workhorse?
+ File.directory?(workhorse_dir) &&
+ workhorse_source_clean? &&
+ File.exist?(workhorse_tree_file) &&
+ workhorse_tree == File.read(workhorse_tree_file)
+ end
+
+ def workhorse_source_clean?
+ out = IO.popen(%w[git status --porcelain workhorse], &:read)
+ $?.success? && out.empty?
+ end
+
+ def workhorse_tree
+ IO.popen(%w[git rev-parse HEAD:workhorse], &:read)
+ end
+
+ def workhorse_tree_file
+ File.join(workhorse_dir, 'WORKHORSE_TREE')
end
def workhorse_dir
@@ -260,7 +288,7 @@ module TestEnv
host = "[#{host}]" if host.include?(':')
listen_addr = [host, port].join(':')
- config_path = Gitlab::SetupHelper::Workhorse.get_config_path(workhorse_dir)
+ config_path = Gitlab::SetupHelper::Workhorse.get_config_path(workhorse_dir, {})
# This should be set up in setup_workhorse, but since
# component_needs_update? only checks that versions are consistent,
diff --git a/spec/support/helpers/usage_data_helpers.rb b/spec/support/helpers/usage_data_helpers.rb
index 8e8aeea2ea1..df79049123d 100644
--- a/spec/support/helpers/usage_data_helpers.rb
+++ b/spec/support/helpers/usage_data_helpers.rb
@@ -85,6 +85,7 @@ module UsageDataHelpers
projects
projects_imported_from_github
projects_asana_active
+ projects_jenkins_active
projects_jira_active
projects_jira_server_active
projects_jira_cloud_active
@@ -161,7 +162,6 @@ module UsageDataHelpers
git
gitaly
database
- avg_cycle_analytics
prometheus_metrics_enabled
web_ide_clientside_preview_enabled
ingress_modsecurity_enabled
diff --git a/spec/support/helpers/workhorse_helpers.rb b/spec/support/helpers/workhorse_helpers.rb
index 7e95f49aea2..cd8387de686 100644
--- a/spec/support/helpers/workhorse_helpers.rb
+++ b/spec/support/helpers/workhorse_helpers.rb
@@ -85,15 +85,15 @@ module WorkhorseHelpers
return {} if upload_params.empty?
- { "#{key}.gitlab-workhorse-upload" => jwt_token('upload' => upload_params) }
+ { "#{key}.gitlab-workhorse-upload" => jwt_token(data: { 'upload' => upload_params }) }
end
- def jwt_token(data = {}, issuer: 'gitlab-workhorse', secret: Gitlab::Workhorse.secret, algorithm: 'HS256')
+ def jwt_token(data: {}, issuer: 'gitlab-workhorse', secret: Gitlab::Workhorse.secret, algorithm: 'HS256')
JWT.encode({ 'iss' => issuer }.merge(data), secret, algorithm)
end
def workhorse_rewritten_fields_header(fields)
- { Gitlab::Middleware::Multipart::RACK_ENV_KEY => jwt_token('rewritten_fields' => fields) }
+ { Gitlab::Middleware::Multipart::RACK_ENV_KEY => jwt_token(data: { 'rewritten_fields' => fields }) }
end
def workhorse_disk_accelerated_file_params(key, file)
diff --git a/spec/support/import_export/common_util.rb b/spec/support/import_export/common_util.rb
index ae951ea35af..a6b395ad4d5 100644
--- a/spec/support/import_export/common_util.rb
+++ b/spec/support/import_export/common_util.rb
@@ -70,9 +70,12 @@ module ImportExport
)
end
- def get_shared_env(path:)
+ def get_shared_env(path:, logger: nil)
+ logger ||= double(info: true, warn: true, error: true)
+
instance_double(Gitlab::ImportExport::Shared).tap do |shared|
allow(shared).to receive(:export_path).and_return(path)
+ allow(shared).to receive(:logger).and_return(logger)
end
end
diff --git a/spec/support/matchers/access_matchers.rb b/spec/support/matchers/access_matchers.rb
index c9ff777f604..acf5fb0944f 100644
--- a/spec/support/matchers/access_matchers.rb
+++ b/spec/support/matchers/access_matchers.rb
@@ -52,7 +52,7 @@ module AccessMatchers
emulate_user(user, @membership)
visit(url)
- status_code == 200 && current_path != new_user_session_path
+ status_code == 200 && !current_path.in?([new_user_session_path, new_admin_session_path])
end
chain :of do |membership|
@@ -67,7 +67,7 @@ module AccessMatchers
emulate_user(user, @membership)
visit(url)
- [401, 404].include?(status_code) || current_path == new_user_session_path
+ [401, 404, 403].include?(status_code) || current_path.in?([new_user_session_path, new_admin_session_path])
end
chain :of do |membership|
diff --git a/spec/support/matchers/be_valid_json.rb b/spec/support/matchers/be_valid_json.rb
new file mode 100644
index 00000000000..f46c35c7198
--- /dev/null
+++ b/spec/support/matchers/be_valid_json.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+RSpec::Matchers.define :be_valid_json do
+ def according_to_schema(schema)
+ @schema = schema
+ self
+ end
+
+ match do |actual|
+ data = Gitlab::Json.parse(actual)
+
+ if @schema.present?
+ @validation_errors = JSON::Validator.fully_validate(@schema, data)
+ @validation_errors.empty?
+ else
+ data.present?
+ end
+ rescue JSON::ParserError => e
+ @error = e
+ false
+ end
+
+ def failure_message
+ if @error
+ "Parse failed with error: #{@error}"
+ elsif @validation_errors.present?
+ "Validation failed because #{@validation_errors.join(', and ')}"
+ else
+ "Parsing did not return any data"
+ end
+ end
+end
diff --git a/spec/support/matchers/exceed_query_limit.rb b/spec/support/matchers/exceed_query_limit.rb
index 04482d3bfb8..7a66eff3a41 100644
--- a/spec/support/matchers/exceed_query_limit.rb
+++ b/spec/support/matchers/exceed_query_limit.rb
@@ -3,6 +3,13 @@
module ExceedQueryLimitHelpers
MARGINALIA_ANNOTATION_REGEX = %r{\s*\/\*.*\*\/}.freeze
+ DB_QUERY_RE = Regexp.union([
+ /^(?<prefix>SELECT .* FROM "?[a-z_]+"?) (?<suffix>.*)$/m,
+ /^(?<prefix>UPDATE "?[a-z_]+"?) (?<suffix>.*)$/m,
+ /^(?<prefix>INSERT INTO "[a-z_]+" \((?:"[a-z_]+",?\s?)+\)) (?<suffix>.*)$/m,
+ /^(?<prefix>DELETE FROM "[a-z_]+") (?<suffix>.*)$/m
+ ]).freeze
+
def with_threshold(threshold)
@threshold = threshold
self
@@ -13,41 +20,129 @@ module ExceedQueryLimitHelpers
self
end
+ def show_common_queries
+ @show_common_queries = true
+ self
+ end
+
+ def ignoring(pattern)
+ @ignoring_pattern = pattern
+ self
+ end
+
def threshold
@threshold.to_i
end
def expected_count
if expected.is_a?(ActiveRecord::QueryRecorder)
- expected.count
+ query_recorder_count(expected)
else
expected
end
end
def actual_count
- @actual_count ||= if @query
- recorder.log.select { |recorded| recorded =~ @query }.size
- else
- recorder.count
- end
+ @actual_count ||= query_recorder_count(recorder)
+ end
+
+ def query_recorder_count(query_recorder)
+ return query_recorder.count unless @query || @ignoring_pattern
+
+ query_log(query_recorder).size
+ end
+
+ def query_log(query_recorder)
+ filtered = query_recorder.log
+ filtered = filtered.select { |q| q =~ @query } if @query
+ filtered = filtered.reject { |q| q =~ @ignoring_pattern } if @ignoring_pattern
+ filtered
end
def recorder
@recorder ||= ActiveRecord::QueryRecorder.new(skip_cached: skip_cached, &@subject_block)
end
- def count_queries(queries)
- queries.each_with_object(Hash.new(0)) { |query, counts| counts[query] += 1 }
+ # Take a query recorder and tabulate the frequencies of suffixes for each prefix.
+ #
+ # @return Hash[String, Hash[String, Int]]
+ #
+ # Example:
+ #
+ # r = ActiveRecord::QueryRecorder.new do
+ # SomeTable.create(x: 1, y: 2, z: 3)
+ # SomeOtherTable.where(id: 1).first
+ # SomeTable.create(x: 4, y: 5, z: 6)
+ # SomeOtherTable.all
+ # end
+ # count_queries(r)
+ # #=>
+ # {
+ # 'INSERT INTO "some_table" VALUES' => {
+ # '(1,2,3)' => 1,
+ # '(4,5,6)' => 1
+ # },
+ # 'SELECT * FROM "some_other_table"' => {
+ # 'WHERE id = 1 LIMIT 1' => 1,
+ # '' => 2
+ # }
+ # }
+ def count_queries(query_recorder)
+ strip_marginalia_annotations(query_log(query_recorder))
+ .map { |q| query_group_key(q) }
+ .group_by { |k| k[:prefix] }
+ .transform_values { |keys| frequencies(:suffix, keys) }
+ end
+
+ def frequencies(key, things)
+ things.group_by { |x| x[key] }.transform_values(&:size)
+ end
+
+ def query_group_key(query)
+ DB_QUERY_RE.match(query) || { prefix: query, suffix: '' }
+ 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? }
+ end
+
+ def diff_query_group_message(query, suffixes)
+ suffix_messages = suffixes.map do |s, counts|
+ "-- (expected: #{counts.first}, got: #{counts.second})\n #{s}"
+ end
+
+ "#{query}...\n#{suffix_messages.join("\n")}"
end
def log_message
if expected.is_a?(ActiveRecord::QueryRecorder)
- counts = count_queries(strip_marginalia_annotations(expected.log))
- extra_queries = strip_marginalia_annotations(@recorder.log).reject { |query| counts[query] -= 1 unless counts[query] == 0 }
- extra_queries_display = count_queries(extra_queries).map { |query, count| "[#{count}] #{query}" }
-
- (['Extra queries:'] + extra_queries_display).join("\n\n")
+ diff_counts = diff_query_counts(count_queries(expected), count_queries(@recorder))
+ sections = diff_counts.map { |q, suffixes| diff_query_group_message(q, suffixes) }
+
+ <<~MSG
+ Query Diff:
+ -----------
+ #{sections.join("\n\n")}
+ MSG
else
@recorder.log_message
end
diff --git a/spec/support/services/issuable_import_csv_service_shared_examples.rb b/spec/support/services/issuable_import_csv_service_shared_examples.rb
index 20ac2ff5c7c..f68750bec32 100644
--- a/spec/support/services/issuable_import_csv_service_shared_examples.rb
+++ b/spec/support/services/issuable_import_csv_service_shared_examples.rb
@@ -26,29 +26,33 @@ RSpec.shared_examples 'issuable import csv service' do |issuable_type|
end
end
+ shared_examples_for 'invalid file' do
+ it 'returns invalid file error' do
+ expect(subject[:success]).to eq(0)
+ expect(subject[:parse_error]).to eq(true)
+ end
+
+ it_behaves_like 'importer with email notification'
+ it_behaves_like 'an issuable importer'
+ end
+
describe '#execute' do
- context 'invalid file' do
+ context 'invalid file extension' do
let(:file) { fixture_file_upload('spec/fixtures/banana_sample.gif') }
- it 'returns invalid file error' do
- expect(subject[:success]).to eq(0)
- expect(subject[:parse_error]).to eq(true)
- end
+ it_behaves_like 'invalid file'
+ end
- it_behaves_like 'importer with email notification'
- it_behaves_like 'an issuable importer'
+ context 'empty file' do
+ let(:file) { fixture_file_upload('spec/fixtures/csv_empty.csv') }
+
+ it_behaves_like 'invalid file'
end
context 'file without headers' do
let(:file) { fixture_file_upload('spec/fixtures/csv_no_headers.csv') }
- it 'returns invalid file error' do
- expect(subject[:success]).to eq(0)
- expect(subject[:parse_error]).to eq(true)
- end
-
- it_behaves_like 'importer with email notification'
- it_behaves_like 'an issuable importer'
+ it_behaves_like 'invalid file'
end
context 'with a file generated by Gitlab CSV export' do
diff --git a/spec/support/shared_contexts/finders/group_projects_finder_shared_contexts.rb b/spec/support/shared_contexts/finders/group_projects_finder_shared_contexts.rb
index 68ff16922d8..6b15eadc1c1 100644
--- a/spec/support/shared_contexts/finders/group_projects_finder_shared_contexts.rb
+++ b/spec/support/shared_contexts/finders/group_projects_finder_shared_contexts.rb
@@ -9,13 +9,13 @@ RSpec.shared_context 'GroupProjectsFinder context' do
let(:finder) { described_class.new(group: group, current_user: current_user, params: params, options: options) }
- let_it_be(:public_project) { create(:project, :public, group: group, path: '1') }
- let_it_be(:private_project) { create(:project, :private, group: group, path: '2') }
- let_it_be(:shared_project_1) { create(:project, :public, path: '3') }
- let_it_be(:shared_project_2) { create(:project, :private, path: '4') }
- let_it_be(:shared_project_3) { create(:project, :internal, path: '5') }
- let_it_be(:subgroup_project) { create(:project, :public, path: '6', group: subgroup) }
- let_it_be(:subgroup_private_project) { create(:project, :private, path: '7', group: subgroup) }
+ let_it_be(:public_project) { create(:project, :public, group: group, path: '1', name: 'g') }
+ let_it_be(:private_project) { create(:project, :private, group: group, path: '2', name: 'f') }
+ let_it_be(:shared_project_1) { create(:project, :public, path: '3', name: 'e') }
+ let_it_be(:shared_project_2) { create(:project, :private, path: '4', name: 'd') }
+ let_it_be(:shared_project_3) { create(:project, :internal, path: '5', name: 'c') }
+ let_it_be(:subgroup_project) { create(:project, :public, path: '6', group: subgroup, name: 'b') }
+ let_it_be(:subgroup_private_project) { create(:project, :private, path: '7', group: subgroup, name: 'a') }
before do
shared_project_1.project_group_links.create!(group_access: Gitlab::Access::MAINTAINER, group: group)
diff --git a/spec/support/shared_contexts/finders/merge_requests_finder_shared_contexts.rb b/spec/support/shared_contexts/finders/merge_requests_finder_shared_contexts.rb
index 88c31bf9cfd..4c003dff947 100644
--- a/spec/support/shared_contexts/finders/merge_requests_finder_shared_contexts.rb
+++ b/spec/support/shared_contexts/finders/merge_requests_finder_shared_contexts.rb
@@ -54,19 +54,19 @@ RSpec.shared_context 'MergeRequestsFinder multiple projects with merge requests
let!(:label2) { create(:label, project: project1) }
let!(:merge_request1) do
- create(:merge_request, assignees: [user], author: user,
+ create(:merge_request, assignees: [user], author: user, reviewers: [user2],
source_project: project2, target_project: project1,
target_branch: 'merged-target')
end
let!(:merge_request2) do
- create(:merge_request, :conflict, assignees: [user], author: user,
+ create(:merge_request, :conflict, assignees: [user], author: user, reviewers: [user2],
source_project: project2, target_project: project1,
state: 'closed')
end
let!(:merge_request3) do
- create(:merge_request, :simple, author: user, assignees: [user2],
+ create(:merge_request, :simple, author: user, assignees: [user2], reviewers: [user],
source_project: project2, target_project: project2,
state: 'locked',
title: 'thing WIP thing')
diff --git a/spec/support/shared_contexts/issuable/merge_request_shared_context.rb b/spec/support/shared_contexts/issuable/merge_request_shared_context.rb
index 79fc42e73c7..0fee170a35d 100644
--- a/spec/support/shared_contexts/issuable/merge_request_shared_context.rb
+++ b/spec/support/shared_contexts/issuable/merge_request_shared_context.rb
@@ -1,8 +1,35 @@
# frozen_string_literal: true
-RSpec.shared_context 'merge request show action' do
+RSpec.shared_context 'open merge request show action' do
+ include Spec::Support::Helpers::Features::MergeRequestHelpers
+
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :public, :repository) }
+ let(:note) { create(:note_on_merge_request, project: project, noteable: open_merge_request) }
+
+ let(:open_merge_request) do
+ create(:merge_request, :opened, source_project: project, author: user)
+ end
+
+ before do
+ assign(:project, project)
+ assign(:merge_request, open_merge_request)
+ assign(:note, note)
+ assign(:noteable, open_merge_request)
+ assign(:notes, [])
+ assign(:pipelines, Ci::Pipeline.none)
+ assign(:issuable_sidebar, serialize_issuable_sidebar(user, project, open_merge_request))
+
+ preload_view_requirements(open_merge_request, note)
+
+ sign_in(user)
+ end
+end
+
+RSpec.shared_context 'closed merge request show action' do
include Devise::Test::ControllerHelpers
include ProjectForksHelper
+ include Spec::Support::Helpers::Features::MergeRequestHelpers
let(:user) { create(:user) }
let(:project) { create(:project, :public, :repository) }
@@ -17,13 +44,6 @@ RSpec.shared_context 'merge request show action' do
author: user)
end
- def preload_view_requirements
- # This will load the status fields of the author of the note and merge request
- # to avoid queries in when rendering the view being tested.
- closed_merge_request.author.status
- note.author.status
- end
-
before do
assign(:project, project)
assign(:merge_request, closed_merge_request)
@@ -34,16 +54,10 @@ RSpec.shared_context 'merge request show action' do
assign(:pipelines, Ci::Pipeline.none)
assign(:issuable_sidebar, serialize_issuable_sidebar(user, project, closed_merge_request))
- preload_view_requirements
+ preload_view_requirements(closed_merge_request, note)
allow(view).to receive_messages(current_user: user,
can?: true,
current_application_settings: Gitlab::CurrentSettings.current_application_settings)
end
-
- def serialize_issuable_sidebar(user, project, merge_request)
- MergeRequestSerializer
- .new(current_user: user, project: project)
- .represent(closed_merge_request, serializer: 'sidebar')
- end
end
diff --git a/spec/support/shared_contexts/lib/gitlab/middleware/with_a_mocked_gitlab_instance_shared_context.rb b/spec/support/shared_contexts/lib/gitlab/middleware/with_a_mocked_gitlab_instance_shared_context.rb
index 3830b89f1ff..5d65c168d8d 100644
--- a/spec/support/shared_contexts/lib/gitlab/middleware/with_a_mocked_gitlab_instance_shared_context.rb
+++ b/spec/support/shared_contexts/lib/gitlab/middleware/with_a_mocked_gitlab_instance_shared_context.rb
@@ -25,7 +25,7 @@ RSpec.shared_context 'with a mocked GitLab instance' do
let(:request) { Rack::MockRequest.new(rack_stack) }
subject do
- described_class.new(fake_app).tap do |app|
+ Gitlab::Middleware::ReadOnly.new(fake_app).tap do |app|
app.extend(observe_env)
end
end
diff --git a/spec/support/shared_contexts/navbar_structure_context.rb b/spec/support/shared_contexts/navbar_structure_context.rb
index ed74c3f179f..549dc1cff1d 100644
--- a/spec/support/shared_contexts/navbar_structure_context.rb
+++ b/spec/support/shared_contexts/navbar_structure_context.rb
@@ -14,6 +14,15 @@ RSpec.shared_context 'project navbar structure' do
}
end
+ let(:security_and_compliance_nav_item) do
+ {
+ nav_item: _('Security & Compliance'),
+ nav_sub_items: [
+ _('Audit Events')
+ ]
+ }
+ end
+
let(:structure) do
[
{
@@ -62,6 +71,7 @@ RSpec.shared_context 'project navbar structure' do
_('Schedules')
]
},
+ (security_and_compliance_nav_item if Gitlab.ee?),
{
nav_item: _('Operations'),
nav_sub_items: [
@@ -101,8 +111,7 @@ RSpec.shared_context 'project navbar structure' do
_('Access Tokens'),
_('Repository'),
_('CI / CD'),
- _('Operations'),
- (_('Audit Events') if Gitlab.ee?)
+ _('Operations')
].compact
}
].compact
@@ -128,8 +137,7 @@ RSpec.shared_context 'group navbar structure' do
_('Projects'),
_('Repository'),
_('CI / CD'),
- _('Webhooks'),
- _('Audit Events')
+ _('Webhooks')
]
}
end
@@ -143,6 +151,15 @@ RSpec.shared_context 'group navbar structure' do
}
end
+ let(:security_and_compliance_nav_item) do
+ {
+ nav_item: _('Security & Compliance'),
+ nav_sub_items: [
+ _('Audit Events')
+ ]
+ }
+ end
+
let(:push_rules_nav_item) do
{
nav_item: _('Push Rules'),
@@ -172,6 +189,7 @@ RSpec.shared_context 'group navbar structure' do
nav_item: _('Merge Requests'),
nav_sub_items: []
},
+ (security_and_compliance_nav_item if Gitlab.ee?),
(push_rules_nav_item if Gitlab.ee?),
{
nav_item: _('Kubernetes'),
diff --git a/spec/support/shared_contexts/policies/group_policy_shared_context.rb b/spec/support/shared_contexts/policies/group_policy_shared_context.rb
index af46e5474b0..e0e2a18cdd2 100644
--- a/spec/support/shared_contexts/policies/group_policy_shared_context.rb
+++ b/spec/support/shared_contexts/policies/group_policy_shared_context.rb
@@ -30,6 +30,7 @@ RSpec.shared_context 'GroupPolicy context' do
let(:owner_permissions) do
[
+ :owner_access,
:admin_group,
:admin_namespace,
:admin_group_member,
diff --git a/spec/support/shared_contexts/services_shared_context.rb b/spec/support/shared_contexts/services_shared_context.rb
index 2bd516a2339..320f7564cf9 100644
--- a/spec/support/shared_contexts/services_shared_context.rb
+++ b/spec/support/shared_contexts/services_shared_context.rb
@@ -16,6 +16,8 @@ Service.available_services_names.each do |service|
hash.merge!(k => 'secrettoken')
elsif service == 'confluence' && k == :confluence_url
hash.merge!(k => 'https://example.atlassian.net/wiki')
+ elsif service == 'datadog' && k == :datadog_site
+ hash.merge!(k => 'datadoghq.com')
elsif k =~ /^(.*_url|url|webhook)/
hash.merge!(k => "http://example.com")
elsif service_klass.method_defined?("#{k}?")
@@ -34,8 +36,7 @@ Service.available_services_names.each do |service|
let(:licensed_features) do
{
- 'github' => :github_project_service_integration,
- 'jenkins' => :jenkins_integration
+ 'github' => :github_project_service_integration
}
end
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 38a5ed244c4..f89d52f81ad 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
@@ -85,7 +85,7 @@ RSpec.shared_examples 'multiple issue boards' do
wait_for_requests
- expect(page).to have_selector('.board', count: 5)
+ expect(page).to have_selector('.board', count: 3)
in_boards_switcher_dropdown do
click_link board.name
@@ -93,7 +93,7 @@ RSpec.shared_examples 'multiple issue boards' do
wait_for_requests
- expect(page).to have_selector('.board', count: 4)
+ expect(page).to have_selector('.board', count: 2)
end
it 'maintains sidebar state over board switch' do
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 5a4322f73b6..422282da4d8 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
@@ -219,7 +219,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do
it 'returns 200 response when the project is imported successfully' do
allow(Gitlab::LegacyGithubImport::ProjectCreator)
- .to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider)
+ .to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, type: provider, **access_params)
.and_return(double(execute: project))
post :create, format: :json
@@ -233,7 +233,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do
project.errors.add(:path, 'is old')
allow(Gitlab::LegacyGithubImport::ProjectCreator)
- .to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider)
+ .to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, type: provider, **access_params)
.and_return(double(execute: project))
post :create, format: :json
@@ -244,7 +244,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do
it "touches the etag cache store" do
allow(Gitlab::LegacyGithubImport::ProjectCreator)
- .to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider)
+ .to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, type: provider, **access_params)
.and_return(double(execute: project))
expect_next_instance_of(Gitlab::EtagCaching::Store) do |store|
expect(store).to receive(:touch) { "realtime_changes_import_#{provider}_path" }
@@ -257,7 +257,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do
context "when the provider user and GitLab user's usernames match" do
it "takes the current user's namespace" do
expect(Gitlab::LegacyGithubImport::ProjectCreator)
- .to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider)
+ .to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, type: provider, **access_params)
.and_return(double(execute: project))
post :create, format: :json
@@ -269,7 +269,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do
it "takes the current user's namespace" do
expect(Gitlab::LegacyGithubImport::ProjectCreator)
- .to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider)
+ .to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, type: provider, **access_params)
.and_return(double(execute: project))
post :create, format: :json
@@ -296,7 +296,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do
it "takes the existing namespace" do
expect(Gitlab::LegacyGithubImport::ProjectCreator)
- .to receive(:new).with(provider_repo, provider_repo.name, existing_namespace, user, access_params, type: provider)
+ .to receive(:new).with(provider_repo, provider_repo.name, existing_namespace, user, type: provider, **access_params)
.and_return(double(execute: project))
post :create, format: :json
@@ -308,7 +308,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do
create(:user, username: other_username)
expect(Gitlab::LegacyGithubImport::ProjectCreator)
- .to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider)
+ .to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, type: provider, **access_params)
.and_return(double(execute: project))
post :create, format: :json
@@ -327,7 +327,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do
it "takes the new namespace" do
expect(Gitlab::LegacyGithubImport::ProjectCreator)
- .to receive(:new).with(provider_repo, provider_repo.name, an_instance_of(Group), user, access_params, type: provider)
+ .to receive(:new).with(provider_repo, provider_repo.name, an_instance_of(Group), user, type: provider, **access_params)
.and_return(double(execute: project))
post :create, params: { target_namespace: provider_repo.name }, format: :json
@@ -348,7 +348,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do
it "takes the current user's namespace" do
expect(Gitlab::LegacyGithubImport::ProjectCreator)
- .to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider)
+ .to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, type: provider, **access_params)
.and_return(double(execute: project))
post :create, format: :json
@@ -366,7 +366,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do
it 'takes the selected namespace and name' do
expect(Gitlab::LegacyGithubImport::ProjectCreator)
- .to receive(:new).with(provider_repo, test_name, test_namespace, user, access_params, type: provider)
+ .to receive(:new).with(provider_repo, test_name, test_namespace, user, type: provider, **access_params)
.and_return(double(execute: project))
post :create, params: { target_namespace: test_namespace.name, new_name: test_name }, format: :json
@@ -374,7 +374,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do
it 'takes the selected name and default namespace' do
expect(Gitlab::LegacyGithubImport::ProjectCreator)
- .to receive(:new).with(provider_repo, test_name, user.namespace, user, access_params, type: provider)
+ .to receive(:new).with(provider_repo, test_name, user.namespace, user, type: provider, **access_params)
.and_return(double(execute: project))
post :create, params: { new_name: test_name }, format: :json
@@ -393,7 +393,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do
it 'takes the selected namespace and name' do
expect(Gitlab::LegacyGithubImport::ProjectCreator)
- .to receive(:new).with(provider_repo, test_name, nested_namespace, user, access_params, type: provider)
+ .to receive(:new).with(provider_repo, test_name, nested_namespace, user, type: provider, **access_params)
.and_return(double(execute: project))
post :create, params: { target_namespace: nested_namespace.full_path, new_name: test_name }, format: :json
@@ -405,7 +405,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do
it 'takes the selected namespace and name' do
expect(Gitlab::LegacyGithubImport::ProjectCreator)
- .to receive(:new).with(provider_repo, test_name, kind_of(Namespace), user, access_params, type: provider)
+ .to receive(:new).with(provider_repo, test_name, kind_of(Namespace), user, type: provider, **access_params)
.and_return(double(execute: project))
post :create, params: { target_namespace: 'foo/bar', new_name: test_name }, format: :json
@@ -413,7 +413,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do
it 'creates the namespaces' do
allow(Gitlab::LegacyGithubImport::ProjectCreator)
- .to receive(:new).with(provider_repo, test_name, kind_of(Namespace), user, access_params, type: provider)
+ .to receive(:new).with(provider_repo, test_name, kind_of(Namespace), user, type: provider, **access_params)
.and_return(double(execute: project))
expect { post :create, params: { target_namespace: 'foo/bar', new_name: test_name }, format: :json }
@@ -422,7 +422,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do
it 'new namespace has the right parent' do
allow(Gitlab::LegacyGithubImport::ProjectCreator)
- .to receive(:new).with(provider_repo, test_name, kind_of(Namespace), user, access_params, type: provider)
+ .to receive(:new).with(provider_repo, test_name, kind_of(Namespace), user, type: provider, **access_params)
.and_return(double(execute: project))
post :create, params: { target_namespace: 'foo/bar', new_name: test_name }, format: :json
@@ -441,7 +441,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do
it 'takes the selected namespace and name' do
expect(Gitlab::LegacyGithubImport::ProjectCreator)
- .to receive(:new).with(provider_repo, test_name, kind_of(Namespace), user, access_params, type: provider)
+ .to receive(:new).with(provider_repo, test_name, kind_of(Namespace), user, type: provider, **access_params)
.and_return(double(execute: project))
post :create, params: { target_namespace: 'foo/foobar/bar', new_name: test_name }, format: :json
@@ -449,7 +449,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do
it 'creates the namespaces' do
allow(Gitlab::LegacyGithubImport::ProjectCreator)
- .to receive(:new).with(provider_repo, test_name, kind_of(Namespace), user, access_params, type: provider)
+ .to receive(:new).with(provider_repo, test_name, kind_of(Namespace), user, type: provider, **access_params)
.and_return(double(execute: project))
expect { post :create, params: { target_namespace: 'foo/foobar/bar', new_name: test_name }, format: :json }
@@ -458,7 +458,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do
it 'does not create a new namespace under the user namespace' do
expect(Gitlab::LegacyGithubImport::ProjectCreator)
- .to receive(:new).with(provider_repo, test_name, user.namespace, user, access_params, type: provider)
+ .to receive(:new).with(provider_repo, test_name, user.namespace, user, type: provider, **access_params)
.and_return(double(execute: project))
expect { post :create, params: { target_namespace: "#{user.namespace_path}/test_group", new_name: test_name }, format: :js }
@@ -472,7 +472,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do
it 'does not take the selected namespace and name' do
expect(Gitlab::LegacyGithubImport::ProjectCreator)
- .to receive(:new).with(provider_repo, test_name, user.namespace, user, access_params, type: provider)
+ .to receive(:new).with(provider_repo, test_name, user.namespace, user, type: provider, **access_params)
.and_return(double(execute: project))
post :create, params: { target_namespace: 'foo/foobar/bar', new_name: test_name }, format: :js
@@ -480,7 +480,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do
it 'does not create the namespaces' do
allow(Gitlab::LegacyGithubImport::ProjectCreator)
- .to receive(:new).with(provider_repo, test_name, kind_of(Namespace), user, access_params, type: provider)
+ .to receive(:new).with(provider_repo, test_name, kind_of(Namespace), user, type: provider, **access_params)
.and_return(double(execute: project))
expect { post :create, params: { target_namespace: 'foo/foobar/bar', new_name: test_name }, format: :js }
@@ -497,7 +497,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do
user.update!(can_create_group: false)
expect(Gitlab::LegacyGithubImport::ProjectCreator)
- .to receive(:new).with(provider_repo, test_name, group, user, access_params, type: provider)
+ .to receive(:new).with(provider_repo, test_name, group, user, type: provider, **access_params)
.and_return(double(execute: project))
post :create, params: { target_namespace: 'foo', new_name: test_name }, format: :js
diff --git a/spec/support/shared_examples/controllers/repositories/git_http_controller_shared_examples.rb b/spec/support/shared_examples/controllers/repositories/git_http_controller_shared_examples.rb
new file mode 100644
index 00000000000..9b738a4b002
--- /dev/null
+++ b/spec/support/shared_examples/controllers/repositories/git_http_controller_shared_examples.rb
@@ -0,0 +1,117 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples Repositories::GitHttpController do
+ include GitHttpHelpers
+
+ let(:repository_path) { "#{container.full_path}.git" }
+ let(:params) { { repository_path: repository_path } }
+
+ describe 'HEAD #info_refs' do
+ it 'returns 403' do
+ head :info_refs, params: params
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+
+ describe 'GET #info_refs' do
+ let(:params) { super().merge(service: 'git-upload-pack') }
+
+ it 'returns 401 for unauthenticated requests to public repositories when http protocol is disabled' do
+ stub_application_setting(enabled_git_access_protocol: 'ssh')
+ allow(controller).to receive(:basic_auth_provided?).and_call_original
+
+ expect(controller).to receive(:http_download_allowed?).and_call_original
+
+ get :info_refs, params: params
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+
+ it 'calls the right access checker class with the right object' do
+ allow(controller).to receive(:verify_workhorse_api!).and_return(true)
+
+ access_double = double
+ options = {
+ authentication_abilities: [:download_code],
+ repository_path: repository_path,
+ redirected_path: nil,
+ auth_result_type: :none
+ }
+
+ expect(access_checker_class).to receive(:new)
+ .with(nil, container, 'http', hash_including(options))
+ .and_return(access_double)
+
+ allow(access_double).to receive(:check).and_return(false)
+
+ get :info_refs, params: params
+ end
+
+ context 'with authorized user' do
+ before do
+ request.headers.merge! auth_env(user.username, user.password, nil)
+ end
+
+ it 'returns 200' do
+ get :info_refs, params: params
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+
+ it 'updates the user activity' do
+ expect_next_instance_of(Users::ActivityService) do |activity_service|
+ expect(activity_service).to receive(:execute)
+ end
+
+ get :info_refs, params: params
+ end
+
+ include_context 'parsed logs' do
+ it 'adds user info to the logs' do
+ get :info_refs, params: params
+
+ expect(log_data).to include('username' => user.username,
+ 'user_id' => user.id,
+ 'meta.user' => user.username)
+ end
+ end
+ end
+
+ context 'with exceptions' do
+ before do
+ allow(controller).to receive(:authenticate_user).and_return(true)
+ allow(controller).to receive(:verify_workhorse_api!).and_return(true)
+ end
+
+ it 'returns 503 with GRPC Unavailable' do
+ allow(controller).to receive(:access_check).and_raise(GRPC::Unavailable)
+
+ get :info_refs, params: params
+
+ expect(response).to have_gitlab_http_status(:service_unavailable)
+ end
+
+ it 'returns 503 with timeout error' do
+ allow(controller).to receive(:access_check).and_raise(Gitlab::GitAccess::TimeoutError)
+
+ get :info_refs, params: params
+
+ expect(response).to have_gitlab_http_status(:service_unavailable)
+ expect(response.body).to eq 'Gitlab::GitAccess::TimeoutError'
+ end
+ end
+ end
+
+ describe 'POST #git_upload_pack' do
+ before do
+ allow(controller).to receive(:verify_workhorse_api!).and_return(true)
+ end
+
+ it 'returns 200' do
+ post :git_upload_pack, params: params
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+end
diff --git a/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb b/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb
index a6ad8fc594c..dcbf494186a 100644
--- a/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb
+++ b/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb
@@ -14,6 +14,22 @@ RSpec.shared_examples 'wiki controller actions' do
sign_in(user)
end
+ shared_examples 'recovers from git timeout' do
+ let(:method_name) { :page }
+
+ context 'when we encounter git command errors' do
+ it 'renders the appropriate template', :aggregate_failures do
+ expect(controller).to receive(method_name) do
+ raise ::Gitlab::Git::CommandTimedOut, 'Deadline Exceeded'
+ end
+
+ request
+
+ expect(response).to render_template('shared/wikis/git_error')
+ end
+ end
+ end
+
describe 'GET #new' do
subject(:request) { get :new, params: routing_params }
@@ -48,6 +64,12 @@ RSpec.shared_examples 'wiki controller actions' do
get :pages, params: routing_params.merge(id: wiki_title)
end
+ it_behaves_like 'recovers from git timeout' do
+ subject(:request) { get :pages, params: routing_params.merge(id: wiki_title) }
+
+ let(:method_name) { :wiki_pages }
+ end
+
it 'assigns the page collections' do
expect(assigns(:wiki_pages)).to contain_exactly(an_instance_of(WikiPage))
expect(assigns(:wiki_entries)).to contain_exactly(an_instance_of(WikiPage))
@@ -99,6 +121,12 @@ RSpec.shared_examples 'wiki controller actions' do
end
end
+ it_behaves_like 'recovers from git timeout' do
+ subject(:request) { get :history, params: routing_params.merge(id: wiki_title) }
+
+ let(:allow_read_wiki) { true }
+ end
+
it_behaves_like 'fetching history', :ok do
let(:allow_read_wiki) { true }
@@ -139,6 +167,10 @@ RSpec.shared_examples 'wiki controller actions' do
expect(response).to have_gitlab_http_status(:not_found)
end
end
+
+ it_behaves_like 'recovers from git timeout' do
+ subject(:request) { get :diff, params: routing_params.merge(id: wiki_title, version_id: wiki.repository.commit.id) }
+ end
end
describe 'GET #show' do
@@ -151,6 +183,8 @@ RSpec.shared_examples 'wiki controller actions' do
context 'when page exists' do
let(:id) { wiki_title }
+ it_behaves_like 'recovers from git timeout'
+
it 'renders the page' do
request
@@ -161,6 +195,28 @@ RSpec.shared_examples 'wiki controller actions' do
expect(assigns(:sidebar_limited)).to be(false)
end
+ context 'the sidebar fails to load' do
+ before do
+ allow(Wiki).to receive(:for_container).and_return(wiki)
+ wiki.wiki
+ expect(wiki).to receive(:find_sidebar) do
+ raise ::Gitlab::Git::CommandTimedOut, 'Deadline Exceeded'
+ end
+ end
+
+ it 'renders the page, and marks the sidebar as failed' do
+ request
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to render_template('shared/wikis/_sidebar')
+ expect(assigns(:page).title).to eq(wiki_title)
+ expect(assigns(:sidebar_page)).to be_nil
+ expect(assigns(:sidebar_wiki_entries)).to be_nil
+ expect(assigns(:sidebar_limited)).to be_nil
+ expect(assigns(:sidebar_error)).to be_a_kind_of(::Gitlab::Git::CommandError)
+ end
+ end
+
context 'page view tracking' do
it_behaves_like 'tracking unique hll events', :track_unique_wiki_page_views do
let(:target_id) { 'wiki_action' }
@@ -308,6 +364,7 @@ RSpec.shared_examples 'wiki controller actions' do
subject(:request) { get(:edit, params: routing_params.merge(id: id_param)) }
it_behaves_like 'edit action'
+ it_behaves_like 'recovers from git timeout'
context 'when page content encoding is valid' do
render_views
@@ -447,6 +504,17 @@ RSpec.shared_examples 'wiki controller actions' do
end
end
+ describe '#git_access' do
+ render_views
+
+ it 'renders the git access page' do
+ get :git_access, params: routing_params
+
+ expect(response).to render_template('shared/wikis/git_access')
+ expect(response.body).to include(wiki.http_url_to_repo)
+ end
+ end
+
def redirect_to_wiki(wiki, page)
redirect_to(controller.wiki_page_path(wiki, page))
end
diff --git a/spec/support/shared_examples/features/issuable_invite_members_shared_examples.rb b/spec/support/shared_examples/features/issuable_invite_members_shared_examples.rb
index ac1cc2da7e3..3fec1a56c0c 100644
--- a/spec/support/shared_examples/features/issuable_invite_members_shared_examples.rb
+++ b/spec/support/shared_examples/features/issuable_invite_members_shared_examples.rb
@@ -3,7 +3,7 @@
RSpec.shared_examples 'issuable invite members experiments' do
context 'when invite_members_version_a experiment is enabled' do
before do
- stub_experiment_for_user(invite_members_version_a: true)
+ stub_experiment_for_subject(invite_members_version_a: true)
end
it 'shows a link for inviting members and follows through to the members page' do
@@ -28,7 +28,7 @@ RSpec.shared_examples 'issuable invite members experiments' do
context 'when invite_members_version_b experiment is enabled' do
before do
- stub_experiment_for_user(invite_members_version_b: true)
+ stub_experiment_for_subject(invite_members_version_b: true)
end
it 'shows a link for inviting members and follows through to modal' do
diff --git a/spec/support/shared_examples/features/master_manages_access_requests_shared_example.rb b/spec/support/shared_examples/features/master_manages_access_requests_shared_example.rb
index 724d6db2705..1dbaace1c89 100644
--- a/spec/support/shared_examples/features/master_manages_access_requests_shared_example.rb
+++ b/spec/support/shared_examples/features/master_manages_access_requests_shared_example.rb
@@ -50,7 +50,6 @@ RSpec.shared_examples 'Maintainer manages access requests' do
def expect_visible_access_request(entity, user)
if has_tabs
expect(page).to have_content "Access requests 1"
- expect(page).to have_content "Users requesting access to #{entity.name}"
else
expect(page).to have_content "Users requesting access to #{entity.name} 1"
end
diff --git a/spec/support/shared_examples/features/reportable_note_shared_examples.rb b/spec/support/shared_examples/features/reportable_note_shared_examples.rb
index bdaa375721f..288e1df9b2a 100644
--- a/spec/support/shared_examples/features/reportable_note_shared_examples.rb
+++ b/spec/support/shared_examples/features/reportable_note_shared_examples.rb
@@ -29,7 +29,7 @@ RSpec.shared_examples 'reportable note' do |type|
end
end
- it 'Report button links to a report page' do
+ it 'report button links to a report page' do
dropdown = comment.find(more_actions_selector)
open_dropdown(dropdown)
diff --git a/spec/support/shared_examples/features/wiki/user_git_access_wiki_page_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_git_access_wiki_page_shared_examples.rb
new file mode 100644
index 00000000000..d3d2a36147d
--- /dev/null
+++ b/spec/support/shared_examples/features/wiki/user_git_access_wiki_page_shared_examples.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'User views Git access wiki page' do
+ let(:wiki_page) { create(:wiki_page, wiki: wiki) }
+
+ before do
+ sign_in(user)
+ end
+
+ it 'shows the correct clone URLs', :js do
+ visit wiki_page_path(wiki, wiki_page)
+ click_link 'Clone repository'
+
+ expect(page).to have_text("Clone repository #{wiki.full_path}")
+
+ within('.git-clone-holder') do
+ expect(page).to have_css('#clone-dropdown', text: 'HTTP')
+ expect(page).to have_field('clone_url', with: wiki.http_url_to_repo)
+
+ click_link 'HTTP' # open the dropdown
+ click_link 'SSH' # select the dropdown item
+
+ expect(page).to have_css('#clone-dropdown', text: 'SSH')
+ expect(page).to have_field('clone_url', with: wiki.ssh_url_to_repo)
+ end
+ end
+end
diff --git a/spec/support/shared_examples/features/wiki/user_uses_wiki_shortcuts_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_uses_wiki_shortcuts_shared_examples.rb
index 0330b345a18..759cfaf6b1f 100644
--- a/spec/support/shared_examples/features/wiki/user_uses_wiki_shortcuts_shared_examples.rb
+++ b/spec/support/shared_examples/features/wiki/user_uses_wiki_shortcuts_shared_examples.rb
@@ -12,7 +12,7 @@ RSpec.shared_examples 'User uses wiki shortcuts' do
visit wiki_page_path(wiki, wiki_page)
end
- it 'Visit edit wiki page using "e" keyboard shortcut', :js do
+ it 'visit edit wiki page using "e" keyboard shortcut', :js do
find('body').native.send_key('e')
expect(find('.wiki-page-title')).to have_content('Edit Page')
diff --git a/spec/support/shared_examples/graphql/connection_redaction_shared_examples.rb b/spec/support/shared_examples/graphql/connection_redaction_shared_examples.rb
new file mode 100644
index 00000000000..12a7b3fe414
--- /dev/null
+++ b/spec/support/shared_examples/graphql/connection_redaction_shared_examples.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+# requires:
+# - `connection` (no-empty, containing `unwanted` and at least one more item)
+# - `unwanted` (single item in collection)
+RSpec.shared_examples 'a redactable connection' do
+ context 'no redactor set' do
+ it 'contains the unwanted item' do
+ expect(connection.nodes).to include(unwanted)
+ end
+
+ it 'does not redact more than once' do
+ connection.nodes
+ r_state = connection.send(:redaction_state)
+
+ expect(r_state.redacted { raise 'Should not be called!' }).to be_present
+ end
+ end
+
+ let_it_be(:constant_redactor) do
+ Class.new do
+ def initialize(remove)
+ @remove = remove
+ end
+
+ def redact(items)
+ items - @remove
+ end
+ end
+ end
+
+ context 'redactor is set' do
+ let(:redactor) do
+ constant_redactor.new([unwanted])
+ end
+
+ before do
+ connection.redactor = redactor
+ end
+
+ it 'does not contain the unwanted item' do
+ expect(connection.nodes).not_to include(unwanted)
+ expect(connection.nodes).not_to be_empty
+ end
+
+ it 'does not redact more than once' do
+ expect(redactor).to receive(:redact).once.and_call_original
+
+ connection.nodes
+ connection.nodes
+ connection.nodes
+ end
+ end
+end
diff --git a/spec/support/shared_examples/graphql/connection_shared_examples.rb b/spec/support/shared_examples/graphql/connection_shared_examples.rb
new file mode 100644
index 00000000000..4cba5b5a69d
--- /dev/null
+++ b/spec/support/shared_examples/graphql/connection_shared_examples.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'a connection with collection methods' do
+ %i[to_a size include? empty?].each do |method_name|
+ it "responds to #{method_name}" do
+ expect(connection).to respond_to(method_name)
+ end
+ end
+end
diff --git a/spec/support/shared_examples/graphql/design_fields_shared_examples.rb b/spec/support/shared_examples/graphql/design_fields_shared_examples.rb
index ef7086234c4..9c2eb3e5a5c 100644
--- a/spec/support/shared_examples/graphql/design_fields_shared_examples.rb
+++ b/spec/support/shared_examples/graphql/design_fields_shared_examples.rb
@@ -26,27 +26,35 @@ RSpec.shared_examples 'a GraphQL type with design fields' do
end
describe '#image' do
+ let_it_be(:current_user) { create(:user) }
let(:schema) { GitlabSchema }
let(:query) { GraphQL::Query.new(schema) }
- let(:context) { double('Context', schema: schema, query: query, parent: nil) }
+ let(:context) { query.context }
let(:field) { described_class.fields['image'] }
let(:args) { GraphQL::Query::Arguments::NO_ARGS }
- let(:instance) do
+ let(:instance) { instantiate(object_id) }
+ let(:instance_b) { instantiate(object_id_b) }
+
+ def instantiate(object_id)
object = GitlabSchema.sync_lazy(GitlabSchema.object_from_id(object_id))
object_type.authorized_new(object, query.context)
end
- let(:instance_b) do
- object_b = GitlabSchema.sync_lazy(GitlabSchema.object_from_id(object_id_b))
- object_type.authorized_new(object_b, query.context)
+ def resolve_image(instance)
+ field.resolve_field(instance, args, context)
+ end
+
+ before do
+ context[:current_user] = current_user
+ allow(Ability).to receive(:allowed?).with(current_user, :read_design, anything).and_return(true)
+ allow(context).to receive(:parent).and_return(nil)
end
it 'resolves to the design image URL' do
- image = field.resolve_field(instance, args, context)
sha = design.versions.first.sha
url = ::Gitlab::Routing.url_helpers.project_design_management_designs_raw_image_url(design.project, design, sha)
- expect(image).to eq(url)
+ expect(resolve_image(instance)).to eq(url)
end
it 'has better than O(N) peformance', :request_store do
@@ -68,10 +76,10 @@ RSpec.shared_examples 'a GraphQL type with design fields' do
# = 10
expect(instance).not_to eq(instance_b) # preload designs themselves.
expect do
- image_a = field.resolve_field(instance, args, context)
- image_b = field.resolve_field(instance, args, context)
- image_c = field.resolve_field(instance_b, args, context)
- image_d = field.resolve_field(instance_b, args, context)
+ image_a = resolve_image(instance)
+ image_b = resolve_image(instance)
+ image_c = resolve_image(instance_b)
+ image_d = resolve_image(instance_b)
expect(image_a).to eq(image_b)
expect(image_c).not_to eq(image_b)
expect(image_c).to eq(image_d)
diff --git a/spec/support/shared_examples/graphql/jira_import/jira_import_resolver_shared_examples.rb b/spec/support/shared_examples/graphql/jira_import/jira_import_resolver_shared_examples.rb
deleted file mode 100644
index b2047f1d32c..00000000000
--- a/spec/support/shared_examples/graphql/jira_import/jira_import_resolver_shared_examples.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-# frozen_string_literal: true
-
-RSpec.shared_examples 'no Jira import data present' do
- it 'returns none' do
- expect(resolve_imports).to eq JiraImportState.none
- end
-end
-
-RSpec.shared_examples 'no Jira import access' do
- it 'raises error' do
- expect do
- resolve_imports
- end.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
- 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 3a046c3feec..b0bdd27a95f 100644
--- a/spec/support/shared_examples/graphql/members_shared_examples.rb
+++ b/spec/support/shared_examples/graphql/members_shared_examples.rb
@@ -36,9 +36,10 @@ RSpec.shared_examples 'querying members with a group' do
let_it_be(:group_2_member) { create(:group_member, user: user_3, group: group_2) }
let(:args) { {} }
+ let(:base_args) { { relations: described_class.arguments['relations'].default_value } }
subject do
- resolve(described_class, obj: resource, args: args, ctx: { current_user: user_4 })
+ resolve(described_class, obj: resource, args: base_args.merge(args), ctx: { current_user: user_4 })
end
describe '#resolve' do
@@ -72,7 +73,7 @@ RSpec.shared_examples 'querying members with a group' do
let_it_be(:other_user) { create(:user) }
subject do
- resolve(described_class, obj: resource, args: args, ctx: { current_user: other_user })
+ resolve(described_class, obj: resource, args: base_args.merge(args), ctx: { current_user: other_user })
end
it 'raises an error' do
diff --git a/spec/support/shared_examples/graphql/mutation_shared_examples.rb b/spec/support/shared_examples/graphql/mutation_shared_examples.rb
index b67cac94547..84ebd4852b9 100644
--- a/spec/support/shared_examples/graphql/mutation_shared_examples.rb
+++ b/spec/support/shared_examples/graphql/mutation_shared_examples.rb
@@ -13,6 +13,8 @@ RSpec.shared_examples 'a mutation that returns top-level errors' do |errors: []|
it do
post_graphql_mutation(mutation, current_user: current_user)
+ expect(graphql_errors).to be_present
+
error_messages = graphql_errors.map { |e| e['message'] }
expect(error_messages).to match_errors
diff --git a/spec/support/shared_examples/graphql/mutations/boards_create_shared_examples.rb b/spec/support/shared_examples/graphql/mutations/boards_create_shared_examples.rb
index 9c0b398a5c1..2b93d174653 100644
--- a/spec/support/shared_examples/graphql/mutations/boards_create_shared_examples.rb
+++ b/spec/support/shared_examples/graphql/mutations/boards_create_shared_examples.rb
@@ -40,6 +40,30 @@ RSpec.shared_examples 'boards create mutation' do
end
end
+ context 'when hide_backlog_list parameter is true' do
+ before do
+ params[:hide_backlog_list] = true
+ end
+
+ it 'returns the board with correct hide_backlog_list field' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(mutation_response['board']['hideBacklogList']).to eq(true)
+ end
+ end
+
+ context 'when hide_closed_list parameter is true' do
+ before do
+ params[:hide_closed_list] = true
+ end
+
+ it 'returns the board with correct hide_closed_list field' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(mutation_response['board']['hideClosedList']).to eq(true)
+ end
+ end
+
context 'when the Boards::CreateService returns an error response' do
before do
allow_next_instance_of(Boards::CreateService) do |service|
diff --git a/spec/support/shared_examples/graphql/mutations/spammable_mutation_fields_examples.rb b/spec/support/shared_examples/graphql/mutations/spammable_mutation_fields_examples.rb
index 54b3f84a6e6..8678b23ad31 100644
--- a/spec/support/shared_examples/graphql/mutations/spammable_mutation_fields_examples.rb
+++ b/spec/support/shared_examples/graphql/mutations/spammable_mutation_fields_examples.rb
@@ -13,7 +13,8 @@ end
RSpec.shared_examples 'can raise spam flag' do
it 'spam parameters are passed to the service' do
- expect(service).to receive(:new).with(anything, anything, hash_including(api: true, request: instance_of(ActionDispatch::Request)))
+ args = [anything, anything, hash_including(api: true, request: instance_of(ActionDispatch::Request))]
+ expect(service).to receive(:new).with(*args).and_call_original
subject
end
@@ -39,7 +40,9 @@ RSpec.shared_examples 'can raise spam flag' do
end
it 'request parameter is not passed to the service' do
- expect(service).to receive(:new).with(anything, anything, hash_not_including(request: instance_of(ActionDispatch::Request)))
+ expect(service).to receive(:new)
+ .with(anything, anything, hash_not_including(request: instance_of(ActionDispatch::Request)))
+ .and_call_original
subject
end
diff --git a/spec/support/shared_examples/graphql/projects/services_resolver_shared_examples.rb b/spec/support/shared_examples/graphql/projects/services_resolver_shared_examples.rb
index 94b7ed1618d..16c2ab07f3a 100644
--- a/spec/support/shared_examples/graphql/projects/services_resolver_shared_examples.rb
+++ b/spec/support/shared_examples/graphql/projects/services_resolver_shared_examples.rb
@@ -2,14 +2,12 @@
RSpec.shared_examples 'no project services' do
it 'returns empty collection' do
- expect(resolve_services).to eq []
+ expect(resolve_services).to be_empty
end
end
RSpec.shared_examples 'cannot access project services' do
it 'raises error' do
- expect do
- resolve_services
- end.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
+ expect(resolve_services).to be_nil
end
end
diff --git a/spec/support/shared_examples/graphql/sorted_paginated_query_shared_examples.rb b/spec/support/shared_examples/graphql/sorted_paginated_query_shared_examples.rb
index 7627a7b4d59..f78ea364147 100644
--- a/spec/support/shared_examples/graphql/sorted_paginated_query_shared_examples.rb
+++ b/spec/support/shared_examples/graphql/sorted_paginated_query_shared_examples.rb
@@ -16,80 +16,111 @@
#
# Example:
# describe 'sorting and pagination' do
-# let(:sort_project) { create(:project, :public) }
+# let_it_be(:sort_project) { create(:project, :public) }
# let(:data_path) { [:project, :issues] }
#
-# def pagination_query(params, page_info)
-# graphql_query_for(
-# 'project',
-# { 'fullPath' => sort_project.full_path },
-# query_graphql_field('issues', params, "#{page_info} edges { node { id } }")
+# def pagination_query(arguments)
+# graphql_query_for(:project, { full_path: sort_project.full_path },
+# query_nodes(:issues, :iid, include_pagination_info: true, args: arguments)
# )
# end
#
-# def pagination_results_data(data)
-# data.map { |issue| issue.dig('node', 'iid').to_i }
+# # A method transforming nodes to data to match against
+# # default: the identity function
+# def pagination_results_data(issues)
+# issues.map { |issue| issue['iid].to_i }
# end
#
# context 'when sorting by weight' do
-# ...
+# let_it_be(:issues) { make_some_issues_with_weights }
+#
# context 'when ascending' do
+# let(:ordered_issues) { issues.sort_by(&:weight) }
+#
# it_behaves_like 'sorted paginated query' do
-# let(:sort_param) { 'WEIGHT_ASC' }
+# let(:sort_param) { :WEIGHT_ASC }
# let(:first_param) { 2 }
-# let(:expected_results) { [weight_issue3.iid, weight_issue5.iid, weight_issue1.iid, weight_issue4.iid, weight_issue2.iid] }
+# let(:expected_results) { ordered_issues.map(&:iid) }
# end
# end
#
RSpec.shared_examples 'sorted paginated query' do
+ # Provided as a convenience when constructing queries using string concatenation
+ let(:page_info) { 'pageInfo { startCursor endCursor }' }
+ # Convenience for using default implementation of pagination_results_data
+ let(:node_path) { ['id'] }
+
it_behaves_like 'requires variables' do
let(:required_variables) { [:sort_param, :first_param, :expected_results, :data_path, :current_user] }
end
describe do
- let(:sort_argument) { "sort: #{sort_param}" if sort_param.present? }
- let(:first_argument) { "first: #{first_param}" if first_param.present? }
+ let(:sort_argument) { graphql_args(sort: sort_param) }
let(:params) { sort_argument }
- let(:start_cursor) { graphql_data_at(*data_path, :pageInfo, :startCursor) }
- let(:end_cursor) { graphql_data_at(*data_path, :pageInfo, :endCursor) }
- let(:sorted_edges) { graphql_data_at(*data_path, :edges) }
- let(:page_info) { "pageInfo { startCursor endCursor }" }
- def pagination_query(params, page_info)
- raise('pagination_query(params, page_info) must be defined in the test, see example in comment') unless defined?(super)
+ # Convenience helper for the large number of queries defined as a projection
+ # from some root value indexed by full_path to a collection of objects with IID
+ def nested_internal_id_query(root_field, parent, field, args, selection: :iid)
+ graphql_query_for(root_field, { full_path: parent.full_path },
+ query_nodes(field, selection, args: args, include_pagination_info: true)
+ )
+ end
+
+ def pagination_query(params)
+ raise('pagination_query(params) must be defined in the test, see example in comment') unless defined?(super)
super
end
- def pagination_results_data(data)
- raise('pagination_results_data(data) must be defined in the test, see example in comment') unless defined?(super)
+ def pagination_results_data(nodes)
+ if defined?(super)
+ super(nodes)
+ else
+ nodes.map { |n| n.dig(*node_path) }
+ end
+ end
+
+ def results
+ nodes = graphql_dig_at(graphql_data(fresh_response_data), *data_path, :nodes)
+ pagination_results_data(nodes)
+ end
+
+ def end_cursor
+ graphql_dig_at(graphql_data(fresh_response_data), *data_path, :page_info, :end_cursor)
+ end
- super(data)
+ def start_cursor
+ graphql_dig_at(graphql_data(fresh_response_data), *data_path, :page_info, :start_cursor)
end
+ let(:query) { pagination_query(params) }
+
before do
- post_graphql(pagination_query(params, page_info), current_user: current_user)
+ post_graphql(query, current_user: current_user)
end
context 'when sorting' do
it 'sorts correctly' do
- expect(pagination_results_data(sorted_edges)).to eq expected_results
+ expect(results).to eq expected_results
end
context 'when paginating' do
- let(:params) { [sort_argument, first_argument].compact.join(',') }
+ let(:params) { sort_argument.merge(first: first_param) }
+ let(:first_page) { expected_results.first(first_param) }
+ let(:rest) { expected_results.drop(first_param) }
it 'paginates correctly' do
- expect(pagination_results_data(sorted_edges)).to eq expected_results.first(first_param)
+ expect(results).to eq first_page
- cursored_query = pagination_query([sort_argument, "after: \"#{end_cursor}\""].compact.join(','), page_info)
- post_graphql(cursored_query, current_user: current_user)
+ fwds = pagination_query(sort_argument.merge(after: end_cursor))
+ post_graphql(fwds, current_user: current_user)
- expect(response).to have_gitlab_http_status(:ok)
+ expect(results).to eq rest
- response_data = graphql_dig_at(Gitlab::Json.parse(response.body), :data, *data_path, :edges)
+ bwds = pagination_query(sort_argument.merge(before: start_cursor))
+ post_graphql(bwds, current_user: current_user)
- expect(pagination_results_data(response_data)).to eq expected_results.drop(first_param)
+ expect(results).to eq first_page
end
end
end
diff --git a/spec/support/shared_examples/graphql/types/gitlab_style_deprecations_shared_examples.rb b/spec/support/shared_examples/graphql/types/gitlab_style_deprecations_shared_examples.rb
index ed139e638bf..269e9170906 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
@@ -32,16 +32,16 @@ RSpec.shared_examples 'Gitlab-style deprecations' do
it 'adds a formatted `deprecated_reason` to the subject' do
deprecable = subject(deprecated: { milestone: '1.10', reason: 'Deprecation reason' })
- expect(deprecable.deprecation_reason).to eq('Deprecation reason. Deprecated in 1.10')
+ expect(deprecable.deprecation_reason).to eq('Deprecation reason. Deprecated in 1.10.')
end
it 'appends to the description if given' do
deprecable = subject(
deprecated: { milestone: '1.10', reason: 'Deprecation reason' },
- description: 'Deprecable description'
+ description: 'Deprecable description.'
)
- expect(deprecable.description).to eq('Deprecable description. Deprecated in 1.10: Deprecation reason')
+ expect(deprecable.description).to eq('Deprecable description. Deprecated in 1.10: Deprecation reason.')
end
it 'does not append to the description if it is absent' do
diff --git a/spec/support/shared_examples/lib/gitlab/diff_file_collections_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/diff_file_collections_shared_examples.rb
index 469c0c287b1..c9e03ced0dd 100644
--- a/spec/support/shared_examples/lib/gitlab/diff_file_collections_shared_examples.rb
+++ b/spec/support/shared_examples/lib/gitlab/diff_file_collections_shared_examples.rb
@@ -143,3 +143,55 @@ RSpec.shared_examples 'cacheable diff collection' do
end
end
end
+
+shared_examples_for 'sortable diff files' do
+ subject { described_class.new(diffable, **collection_default_args) }
+
+ describe '#raw_diff_files' do
+ let(:raw_diff_files_paths) do
+ subject.raw_diff_files(sorted: sorted).map { |file| file.new_path.presence || file.old_path }
+ end
+
+ context 'when sorted is false (default)' do
+ let(:sorted) { false }
+
+ it 'returns unsorted diff files' do
+ expect(raw_diff_files_paths).to eq(unsorted_diff_files_paths)
+ end
+ end
+
+ context 'when sorted is true' do
+ let(:sorted) { true }
+
+ it 'returns sorted diff files' do
+ expect(raw_diff_files_paths).to eq(sorted_diff_files_paths)
+ end
+
+ context 'when sort_diffs feature flag is disabled' do
+ before do
+ stub_feature_flags(sort_diffs: false)
+ end
+
+ it 'returns unsorted diff files' do
+ expect(raw_diff_files_paths).to eq(unsorted_diff_files_paths)
+ end
+ end
+ end
+ end
+end
+
+shared_examples_for 'unsortable diff files' do
+ subject { described_class.new(diffable, **collection_default_args) }
+
+ describe '#raw_diff_files' do
+ it 'does not call Gitlab::Diff::FileCollectionSorter even when sorted is true' do
+ # Ensure that diffable is created before expectation to ensure that we are
+ # not calling it from `FileCollectionSorter` from `#raw_diff_files`.
+ diffable
+
+ expect(Gitlab::Diff::FileCollectionSorter).not_to receive(:new)
+
+ subject.raw_diff_files(sorted: true)
+ end
+ end
+end
diff --git a/spec/support/shared_examples/lib/gitlab/import_export/import_failure_service_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/import_export/import_failure_service_shared_examples.rb
index 801be5ae946..67afd2035c4 100644
--- a/spec/support/shared_examples/lib/gitlab/import_export/import_failure_service_shared_examples.rb
+++ b/spec/support/shared_examples/lib/gitlab/import_export/import_failure_service_shared_examples.rb
@@ -3,10 +3,10 @@
RSpec.shared_examples 'log import failure' do |importable_column|
it 'tracks error' do
extra = {
- source: action,
- relation_key: relation_key,
- relation_index: relation_index,
- retry_count: retry_count
+ source: action,
+ relation_name: relation_key,
+ relation_index: relation_index,
+ retry_count: retry_count
}
extra[importable_column] = importable.id
diff --git a/spec/support/shared_examples/lib/gitlab/middleware/read_only_gitlab_instance_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/middleware/read_only_gitlab_instance_shared_examples.rb
index e07d3e2dec9..5b3d30df739 100644
--- a/spec/support/shared_examples/lib/gitlab/middleware/read_only_gitlab_instance_shared_examples.rb
+++ b/spec/support/shared_examples/lib/gitlab/middleware/read_only_gitlab_instance_shared_examples.rb
@@ -125,6 +125,9 @@ RSpec.shared_examples 'write access for a read-only GitLab instance' do
where(:description, :path) do
'LFS request to batch' | '/root/rouge.git/info/lfs/objects/batch'
'request to git-upload-pack' | '/root/rouge.git/git-upload-pack'
+ 'user sign out' | '/users/sign_out'
+ 'admin session' | '/admin/session'
+ 'admin session destroy' | '/admin/session/destroy'
end
with_them do
diff --git a/spec/support/shared_examples/lib/gitlab/sidekiq_middleware/strategy_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/sidekiq_middleware/strategy_shared_examples.rb
index 2936bb354cf..89b793d5e16 100644
--- a/spec/support/shared_examples/lib/gitlab/sidekiq_middleware/strategy_shared_examples.rb
+++ b/spec/support/shared_examples/lib/gitlab/sidekiq_middleware/strategy_shared_examples.rb
@@ -38,7 +38,7 @@ RSpec.shared_examples 'deduplicating jobs when scheduling' do |strategy_name|
it 'adds the jid of the existing job to the job hash' do
allow(fake_duplicate_job).to receive(:scheduled?).and_return(false)
allow(fake_duplicate_job).to receive(:check!).and_return('the jid')
- allow(fake_duplicate_job).to receive(:droppable?).and_return(true)
+ allow(fake_duplicate_job).to receive(:idempotent?).and_return(true)
allow(fake_duplicate_job).to receive(:options).and_return({})
job_hash = {}
@@ -62,7 +62,7 @@ RSpec.shared_examples 'deduplicating jobs when scheduling' do |strategy_name|
receive(:check!)
.with(Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob::DUPLICATE_KEY_TTL)
.and_return('the jid'))
- allow(fake_duplicate_job).to receive(:droppable?).and_return(true)
+ allow(fake_duplicate_job).to receive(:idempotent?).and_return(true)
job_hash = {}
expect(fake_duplicate_job).to receive(:duplicate?).and_return(true)
@@ -82,7 +82,7 @@ RSpec.shared_examples 'deduplicating jobs when scheduling' do |strategy_name|
allow(fake_duplicate_job).to receive(:options).and_return({ including_scheduled: true })
allow(fake_duplicate_job).to(
receive(:check!).with(time_diff.to_i).and_return('the jid'))
- allow(fake_duplicate_job).to receive(:droppable?).and_return(true)
+ allow(fake_duplicate_job).to receive(:idempotent?).and_return(true)
job_hash = {}
expect(fake_duplicate_job).to receive(:duplicate?).and_return(true)
@@ -104,13 +104,13 @@ RSpec.shared_examples 'deduplicating jobs when scheduling' do |strategy_name|
allow(fake_duplicate_job).to receive(:duplicate?).and_return(true)
allow(fake_duplicate_job).to receive(:options).and_return({})
allow(fake_duplicate_job).to receive(:existing_jid).and_return('the jid')
- allow(fake_duplicate_job).to receive(:droppable?).and_return(true)
+ allow(fake_duplicate_job).to receive(:idempotent?).and_return(true)
end
it 'drops the job' do
schedule_result = nil
- expect(fake_duplicate_job).to receive(:droppable?).and_return(true)
+ expect(fake_duplicate_job).to receive(:idempotent?).and_return(true)
expect { |b| schedule_result = strategy.schedule({}, &b) }.not_to yield_control
expect(schedule_result).to be(false)
diff --git a/spec/support/shared_examples/models/concerns/can_move_repository_storage_shared_examples.rb b/spec/support/shared_examples/models/concerns/can_move_repository_storage_shared_examples.rb
new file mode 100644
index 00000000000..85a2c6f1449
--- /dev/null
+++ b/spec/support/shared_examples/models/concerns/can_move_repository_storage_shared_examples.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'can move repository storage' do
+ let(:container) { raise NotImplementedError }
+
+ describe '#set_repository_read_only!' do
+ it 'makes the repository read-only' do
+ expect { container.set_repository_read_only! }
+ .to change(container, :repository_read_only?)
+ .from(false)
+ .to(true)
+ end
+
+ it 'raises an error if the project is already read-only' do
+ container.set_repository_read_only!
+
+ expect { container.set_repository_read_only! }.to raise_error(described_class::RepositoryReadOnlyError, /already read-only/)
+ end
+
+ it 'raises an error when there is an existing git transfer in progress' do
+ allow(container).to receive(:git_transfer_in_progress?) { true }
+
+ expect { container.set_repository_read_only! }.to raise_error(described_class::RepositoryReadOnlyError, /in progress/)
+ end
+
+ context 'skip_git_transfer_check is true' do
+ it 'makes the project read-only when git transfers are in progress' do
+ allow(container).to receive(:git_transfer_in_progress?) { true }
+
+ expect { container.set_repository_read_only!(skip_git_transfer_check: true) }
+ .to change(container, :repository_read_only?)
+ .from(false)
+ .to(true)
+ end
+ end
+ end
+
+ describe '#set_repository_writable!' do
+ it 'sets repository_read_only to false' do
+ expect { container.set_repository_writable! }
+ .to change(container, :repository_read_only)
+ .from(true).to(false)
+ end
+ end
+
+ describe '#reference_counter' do
+ it 'returns a Gitlab::ReferenceCounter object' do
+ expect(Gitlab::ReferenceCounter).to receive(:new).with(container.repository.gl_repository).and_call_original
+
+ result = container.reference_counter(type: container.repository.repo_type)
+
+ expect(result).to be_a Gitlab::ReferenceCounter
+ end
+ end
+end
diff --git a/spec/support/shared_examples/models/concerns/repository_storage_movable_shared_examples.rb b/spec/support/shared_examples/models/concerns/repository_storage_movable_shared_examples.rb
new file mode 100644
index 00000000000..5a8388d01df
--- /dev/null
+++ b/spec/support/shared_examples/models/concerns/repository_storage_movable_shared_examples.rb
@@ -0,0 +1,115 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'handles repository moves' do
+ describe 'associations' do
+ it { is_expected.to belong_to(:container) }
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:container) }
+ it { is_expected.to validate_presence_of(:state) }
+ it { is_expected.to validate_presence_of(:source_storage_name) }
+ it { is_expected.to validate_presence_of(:destination_storage_name) }
+
+ context 'source_storage_name inclusion' do
+ subject { build(repository_storage_factory_key, source_storage_name: 'missing') }
+
+ it "does not allow repository storages that don't match a label in the configuration" do
+ expect(subject).not_to be_valid
+ expect(subject.errors[:source_storage_name].first).to match(/is not included in the list/)
+ end
+ end
+
+ context 'destination_storage_name inclusion' do
+ subject { build(repository_storage_factory_key, destination_storage_name: 'missing') }
+
+ it "does not allow repository storages that don't match a label in the configuration" do
+ expect(subject).not_to be_valid
+ expect(subject.errors[:destination_storage_name].first).to match(/is not included in the list/)
+ end
+ end
+
+ context 'container repository read-only' do
+ subject { build(repository_storage_factory_key, container: container) }
+
+ it "does not allow the container to be read-only on create" do
+ container.update!(repository_read_only: true)
+
+ expect(subject).not_to be_valid
+ expect(subject.errors[error_key].first).to match(/is read only/)
+ end
+ end
+ end
+
+ describe 'defaults' do
+ context 'destination_storage_name' do
+ subject { build(repository_storage_factory_key) }
+
+ it 'picks storage from ApplicationSetting' do
+ expect(Gitlab::CurrentSettings).to receive(:pick_repository_storage).and_return('picked').at_least(:once)
+
+ expect(subject.destination_storage_name).to eq('picked')
+ end
+ end
+ end
+
+ describe 'state transitions' do
+ before do
+ stub_storage_settings('test_second_storage' => { 'path' => 'tmp/tests/extra_storage' })
+ end
+
+ context 'when in the default state' do
+ subject(:storage_move) { create(repository_storage_factory_key, container: container, destination_storage_name: 'test_second_storage') }
+
+ context 'and transits to scheduled' do
+ it 'triggers the corresponding repository storage worker' do
+ skip unless repository_storage_worker # TODO remove after https://gitlab.com/gitlab-org/gitlab/-/issues/218991 is implemented
+ expect(repository_storage_worker).to receive(:perform_async).with(container.id, 'test_second_storage', storage_move.id)
+
+ storage_move.schedule!
+
+ expect(container).to be_repository_read_only
+ end
+
+ context 'when the transition fails' do
+ it 'does not trigger ProjectUpdateRepositoryStorageWorker and adds an error' do
+ skip unless repository_storage_worker # TODO remove after https://gitlab.com/gitlab-org/gitlab/-/issues/218991 is implemented
+ allow(storage_move.container).to receive(:set_repository_read_only!).and_raise(StandardError, 'foobar')
+ expect(repository_storage_worker).not_to receive(:perform_async)
+
+ storage_move.schedule!
+
+ expect(storage_move.errors[error_key]).to include('foobar')
+ end
+ end
+ end
+
+ context 'and transits to started' do
+ it 'does not allow the transition' do
+ expect { storage_move.start! }
+ .to raise_error(StateMachines::InvalidTransition)
+ end
+ end
+ end
+
+ context 'when started' do
+ subject(:storage_move) { create(repository_storage_factory_key, :started, container: container, destination_storage_name: 'test_second_storage') }
+
+ context 'and transits to replicated' do
+ it 'marks the container as writable' do
+ storage_move.finish_replication!
+
+ expect(container).not_to be_repository_read_only
+ end
+ end
+
+ context 'and transits to failed' do
+ it 'marks the container as writable' do
+ storage_move.do_fail!
+
+ expect(container).not_to be_repository_read_only
+ end
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/models/concerns/shardable_shared_examples.rb b/spec/support/shared_examples/models/concerns/shardable_shared_examples.rb
index fa929d5b791..fd0639b628e 100644
--- a/spec/support/shared_examples/models/concerns/shardable_shared_examples.rb
+++ b/spec/support/shared_examples/models/concerns/shardable_shared_examples.rb
@@ -18,4 +18,10 @@ RSpec.shared_examples 'shardable scopes' do
expect(described_class.excluding_repository_storage('default')).to eq([record_2])
end
end
+
+ describe '.for_shard' do
+ it 'returns the objects for a given shard' do
+ expect(described_class.for_shard(record_1.shard)).to eq([record_1])
+ end
+ end
end
diff --git a/spec/support/shared_examples/models/mentionable_shared_examples.rb b/spec/support/shared_examples/models/mentionable_shared_examples.rb
index 0ee0b7e6d88..2392658e584 100644
--- a/spec/support/shared_examples/models/mentionable_shared_examples.rb
+++ b/spec/support/shared_examples/models/mentionable_shared_examples.rb
@@ -92,7 +92,7 @@ RSpec.shared_examples 'a mentionable' do
end
end
- expect(subject).to receive(:cached_markdown_fields).at_least(:once).and_call_original
+ expect(subject).to receive(:cached_markdown_fields).at_least(1).and_call_original
subject.all_references(author)
end
@@ -151,7 +151,7 @@ RSpec.shared_examples 'an editable mentionable' do
end
it 'persists the refreshed cache so that it does not have to be refreshed every time' do
- expect(subject).to receive(:refresh_markdown_cache).once.and_call_original
+ expect(subject).to receive(:refresh_markdown_cache).at_least(1).and_call_original
subject.all_references(author)
diff --git a/spec/support/shared_examples/models/resource_timebox_event_shared_examples.rb b/spec/support/shared_examples/models/resource_timebox_event_shared_examples.rb
index 5198508d48b..f56e8d4e085 100644
--- a/spec/support/shared_examples/models/resource_timebox_event_shared_examples.rb
+++ b/spec/support/shared_examples/models/resource_timebox_event_shared_examples.rb
@@ -75,11 +75,17 @@ RSpec.shared_examples 'timebox resource event actions' do
end
RSpec.shared_examples 'timebox resource tracks issue metrics' do |type|
- describe '#usage_metrics' do
- it 'tracks usage' do
+ describe '#issue_usage_metrics' do
+ it 'tracks usage for issues' do
expect(Gitlab::UsageDataCounters::IssueActivityUniqueCounter).to receive(:"track_issue_#{type}_changed_action")
create(described_class.name.underscore.to_sym, issue: create(:issue))
end
+
+ it 'does not track usage for merge requests' do
+ expect(Gitlab::UsageDataCounters::IssueActivityUniqueCounter).not_to receive(:"track_issue_#{type}_changed_action")
+
+ create(described_class.name.underscore.to_sym, merge_request: create(:merge_request))
+ end
end
end
diff --git a/spec/support/shared_examples/quick_actions/issue/clone_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issue/clone_quick_action_shared_examples.rb
new file mode 100644
index 00000000000..a99304f7214
--- /dev/null
+++ b/spec/support/shared_examples/quick_actions/issue/clone_quick_action_shared_examples.rb
@@ -0,0 +1,187 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'clone quick action' do
+ context 'clone the issue to another project' do
+ let(:target_project) { create(:project, :public) }
+
+ context 'when no target is given' do
+ it 'clones the issue in the current project' do
+ add_note("/clone")
+
+ expect(page).to have_content "Cloned this issue to #{project.full_path}."
+ expect(issue.reload).to be_open
+
+ visit project_issue_path(project, issue)
+
+ expect(page).to have_content 'Issues 2'
+ end
+ end
+
+ context 'when the project is valid' do
+ before do
+ target_project.add_maintainer(user)
+ end
+
+ it 'clones the issue' do
+ add_note("/clone #{target_project.full_path}")
+
+ expect(page).to have_content "Cloned this issue to #{target_project.full_path}."
+ expect(issue.reload).to be_open
+
+ visit project_issue_path(target_project, issue)
+
+ expect(page).to have_content 'Issues 1'
+ end
+
+ context 'when cloning with notes', :aggregate_failures do
+ it 'clones the issue with all notes' do
+ add_note("Some random note")
+ add_note("Another note")
+
+ add_note("/clone --with_notes #{target_project.full_path}")
+
+ expect(page).to have_content "Cloned this issue to #{target_project.full_path}."
+ expect(issue.reload).to be_open
+
+ visit project_issue_path(target_project, issue)
+
+ expect(page).to have_content 'Issues 1'
+ expect(page).to have_content 'Some random note'
+ expect(page).to have_content 'Another note'
+ end
+
+ it 'returns an error if the params are malformed' do
+ # Note that this is missing one `-`
+ add_note("/clone -with_notes #{target_project.full_path}")
+
+ wait_for_requests
+
+ expect(page).to have_content 'Failed to clone this issue: wrong parameters.'
+ expect(issue.reload).to be_open
+ end
+ end
+ end
+
+ context 'when the project is valid but the user not authorized' do
+ let(:project_unauthorized) { create(:project, :public) }
+
+ it 'does not clone the issue' do
+ add_note("/clone #{project_unauthorized.full_path}")
+
+ wait_for_requests
+
+ expect(page).to have_content "Cloned this issue to #{project_unauthorized.full_path}."
+ expect(issue.reload).to be_open
+
+ visit project_issue_path(target_project, issue)
+
+ expect(page).not_to have_content 'Issues 1'
+ end
+ end
+
+ context 'when the project is invalid' do
+ it 'does not clone the issue' do
+ add_note("/clone not/valid")
+
+ wait_for_requests
+
+ expect(page).to have_content "Failed to clone this issue because target project doesn't exist."
+ expect(issue.reload).to be_open
+ end
+ end
+
+ context 'when the user issues multiple commands' do
+ let(:milestone) { create(:milestone, title: '1.0', project: project) }
+ let(:bug) { create(:label, project: project, title: 'bug') }
+ let(:wontfix) { create(:label, project: project, title: 'wontfix') }
+
+ let!(:target_milestone) { create(:milestone, title: '1.0', project: target_project) }
+
+ before do
+ target_project.add_maintainer(user)
+ end
+
+ shared_examples 'applies the commands to issues in both projects, target and source' do
+ it "applies quick actions" do
+ expect(page).to have_content "Cloned this issue to #{target_project.full_path}."
+ expect(issue.reload).to be_open
+
+ visit project_issue_path(target_project, issue)
+
+ expect(page).to have_content 'bug'
+ expect(page).to have_content 'wontfix'
+ expect(page).to have_content '1.0'
+
+ visit project_issue_path(project, issue)
+ expect(page).to have_content 'bug'
+ expect(page).to have_content 'wontfix'
+ expect(page).to have_content '1.0'
+ end
+ end
+
+ context 'applies multiple commands with clone command in the end' do
+ before do
+ add_note("/label ~#{bug.title} ~#{wontfix.title}\n\n/milestone %\"#{milestone.title}\"\n\n/clone #{target_project.full_path}")
+ end
+
+ it_behaves_like 'applies the commands to issues in both projects, target and source'
+ end
+
+ context 'applies multiple commands with clone command in the begining' do
+ before do
+ add_note("/clone #{target_project.full_path}\n\n/label ~#{bug.title} ~#{wontfix.title}\n\n/milestone %\"#{milestone.title}\"")
+ end
+
+ it_behaves_like 'applies the commands to issues in both projects, target and source'
+ end
+ end
+
+ context 'when editing comments' do
+ let(:target_project) { create(:project, :public) }
+
+ before do
+ target_project.add_maintainer(user)
+
+ sign_in(user)
+ visit project_issue_path(project, issue)
+ wait_for_all_requests
+ end
+
+ it 'clones the issue after quickcommand note was updated' do
+ # misspelled quick action
+ add_note("test note.\n/cloe #{target_project.full_path}")
+
+ expect(issue.reload).not_to be_closed
+
+ edit_note("/cloe #{target_project.full_path}", "test note.\n/clone #{target_project.full_path}")
+ wait_for_all_requests
+
+ expect(page).to have_content 'test note.'
+ expect(issue.reload).to be_open
+
+ visit project_issue_path(target_project, issue)
+ wait_for_all_requests
+
+ expect(page).to have_content 'Issues 1'
+ end
+
+ it 'deletes the note if it was updated to just contain a command' do
+ # missspelled quick action
+ add_note("test note.\n/cloe #{target_project.full_path}")
+
+ expect(page).not_to have_content 'Commands applied'
+
+ edit_note("/cloe #{target_project.full_path}", "/clone #{target_project.full_path}")
+ wait_for_all_requests
+
+ expect(page).not_to have_content "/clone #{target_project.full_path}"
+ expect(issue.reload).to be_open
+
+ visit project_issue_path(target_project, issue)
+ wait_for_all_requests
+
+ expect(page).to have_content 'Issues 1'
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/requests/api/conan_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/conan_packages_shared_examples.rb
index c56290a0aa9..49b6fc13900 100644
--- a/spec/support/shared_examples/requests/api/conan_packages_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/conan_packages_shared_examples.rb
@@ -629,6 +629,7 @@ RSpec.shared_examples 'workhorse recipe file upload endpoint' do
it_behaves_like 'rejects invalid recipe'
it_behaves_like 'rejects invalid file_name', 'conanfile.py.git%2fgit-upload-pack'
it_behaves_like 'uploads a package file'
+ it_behaves_like 'creates build_info when there is a job'
end
RSpec.shared_examples 'workhorse package file upload endpoint' do
@@ -649,6 +650,7 @@ RSpec.shared_examples 'workhorse package file upload endpoint' do
it_behaves_like 'rejects invalid recipe'
it_behaves_like 'rejects invalid file_name', 'conaninfo.txttest'
it_behaves_like 'uploads a package file'
+ it_behaves_like 'creates build_info when there is a job'
context 'tracking the conan_package.tgz upload' do
let(:file_name) { ::Packages::Conan::FileMetadatum::PACKAGE_BINARY }
@@ -657,6 +659,20 @@ RSpec.shared_examples 'workhorse package file upload endpoint' do
end
end
+RSpec.shared_examples 'creates build_info when there is a job' do
+ context 'with job token' do
+ let(:jwt) { build_jwt_from_job(job) }
+
+ it 'creates a build_info record' do
+ expect { subject }.to change { Packages::BuildInfo.count }.by(1)
+ end
+
+ it 'creates a package_file_build_info record' do
+ expect { subject }.to change { Packages::PackageFileBuildInfo.count }.by(1)
+ end
+ end
+end
+
RSpec.shared_examples 'uploads a package file' do
context 'file size above maximum limit' do
before do
diff --git a/spec/support/shared_examples/requests/api/graphql/group_and_project_boards_query_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/group_and_project_boards_query_shared_examples.rb
index 5145880ef9a..54f4ba7ff73 100644
--- a/spec/support/shared_examples/requests/api/graphql/group_and_project_boards_query_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/graphql/group_and_project_boards_query_shared_examples.rb
@@ -47,18 +47,12 @@ RSpec.shared_examples 'group and project boards query' do
describe 'sorting and pagination' do
let(:data_path) { [board_parent_type, :boards] }
- def pagination_query(params, page_info)
- graphql_query_for(
- board_parent_type,
- { 'fullPath' => board_parent.full_path },
- query_graphql_field('boards', params, "#{page_info} edges { node { id } }")
+ def pagination_query(params)
+ graphql_query_for(board_parent_type, { full_path: board_parent.full_path },
+ query_nodes(:boards, :id, include_pagination_info: true, args: params)
)
end
- def pagination_results_data(data)
- data.map { |board| board.dig('node', 'id') }
- end
-
context 'when using default sorting' do
let!(:board_B) { create(:board, resource_parent: board_parent, name: 'B') }
let!(:board_C) { create(:board, resource_parent: board_parent, name: 'C') }
@@ -72,9 +66,9 @@ RSpec.shared_examples 'group and project boards query' do
let(:first_param) { 2 }
let(:expected_results) do
if board_parent.multiple_issue_boards_available?
- boards.map { |board| board.to_global_id.to_s }
+ boards.map { |board| global_id_of(board) }
else
- [boards.first.to_global_id.to_s]
+ [global_id_of(boards.first)]
end
end
end
diff --git a/spec/support/shared_examples/requests/api/nuget_endpoints_shared_examples.rb b/spec/support/shared_examples/requests/api/nuget_endpoints_shared_examples.rb
new file mode 100644
index 00000000000..f808d12baf4
--- /dev/null
+++ b/spec/support/shared_examples/requests/api/nuget_endpoints_shared_examples.rb
@@ -0,0 +1,265 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'handling nuget service requests' do
+ subject { get api(url) }
+
+ context 'with valid project' do
+ using RSpec::Parameterized::TableSyntax
+
+ context 'personal token' do
+ where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do
+ 'PUBLIC' | :developer | true | true | 'process nuget service index request' | :success
+ 'PUBLIC' | :guest | true | true | 'process nuget service index request' | :success
+ 'PUBLIC' | :developer | true | false | 'process nuget service index request' | :success
+ 'PUBLIC' | :guest | true | false | 'process nuget service index request' | :success
+ 'PUBLIC' | :developer | false | true | 'process nuget service index request' | :success
+ 'PUBLIC' | :guest | false | true | 'process nuget service index request' | :success
+ 'PUBLIC' | :developer | false | false | 'process nuget service index request' | :success
+ 'PUBLIC' | :guest | false | false | 'process nuget service index request' | :success
+ 'PUBLIC' | :anonymous | false | true | 'process nuget service index request' | :success
+ 'PRIVATE' | :developer | true | true | 'process nuget service index request' | :success
+ 'PRIVATE' | :guest | true | true | 'rejects nuget packages access' | :forbidden
+ 'PRIVATE' | :developer | true | false | 'rejects nuget packages access' | :unauthorized
+ 'PRIVATE' | :guest | true | false | 'rejects nuget packages access' | :unauthorized
+ 'PRIVATE' | :developer | false | true | 'rejects nuget packages access' | :not_found
+ 'PRIVATE' | :guest | false | true | 'rejects nuget packages access' | :not_found
+ 'PRIVATE' | :developer | false | false | 'rejects nuget packages access' | :unauthorized
+ 'PRIVATE' | :guest | false | false | 'rejects nuget packages access' | :unauthorized
+ 'PRIVATE' | :anonymous | false | true | 'rejects nuget packages access' | :unauthorized
+ end
+
+ with_them do
+ let(:token) { user_token ? personal_access_token.token : 'wrong' }
+ let(:headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) }
+
+ subject { get api(url), headers: headers }
+
+ before do
+ project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility_level, false))
+ end
+
+ it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member]
+ end
+ end
+
+ context 'with job token' do
+ where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do
+ 'PUBLIC' | :developer | true | true | 'process nuget service index request' | :success
+ 'PUBLIC' | :guest | true | true | 'process nuget service index request' | :success
+ 'PUBLIC' | :developer | true | false | 'rejects nuget packages access' | :unauthorized
+ 'PUBLIC' | :guest | true | false | 'rejects nuget packages access' | :unauthorized
+ 'PUBLIC' | :developer | false | true | 'process nuget service index request' | :success
+ 'PUBLIC' | :guest | false | true | 'process nuget service index request' | :success
+ 'PUBLIC' | :developer | false | false | 'rejects nuget packages access' | :unauthorized
+ 'PUBLIC' | :guest | false | false | 'rejects nuget packages access' | :unauthorized
+ 'PUBLIC' | :anonymous | false | true | 'process nuget service index request' | :success
+ 'PRIVATE' | :developer | true | true | 'process nuget service index request' | :success
+ 'PRIVATE' | :guest | true | true | 'rejects nuget packages access' | :forbidden
+ 'PRIVATE' | :developer | true | false | 'rejects nuget packages access' | :unauthorized
+ 'PRIVATE' | :guest | true | false | 'rejects nuget packages access' | :unauthorized
+ 'PRIVATE' | :developer | false | true | 'rejects nuget packages access' | :not_found
+ 'PRIVATE' | :guest | false | true | 'rejects nuget packages access' | :not_found
+ 'PRIVATE' | :developer | false | false | 'rejects nuget packages access' | :unauthorized
+ 'PRIVATE' | :guest | false | false | 'rejects nuget packages access' | :unauthorized
+ 'PRIVATE' | :anonymous | false | true | 'rejects nuget packages access' | :unauthorized
+ end
+
+ with_them do
+ let(:job) { user_token ? create(:ci_build, project: project, user: user, status: :running) : double(token: 'wrong') }
+ let(:headers) { user_role == :anonymous ? {} : job_basic_auth_header(job) }
+
+ subject { get api(url), headers: headers }
+
+ before do
+ project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility_level, false))
+ end
+
+ it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member]
+ end
+ end
+ end
+
+ it_behaves_like 'deploy token for package GET requests'
+
+ it_behaves_like 'rejects nuget access with unknown project id'
+
+ it_behaves_like 'rejects nuget access with invalid project id'
+end
+
+RSpec.shared_examples 'handling nuget metadata requests with package name' do
+ include_context 'with expected presenters dependency groups'
+
+ let_it_be(:package_name) { 'Dummy.Package' }
+ let_it_be(:packages) { create_list(:nuget_package, 5, :with_metadatum, name: package_name, project: project) }
+ let_it_be(:tags) { packages.each { |pkg| create(:packages_tag, package: pkg, name: 'test') } }
+
+ subject { get api(url) }
+
+ before do
+ packages.each { |pkg| create_dependencies_for(pkg) }
+ end
+
+ context 'with valid project' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do
+ 'PUBLIC' | :developer | true | true | 'process nuget metadata request at package name level' | :success
+ 'PUBLIC' | :guest | true | true | 'process nuget metadata request at package name level' | :success
+ 'PUBLIC' | :developer | true | false | 'process nuget metadata request at package name level' | :success
+ 'PUBLIC' | :guest | true | false | 'process nuget metadata request at package name level' | :success
+ 'PUBLIC' | :developer | false | true | 'process nuget metadata request at package name level' | :success
+ 'PUBLIC' | :guest | false | true | 'process nuget metadata request at package name level' | :success
+ 'PUBLIC' | :developer | false | false | 'process nuget metadata request at package name level' | :success
+ 'PUBLIC' | :guest | false | false | 'process nuget metadata request at package name level' | :success
+ 'PUBLIC' | :anonymous | false | true | 'process nuget metadata request at package name level' | :success
+ 'PRIVATE' | :developer | true | true | 'process nuget metadata request at package name level' | :success
+ 'PRIVATE' | :guest | true | true | 'rejects nuget packages access' | :forbidden
+ 'PRIVATE' | :developer | true | false | 'rejects nuget packages access' | :unauthorized
+ 'PRIVATE' | :guest | true | false | 'rejects nuget packages access' | :unauthorized
+ 'PRIVATE' | :developer | false | true | 'rejects nuget packages access' | :not_found
+ 'PRIVATE' | :guest | false | true | 'rejects nuget packages access' | :not_found
+ 'PRIVATE' | :developer | false | false | 'rejects nuget packages access' | :unauthorized
+ 'PRIVATE' | :guest | false | false | 'rejects nuget packages access' | :unauthorized
+ 'PRIVATE' | :anonymous | false | true | 'rejects nuget packages access' | :unauthorized
+ end
+
+ with_them do
+ let(:token) { user_token ? personal_access_token.token : 'wrong' }
+ let(:headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) }
+
+ subject { get api(url), headers: headers }
+
+ before do
+ project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility_level, false))
+ end
+
+ it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member]
+ end
+
+ it_behaves_like 'deploy token for package GET requests'
+
+ it_behaves_like 'rejects nuget access with unknown project id'
+
+ it_behaves_like 'rejects nuget access with invalid project id'
+ end
+end
+
+RSpec.shared_examples 'handling nuget metadata requests with package name and package version' do
+ include_context 'with expected presenters dependency groups'
+
+ let_it_be(:package_name) { 'Dummy.Package' }
+ let_it_be(:package) { create(:nuget_package, :with_metadatum, name: 'Dummy.Package', project: project) }
+ let_it_be(:tag) { create(:packages_tag, package: package, name: 'test') }
+
+ subject { get api(url) }
+
+ before do
+ create_dependencies_for(package)
+ end
+
+ context 'with valid project' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do
+ 'PUBLIC' | :developer | true | true | 'process nuget metadata request at package name and package version level' | :success
+ 'PUBLIC' | :guest | true | true | 'process nuget metadata request at package name and package version level' | :success
+ 'PUBLIC' | :developer | true | false | 'process nuget metadata request at package name and package version level' | :success
+ 'PUBLIC' | :guest | true | false | 'process nuget metadata request at package name and package version level' | :success
+ 'PUBLIC' | :developer | false | true | 'process nuget metadata request at package name and package version level' | :success
+ 'PUBLIC' | :guest | false | true | 'process nuget metadata request at package name and package version level' | :success
+ 'PUBLIC' | :developer | false | false | 'process nuget metadata request at package name and package version level' | :success
+ 'PUBLIC' | :guest | false | false | 'process nuget metadata request at package name and package version level' | :success
+ 'PUBLIC' | :anonymous | false | true | 'process nuget metadata request at package name and package version level' | :success
+ 'PRIVATE' | :developer | true | true | 'process nuget metadata request at package name and package version level' | :success
+ 'PRIVATE' | :guest | true | true | 'rejects nuget packages access' | :forbidden
+ 'PRIVATE' | :developer | true | false | 'rejects nuget packages access' | :unauthorized
+ 'PRIVATE' | :guest | true | false | 'rejects nuget packages access' | :unauthorized
+ 'PRIVATE' | :developer | false | true | 'rejects nuget packages access' | :not_found
+ 'PRIVATE' | :guest | false | true | 'rejects nuget packages access' | :not_found
+ 'PRIVATE' | :developer | false | false | 'rejects nuget packages access' | :unauthorized
+ 'PRIVATE' | :guest | false | false | 'rejects nuget packages access' | :unauthorized
+ 'PRIVATE' | :anonymous | false | true | 'rejects nuget packages access' | :unauthorized
+ end
+
+ with_them do
+ let(:token) { user_token ? personal_access_token.token : 'wrong' }
+ let(:headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) }
+
+ subject { get api(url), headers: headers }
+
+ before do
+ project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility_level, false))
+ end
+
+ it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member]
+ end
+ end
+
+ it_behaves_like 'deploy token for package GET requests'
+
+ context 'with invalid package name' do
+ let_it_be(:package_name) { 'Unkown' }
+
+ it_behaves_like 'rejects nuget packages access', :developer, :not_found
+ end
+end
+
+RSpec.shared_examples 'handling nuget search requests' do
+ let_it_be(:package_a) { create(:nuget_package, :with_metadatum, name: 'Dummy.PackageA', project: project) }
+ let_it_be(:tag) { create(:packages_tag, package: package_a, name: 'test') }
+ let_it_be(:packages_b) { create_list(:nuget_package, 5, name: 'Dummy.PackageB', project: project) }
+ let_it_be(:packages_c) { create_list(:nuget_package, 5, name: 'Dummy.PackageC', project: project) }
+ let_it_be(:package_d) { create(:nuget_package, name: 'Dummy.PackageD', version: '5.0.5-alpha', project: project) }
+ let_it_be(:package_e) { create(:nuget_package, name: 'Foo.BarE', project: project) }
+ let(:search_term) { 'uMmy' }
+ let(:take) { 26 }
+ let(:skip) { 0 }
+ let(:include_prereleases) { true }
+ let(:query_parameters) { { q: search_term, take: take, skip: skip, prerelease: include_prereleases } }
+
+ subject { get api(url) }
+
+ context 'with valid project' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do
+ 'PUBLIC' | :developer | true | true | 'process nuget search request' | :success
+ 'PUBLIC' | :guest | true | true | 'process nuget search request' | :success
+ 'PUBLIC' | :developer | true | false | 'process nuget search request' | :success
+ 'PUBLIC' | :guest | true | false | 'process nuget search request' | :success
+ 'PUBLIC' | :developer | false | true | 'process nuget search request' | :success
+ 'PUBLIC' | :guest | false | true | 'process nuget search request' | :success
+ 'PUBLIC' | :developer | false | false | 'process nuget search request' | :success
+ 'PUBLIC' | :guest | false | false | 'process nuget search request' | :success
+ 'PUBLIC' | :anonymous | false | true | 'process nuget search request' | :success
+ 'PRIVATE' | :developer | true | true | 'process nuget search request' | :success
+ 'PRIVATE' | :guest | true | true | 'rejects nuget packages access' | :forbidden
+ 'PRIVATE' | :developer | true | false | 'rejects nuget packages access' | :unauthorized
+ 'PRIVATE' | :guest | true | false | 'rejects nuget packages access' | :unauthorized
+ 'PRIVATE' | :developer | false | true | 'rejects nuget packages access' | :not_found
+ 'PRIVATE' | :guest | false | true | 'rejects nuget packages access' | :not_found
+ 'PRIVATE' | :developer | false | false | 'rejects nuget packages access' | :unauthorized
+ 'PRIVATE' | :guest | false | false | 'rejects nuget packages access' | :unauthorized
+ 'PRIVATE' | :anonymous | false | true | 'rejects nuget packages access' | :unauthorized
+ end
+
+ with_them do
+ let(:token) { user_token ? personal_access_token.token : 'wrong' }
+ let(:headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) }
+
+ subject { get api(url), headers: headers }
+
+ before do
+ project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility_level, false))
+ end
+
+ it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member]
+ end
+ end
+
+ it_behaves_like 'deploy token for package GET requests'
+
+ it_behaves_like 'rejects nuget access with unknown project id'
+
+ it_behaves_like 'rejects nuget access with invalid project id'
+end
diff --git a/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb
index 58e99776fd9..dc6ac5f0371 100644
--- a/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb
@@ -12,7 +12,7 @@ RSpec.shared_examples 'rejects nuget packages access' do |user_type, status, add
it 'has the correct response header' do
subject
- expect(response.headers['Www-Authenticate: Basic realm']).to eq 'GitLab Packages Registry'
+ expect(response.headers['WWW-Authenticate']).to eq 'Basic realm="GitLab Packages Registry"'
end
end
end
@@ -26,7 +26,7 @@ RSpec.shared_examples 'process nuget service index request' do |user_type, statu
it_behaves_like 'returning response status', status
- it_behaves_like 'a package tracking event', described_class.name, 'cli_metadata'
+ it_behaves_like 'a package tracking event', 'API::NugetPackages', 'cli_metadata'
it 'returns a valid json response' do
subject
@@ -169,7 +169,7 @@ RSpec.shared_examples 'process nuget upload' do |user_type, status, add_member =
context 'with correct params' do
it_behaves_like 'package workhorse uploads'
it_behaves_like 'creates nuget package files'
- it_behaves_like 'a package tracking event', described_class.name, 'push_package'
+ it_behaves_like 'a package tracking event', 'API::NugetPackages', 'push_package'
end
end
@@ -286,7 +286,7 @@ RSpec.shared_examples 'process nuget download content request' do |user_type, st
it_behaves_like 'returning response status', status
- it_behaves_like 'a package tracking event', described_class.name, 'pull_package'
+ it_behaves_like 'a package tracking event', 'API::NugetPackages', 'pull_package'
it 'returns a valid package archive' do
subject
@@ -336,7 +336,7 @@ RSpec.shared_examples 'process nuget search request' do |user_type, status, add_
it_behaves_like 'returns a valid json search response', status, 4, [1, 5, 5, 1]
- it_behaves_like 'a package tracking event', described_class.name, 'search_package'
+ it_behaves_like 'a package tracking event', 'API::NugetPackages', 'search_package'
context 'with skip set to 2' do
let(:skip) { 2 }
diff --git a/spec/support/shared_examples/requests/graphql_shared_examples.rb b/spec/support/shared_examples/requests/graphql_shared_examples.rb
index 0045fe14501..a66bc7112fe 100644
--- a/spec/support/shared_examples/requests/graphql_shared_examples.rb
+++ b/spec/support/shared_examples/requests/graphql_shared_examples.rb
@@ -9,3 +9,8 @@ RSpec.shared_examples 'a working graphql query' do
expect(json_response.keys).to include('data')
end
end
+
+RSpec.shared_examples 'a mutation on an unauthorized resource' do
+ it_behaves_like 'a mutation that returns top-level errors',
+ errors: [::Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR]
+end
diff --git a/spec/support/shared_examples/requests/lfs_http_shared_examples.rb b/spec/support/shared_examples/requests/lfs_http_shared_examples.rb
index 4ae77179527..294ceffd77b 100644
--- a/spec/support/shared_examples/requests/lfs_http_shared_examples.rb
+++ b/spec/support/shared_examples/requests/lfs_http_shared_examples.rb
@@ -65,12 +65,19 @@ end
RSpec.shared_examples 'LFS http requests' do
include LfsHttpHelpers
+ let(:lfs_enabled) { true }
let(:authorize_guest) {}
let(:authorize_download) {}
let(:authorize_upload) {}
let(:lfs_object) { create(:lfs_object, :with_file) }
let(:sample_oid) { lfs_object.oid }
+ let(:sample_size) { lfs_object.size }
+ let(:sample_object) { { 'oid' => sample_oid, 'size' => sample_size } }
+ let(:non_existing_object_oid) { '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897' }
+ let(:non_existing_object_size) { 1575078 }
+ let(:non_existing_object) { { 'oid' => non_existing_object_oid, 'size' => non_existing_object_size } }
+ let(:multiple_objects) { [sample_object, non_existing_object] }
let(:authorization) { authorize_user }
let(:headers) do
@@ -89,13 +96,11 @@ RSpec.shared_examples 'LFS http requests' do
end
before do
- stub_lfs_setting(enabled: true)
+ stub_lfs_setting(enabled: lfs_enabled)
end
context 'when LFS is disabled globally' do
- before do
- stub_lfs_setting(enabled: false)
- end
+ let(:lfs_enabled) { false }
describe 'download request' do
before do
diff --git a/spec/support/shared_examples/requests/rack_attack_shared_examples.rb b/spec/support/shared_examples/requests/rack_attack_shared_examples.rb
index d4ee68309ff..5d300d38e4a 100644
--- a/spec/support/shared_examples/requests/rack_attack_shared_examples.rb
+++ b/spec/support/shared_examples/requests/rack_attack_shared_examples.rb
@@ -23,6 +23,11 @@ RSpec.shared_examples 'rate-limited token-authenticated requests' do
settings_to_set[:"#{throttle_setting_prefix}_period_in_seconds"] = period_in_seconds
end
+ after do
+ stub_env('GITLAB_THROTTLE_USER_ALLOWLIST', nil)
+ Gitlab::RackAttack.configure_user_allowlist
+ end
+
context 'when the throttle is enabled' do
before do
settings_to_set[:"#{throttle_setting_prefix}_enabled"] = true
@@ -30,6 +35,8 @@ RSpec.shared_examples 'rate-limited token-authenticated requests' do
end
it 'rejects requests over the rate limit' do
+ expect(Gitlab::Instrumentation::Throttle).not_to receive(:safelist=)
+
# At first, allow requests under the rate limit.
requests_per_period.times do
make_request(request_args)
@@ -40,6 +47,18 @@ RSpec.shared_examples 'rate-limited token-authenticated requests' do
expect_rejection { make_request(request_args) }
end
+ it 'does not reject requests if the user is in the allowlist' do
+ stub_env('GITLAB_THROTTLE_USER_ALLOWLIST', user.id.to_s)
+ Gitlab::RackAttack.configure_user_allowlist
+
+ expect(Gitlab::Instrumentation::Throttle).to receive(:safelist=).with('throttle_user_allowlist').at_least(:once)
+
+ (requests_per_period + 1).times do
+ make_request(request_args)
+ expect(response).not_to have_gitlab_http_status(:too_many_requests)
+ end
+ end
+
it 'allows requests after throttling and then waiting for the next period' do
requests_per_period.times do
make_request(request_args)
@@ -110,6 +129,14 @@ RSpec.shared_examples 'rate-limited token-authenticated requests' do
expect { make_request(request_args) }.not_to exceed_query_limit(control_count)
end
end
+
+ it_behaves_like 'tracking when dry-run mode is set' do
+ let(:throttle_name) { throttle_types[throttle_setting_prefix] }
+
+ def do_request
+ make_request(request_args)
+ end
+ end
end
context 'when the throttle is disabled' do
@@ -159,6 +186,11 @@ RSpec.shared_examples 'rate-limited web authenticated requests' do
settings_to_set[:"#{throttle_setting_prefix}_period_in_seconds"] = period_in_seconds
end
+ after do
+ stub_env('GITLAB_THROTTLE_USER_ALLOWLIST', nil)
+ Gitlab::RackAttack.configure_user_allowlist
+ end
+
context 'when the throttle is enabled' do
before do
settings_to_set[:"#{throttle_setting_prefix}_enabled"] = true
@@ -166,6 +198,8 @@ RSpec.shared_examples 'rate-limited web authenticated requests' do
end
it 'rejects requests over the rate limit' do
+ expect(Gitlab::Instrumentation::Throttle).not_to receive(:safelist=)
+
# At first, allow requests under the rate limit.
requests_per_period.times do
request_authenticated_web_url
@@ -176,6 +210,18 @@ RSpec.shared_examples 'rate-limited web authenticated requests' do
expect_rejection { request_authenticated_web_url }
end
+ it 'does not reject requests if the user is in the allowlist' do
+ stub_env('GITLAB_THROTTLE_USER_ALLOWLIST', user.id.to_s)
+ Gitlab::RackAttack.configure_user_allowlist
+
+ expect(Gitlab::Instrumentation::Throttle).to receive(:safelist=).with('throttle_user_allowlist').at_least(:once)
+
+ (requests_per_period + 1).times do
+ request_authenticated_web_url
+ expect(response).not_to have_gitlab_http_status(:too_many_requests)
+ end
+ end
+
it 'allows requests after throttling and then waiting for the next period' do
requests_per_period.times do
request_authenticated_web_url
@@ -245,6 +291,14 @@ RSpec.shared_examples 'rate-limited web authenticated requests' do
expect(Gitlab::AuthLogger).to receive(:error).with(arguments).once
expect { request_authenticated_web_url }.not_to exceed_query_limit(control_count)
end
+
+ it_behaves_like 'tracking when dry-run mode is set' do
+ let(:throttle_name) { throttle_types[throttle_setting_prefix] }
+
+ def do_request
+ request_authenticated_web_url
+ end
+ end
end
context 'when the throttle is disabled' do
@@ -269,3 +323,63 @@ RSpec.shared_examples 'rate-limited web authenticated requests' do
end
end
end
+
+# Requires:
+# - #do_request - This needs to be a method so the result isn't memoized
+# - throttle_name
+RSpec.shared_examples 'tracking when dry-run mode is set' do
+ let(:dry_run_config) { '*' }
+
+ # we can't use `around` here, because stub_env isn't supported outside of the
+ # example itself
+ before do
+ stub_env('GITLAB_THROTTLE_DRY_RUN', dry_run_config)
+ reset_rack_attack
+ end
+
+ after do
+ stub_env('GITLAB_THROTTLE_DRY_RUN', '')
+ reset_rack_attack
+ end
+
+ def reset_rack_attack
+ Rack::Attack.reset!
+ Rack::Attack.clear_configuration
+ Gitlab::RackAttack.configure(Rack::Attack)
+ end
+
+ it 'does not throttle the requests when `*` is configured' do
+ (1 + requests_per_period).times do
+ do_request
+ expect(response).not_to have_gitlab_http_status(:too_many_requests)
+ end
+ end
+
+ it 'logs RackAttack info into structured logs' do
+ arguments = a_hash_including({
+ message: 'Rack_Attack',
+ env: :track,
+ remote_ip: '127.0.0.1',
+ matched: throttle_name
+ })
+
+ expect(Gitlab::AuthLogger).to receive(:error).with(arguments)
+
+ (1 + requests_per_period).times do
+ do_request
+ end
+ end
+
+ context 'when configured with the the throttled name in a list' do
+ let(:dry_run_config) do
+ "throttle_list, #{throttle_name}, other_throttle"
+ end
+
+ it 'does not throttle' do
+ (1 + requests_per_period).times do
+ do_request
+ expect(response).not_to have_gitlab_http_status(:too_many_requests)
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/requests/self_monitoring_shared_examples.rb b/spec/support/shared_examples/requests/self_monitoring_shared_examples.rb
index db11b1fe07d..ff87fc5d8df 100644
--- a/spec/support/shared_examples/requests/self_monitoring_shared_examples.rb
+++ b/spec/support/shared_examples/requests/self_monitoring_shared_examples.rb
@@ -20,6 +20,18 @@ RSpec.shared_examples 'not accessible to non-admin users' do
expect(response).to have_gitlab_http_status(:not_found)
end
end
+
+ context 'with authenticated admin user without admin mode' do
+ before do
+ login_as(create(:admin))
+ end
+
+ it 'redirects to enable admin mode' do
+ subject
+
+ expect(response).to redirect_to(new_admin_session_path)
+ end
+ end
end
# Requires subject and worker_class and status_api to be defined
diff --git a/spec/support/shared_examples/requests/uploads_auhorize_shared_examples.rb b/spec/support/shared_examples/requests/uploads_auhorize_shared_examples.rb
new file mode 100644
index 00000000000..9cef5cfc25e
--- /dev/null
+++ b/spec/support/shared_examples/requests/uploads_auhorize_shared_examples.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'handle uploads authorize request' do
+ before do
+ login_as(user)
+ end
+
+ describe 'POST authorize' do
+ it 'authorizes workhorse header' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.media_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
+ expect(json_response['TempPath']).to eq(uploader_class.workhorse_local_upload_path)
+ end
+
+ it 'rejects requests that bypassed gitlab-workhorse' do
+ workhorse_headers.delete(Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER)
+
+ expect { subject }.to raise_error(JWT::DecodeError)
+ end
+
+ context 'when using remote storage' do
+ context 'when direct upload is enabled' do
+ before do
+ stub_uploads_object_storage(uploader_class, enabled: true, direct_upload: true)
+ end
+
+ it 'responds with status 200, location of file remote store and object details' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.media_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
+ expect(json_response).not_to have_key('TempPath')
+ expect(json_response['RemoteObject']).to have_key('ID')
+ expect(json_response['RemoteObject']).to have_key('GetURL')
+ expect(json_response['RemoteObject']).to have_key('StoreURL')
+ expect(json_response['RemoteObject']).to have_key('DeleteURL')
+ expect(json_response['RemoteObject']).to have_key('MultipartUpload')
+ expect(json_response['MaximumSize']).to eq(maximum_size)
+ end
+ end
+
+ context 'when direct upload is disabled' do
+ before do
+ stub_uploads_object_storage(uploader_class, enabled: true, direct_upload: false)
+ end
+
+ it 'handles as a local file' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.media_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
+ expect(json_response['TempPath']).to eq(uploader_class.workhorse_local_upload_path)
+ expect(json_response['RemoteObject']).to be_nil
+ expect(json_response['MaximumSize']).to eq(maximum_size)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/routing/git_http_routing_shared_examples.rb b/spec/support/shared_examples/routing/git_http_routing_shared_examples.rb
new file mode 100644
index 00000000000..b0e1e942d81
--- /dev/null
+++ b/spec/support/shared_examples/routing/git_http_routing_shared_examples.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'git repository routes' do
+ let(:params) { { repository_path: path.delete_prefix('/') } }
+ let(:container_path) { path.delete_suffix('.git') }
+
+ it 'routes Git endpoints' do
+ expect(get("#{path}/info/refs")).to route_to('repositories/git_http#info_refs', **params)
+ expect(post("#{path}/git-upload-pack")).to route_to('repositories/git_http#git_upload_pack', **params)
+ expect(post("#{path}/git-receive-pack")).to route_to('repositories/git_http#git_receive_pack', **params)
+ end
+
+ context 'requests without .git format' do
+ it 'redirects requests to /info/refs', type: :request do
+ expect(get("#{container_path}/info/refs")).to redirect_to("#{container_path}.git/info/refs")
+ expect(get("#{container_path}/info/refs?service=git-upload-pack")).to redirect_to("#{container_path}.git/info/refs?service=git-upload-pack")
+ expect(get("#{container_path}/info/refs?service=git-receive-pack")).to redirect_to("#{container_path}.git/info/refs?service=git-receive-pack")
+ end
+
+ it 'does not redirect other requests' do
+ expect(post("#{container_path}/git-upload-pack")).not_to be_routable
+ end
+ end
+
+ it 'routes LFS endpoints' do
+ oid = generate(:oid)
+
+ expect(post("#{path}/info/lfs/objects/batch")).to route_to('repositories/lfs_api#batch', **params)
+ expect(post("#{path}/info/lfs/objects")).to route_to('repositories/lfs_api#deprecated', **params)
+ expect(get("#{path}/info/lfs/objects/#{oid}")).to route_to('repositories/lfs_api#deprecated', oid: oid, **params)
+
+ expect(post("#{path}/info/lfs/locks/123/unlock")).to route_to('repositories/lfs_locks_api#unlock', id: '123', **params)
+ expect(post("#{path}/info/lfs/locks/verify")).to route_to('repositories/lfs_locks_api#verify', **params)
+
+ expect(get("#{path}/gitlab-lfs/objects/#{oid}")).to route_to('repositories/lfs_storage#download', oid: oid, **params)
+ expect(put("#{path}/gitlab-lfs/objects/#{oid}/456/authorize")).to route_to('repositories/lfs_storage#upload_authorize', oid: oid, size: '456', **params)
+ expect(put("#{path}/gitlab-lfs/objects/#{oid}/456")).to route_to('repositories/lfs_storage#upload_finalize', oid: oid, size: '456', **params)
+
+ expect(put("#{path}/gitlab-lfs/objects/foo")).not_to be_routable
+ expect(put("#{path}/gitlab-lfs/objects/#{oid}/foo")).not_to be_routable
+ expect(put("#{path}/gitlab-lfs/objects/#{oid}/foo/authorize")).not_to be_routable
+ end
+end
diff --git a/spec/support/shared_examples/services/alert_management_shared_examples.rb b/spec/support/shared_examples/services/alert_management_shared_examples.rb
index 003705ca21c..d9f28a97a0f 100644
--- a/spec/support/shared_examples/services/alert_management_shared_examples.rb
+++ b/spec/support/shared_examples/services/alert_management_shared_examples.rb
@@ -16,6 +16,38 @@ RSpec.shared_examples 'creates an alert management alert' do
end
end
+# This shared_example requires the following variables:
+# - last_alert_attributes, last created alert
+# - project, project that alert created
+# - payload_raw, hash representation of payload
+# - environment, project's environment
+# - fingerprint, fingerprint hash
+RSpec.shared_examples 'assigns the alert properties' do
+ it 'ensures that created alert has all data properly assigned' do
+ subject
+
+ expect(last_alert_attributes).to match(
+ project_id: project.id,
+ title: payload_raw.fetch(:title),
+ started_at: Time.zone.parse(payload_raw.fetch(:start_time)),
+ severity: payload_raw.fetch(:severity),
+ status: AlertManagement::Alert.status_value(:triggered),
+ events: 1,
+ domain: domain,
+ hosts: payload_raw.fetch(:hosts),
+ payload: payload_raw.with_indifferent_access,
+ issue_id: nil,
+ description: payload_raw.fetch(:description),
+ monitoring_tool: payload_raw.fetch(:monitoring_tool),
+ service: payload_raw.fetch(:service),
+ fingerprint: Digest::SHA1.hexdigest(fingerprint),
+ environment_id: environment.id,
+ ended_at: nil,
+ prometheus_alert_id: nil
+ )
+ end
+end
+
RSpec.shared_examples 'does not an create alert management alert' do
it 'does not create alert' do
expect { subject }.not_to change(AlertManagement::Alert, :count)
diff --git a/spec/support/shared_examples/services/container_registry_auth_service_shared_examples.rb b/spec/support/shared_examples/services/container_registry_auth_service_shared_examples.rb
new file mode 100644
index 00000000000..ba176b616c3
--- /dev/null
+++ b/spec/support/shared_examples/services/container_registry_auth_service_shared_examples.rb
@@ -0,0 +1,1006 @@
+# frozen_string_literal: true
+
+RSpec.shared_context 'container registry auth service context' do
+ let(:current_project) { nil }
+ let(:current_user) { nil }
+ let(:current_params) { {} }
+ let(:rsa_key) { OpenSSL::PKey::RSA.generate(512) }
+ let(:payload) { JWT.decode(subject[:token], rsa_key, true, { algorithm: 'RS256' }).first }
+
+ let(:authentication_abilities) do
+ [:read_container_image, :create_container_image, :admin_container_image]
+ end
+
+ let(:log_data) { { message: 'Denied container registry permissions' } }
+
+ subject do
+ described_class.new(current_project, current_user, current_params)
+ .execute(authentication_abilities: authentication_abilities)
+ end
+
+ before do
+ allow(Gitlab.config.registry).to receive_messages(enabled: true, issuer: 'rspec', key: nil)
+ allow_next_instance_of(JSONWebToken::RSAToken) do |instance|
+ allow(instance).to receive(:key).and_return(rsa_key)
+ end
+ end
+end
+
+RSpec.shared_examples 'an authenticated' do
+ it { is_expected.to include(:token) }
+ it { expect(payload).to include('access') }
+end
+
+RSpec.shared_examples 'a valid token' do
+ it { is_expected.to include(:token) }
+ it { expect(payload).to include('access') }
+
+ context 'a expirable' do
+ let(:expires_at) { Time.zone.at(payload['exp']) }
+ let(:expire_delay) { 10 }
+
+ context 'for default configuration' do
+ it { expect(expires_at).not_to be_within(2.seconds).of(Time.current + expire_delay.minutes) }
+ end
+
+ context 'for changed configuration' do
+ before do
+ stub_application_setting(container_registry_token_expire_delay: expire_delay)
+ end
+
+ it { expect(expires_at).to be_within(2.seconds).of(Time.current + expire_delay.minutes) }
+ end
+ end
+end
+
+RSpec.shared_examples 'a browsable' do
+ let(:access) do
+ [{ 'type' => 'registry',
+ 'name' => 'catalog',
+ 'actions' => ['*'] }]
+ end
+
+ it_behaves_like 'a valid token'
+ it_behaves_like 'not a container repository factory'
+
+ it 'has the correct scope' do
+ expect(payload).to include('access' => access)
+ end
+end
+
+RSpec.shared_examples 'an accessible' do
+ let(:access) do
+ [{ 'type' => 'repository',
+ 'name' => project.full_path,
+ 'actions' => actions }]
+ end
+
+ it_behaves_like 'a valid token'
+
+ it 'has the correct scope' do
+ expect(payload).to include('access' => access)
+ end
+end
+
+RSpec.shared_examples 'an inaccessible' do
+ it_behaves_like 'a valid token'
+ it { expect(payload).to include('access' => []) }
+end
+
+RSpec.shared_examples 'a deletable' do
+ it_behaves_like 'an accessible' do
+ let(:actions) { ['*'] }
+ end
+end
+
+RSpec.shared_examples 'a deletable since registry 2.7' do
+ it_behaves_like 'an accessible' do
+ let(:actions) { ['delete'] }
+ end
+end
+
+RSpec.shared_examples 'a pullable' do
+ it_behaves_like 'an accessible' do
+ let(:actions) { ['pull'] }
+ end
+end
+
+RSpec.shared_examples 'a pushable' do
+ it_behaves_like 'an accessible' do
+ let(:actions) { ['push'] }
+ end
+end
+
+RSpec.shared_examples 'a pullable and pushable' do
+ it_behaves_like 'an accessible' do
+ let(:actions) { %w(pull push) }
+ end
+end
+
+RSpec.shared_examples 'a forbidden' do
+ it { is_expected.to include(http_status: 403) }
+ it { is_expected.not_to include(:token) }
+end
+
+RSpec.shared_examples 'container repository factory' do
+ it 'creates a new container repository resource' do
+ expect { subject }
+ .to change { project.container_repositories.count }.by(1)
+ end
+end
+
+RSpec.shared_examples 'not a container repository factory' do
+ it 'does not create a new container repository resource' do
+ expect { subject }.not_to change { ContainerRepository.count }
+ end
+end
+
+RSpec.shared_examples 'logs an auth warning' do |requested_actions|
+ let(:expected) do
+ {
+ scope_type: 'repository',
+ requested_project_path: project.full_path,
+ requested_actions: requested_actions,
+ authorized_actions: [],
+ user_id: current_user.id,
+ username: current_user.username
+ }
+ end
+
+ it do
+ expect(Gitlab::AuthLogger).to receive(:warn).with(expected.merge!(log_data))
+
+ subject
+ end
+end
+
+RSpec.shared_examples 'a container registry auth service' do
+ include_context 'container registry auth service context'
+
+ describe '#full_access_token' do
+ let_it_be(:project) { create(:project) }
+ let(:token) { described_class.full_access_token(project.full_path) }
+
+ subject { { token: token } }
+
+ it_behaves_like 'an accessible' do
+ let(:actions) { ['*'] }
+ end
+
+ it_behaves_like 'not a container repository factory'
+ end
+
+ describe '#pull_access_token' do
+ let_it_be(:project) { create(:project) }
+ let(:token) { described_class.pull_access_token(project.full_path) }
+
+ subject { { token: token } }
+
+ it_behaves_like 'an accessible' do
+ let(:actions) { ['pull'] }
+ end
+
+ it_behaves_like 'not a container repository factory'
+ end
+
+ context 'user authorization' do
+ let_it_be(:current_user) { create(:user) }
+
+ context 'for registry catalog' do
+ let(:current_params) do
+ { scopes: ["registry:catalog:*"] }
+ end
+
+ context 'disallow browsing for users without GitLab admin rights' do
+ it_behaves_like 'an inaccessible'
+ it_behaves_like 'not a container repository factory'
+ end
+ end
+
+ context 'for private project' do
+ let_it_be(:project) { create(:project) }
+
+ context 'allow to use scope-less authentication' do
+ it_behaves_like 'a valid token'
+ end
+
+ context 'allow developer to push images' do
+ before_all do
+ project.add_developer(current_user)
+ end
+
+ let(:current_params) do
+ { scopes: ["repository:#{project.full_path}:push"] }
+ end
+
+ it_behaves_like 'a pushable'
+ it_behaves_like 'container repository factory'
+ end
+
+ context 'disallow developer to delete images' do
+ before_all do
+ project.add_developer(current_user)
+ end
+
+ let(:current_params) do
+ { scopes: ["repository:#{project.full_path}:*"] }
+ end
+
+ it_behaves_like 'an inaccessible'
+ it_behaves_like 'not a container repository factory'
+
+ it_behaves_like 'logs an auth warning', ['*']
+ end
+
+ context 'disallow developer to delete images since registry 2.7' do
+ before_all do
+ project.add_developer(current_user)
+ end
+
+ let(:current_params) do
+ { scopes: ["repository:#{project.full_path}:delete"] }
+ end
+
+ it_behaves_like 'an inaccessible'
+ it_behaves_like 'not a container repository factory'
+ end
+
+ context 'allow reporter to pull images' do
+ before_all do
+ project.add_reporter(current_user)
+ end
+
+ context 'when pulling from root level repository' do
+ let(:current_params) do
+ { scopes: ["repository:#{project.full_path}:pull"] }
+ end
+
+ it_behaves_like 'a pullable'
+ it_behaves_like 'not a container repository factory'
+ end
+ end
+
+ context 'disallow reporter to delete images' do
+ before_all do
+ project.add_reporter(current_user)
+ end
+
+ let(:current_params) do
+ { scopes: ["repository:#{project.full_path}:*"] }
+ end
+
+ it_behaves_like 'an inaccessible'
+ it_behaves_like 'not a container repository factory'
+ end
+
+ context 'disallow reporter to delete images since registry 2.7' do
+ before_all do
+ project.add_reporter(current_user)
+ end
+
+ let(:current_params) do
+ { scopes: ["repository:#{project.full_path}:delete"] }
+ end
+
+ it_behaves_like 'an inaccessible'
+ it_behaves_like 'not a container repository factory'
+ end
+
+ context 'return a least of privileges' do
+ before_all do
+ project.add_reporter(current_user)
+ end
+
+ let(:current_params) do
+ { scopes: ["repository:#{project.full_path}:push,pull"] }
+ end
+
+ it_behaves_like 'a pullable'
+ it_behaves_like 'not a container repository factory'
+ end
+
+ context 'disallow guest to pull or push images' do
+ before_all do
+ project.add_guest(current_user)
+ end
+
+ let(:current_params) do
+ { scopes: ["repository:#{project.full_path}:pull,push"] }
+ end
+
+ it_behaves_like 'an inaccessible'
+ it_behaves_like 'not a container repository factory'
+ end
+
+ context 'disallow guest to delete images' do
+ before_all do
+ project.add_guest(current_user)
+ end
+
+ let(:current_params) do
+ { scopes: ["repository:#{project.full_path}:*"] }
+ end
+
+ it_behaves_like 'an inaccessible'
+ it_behaves_like 'not a container repository factory'
+ end
+
+ context 'disallow guest to delete images since registry 2.7' do
+ before_all do
+ project.add_guest(current_user)
+ end
+
+ let(:current_params) do
+ { scopes: ["repository:#{project.full_path}:delete"] }
+ end
+
+ it_behaves_like 'an inaccessible'
+ it_behaves_like 'not a container repository factory'
+ end
+ end
+
+ context 'for public project' do
+ let_it_be(:project) { create(:project, :public) }
+
+ context 'allow anyone to pull images' do
+ let(:current_params) do
+ { scopes: ["repository:#{project.full_path}:pull"] }
+ end
+
+ it_behaves_like 'a pullable'
+ it_behaves_like 'not a container repository factory'
+ end
+
+ context 'disallow anyone to push images' do
+ let(:current_params) do
+ { scopes: ["repository:#{project.full_path}:push"] }
+ end
+
+ it_behaves_like 'an inaccessible'
+ it_behaves_like 'not a container repository factory'
+ end
+
+ context 'disallow anyone to delete images' do
+ let(:current_params) do
+ { scopes: ["repository:#{project.full_path}:*"] }
+ end
+
+ it_behaves_like 'an inaccessible'
+ it_behaves_like 'not a container repository factory'
+ end
+
+ context 'disallow anyone to delete images since registry 2.7' do
+ let(:current_params) do
+ { scopes: ["repository:#{project.full_path}:delete"] }
+ end
+
+ it_behaves_like 'an inaccessible'
+ it_behaves_like 'not a container repository factory'
+ end
+
+ context 'when repository name is invalid' do
+ let(:current_params) do
+ { scopes: ['repository:invalid:push'] }
+ end
+
+ it_behaves_like 'an inaccessible'
+ it_behaves_like 'not a container repository factory'
+ end
+ end
+
+ context 'for internal project' do
+ let_it_be(:project) { create(:project, :internal) }
+
+ context 'for internal user' do
+ context 'allow anyone to pull images' do
+ let(:current_params) do
+ { scopes: ["repository:#{project.full_path}:pull"] }
+ end
+
+ it_behaves_like 'a pullable'
+ it_behaves_like 'not a container repository factory'
+ end
+
+ context 'disallow anyone to push images' do
+ let(:current_params) do
+ { scopes: ["repository:#{project.full_path}:push"] }
+ end
+
+ it_behaves_like 'an inaccessible'
+ it_behaves_like 'not a container repository factory'
+ end
+
+ context 'disallow anyone to delete images' do
+ let(:current_params) do
+ { scopes: ["repository:#{project.full_path}:*"] }
+ end
+
+ it_behaves_like 'an inaccessible'
+ it_behaves_like 'not a container repository factory'
+ end
+
+ context 'disallow anyone to delete images since registry 2.7' do
+ let(:current_params) do
+ { scopes: ["repository:#{project.full_path}:delete"] }
+ end
+
+ it_behaves_like 'an inaccessible'
+ it_behaves_like 'not a container repository factory'
+ end
+ end
+
+ context 'for external user' do
+ context 'disallow anyone to pull or push images' do
+ let_it_be(:current_user) { create(:user, external: true) }
+ let(:current_params) do
+ { scopes: ["repository:#{project.full_path}:pull,push"] }
+ end
+
+ it_behaves_like 'an inaccessible'
+ it_behaves_like 'not a container repository factory'
+ end
+
+ context 'disallow anyone to delete images' do
+ let_it_be(:current_user) { create(:user, external: true) }
+ let(:current_params) do
+ { scopes: ["repository:#{project.full_path}:*"] }
+ end
+
+ it_behaves_like 'an inaccessible'
+ it_behaves_like 'not a container repository factory'
+ end
+
+ context 'disallow anyone to delete images since registry 2.7' do
+ let_it_be(:current_user) { create(:user, external: true) }
+ let(:current_params) do
+ { scopes: ["repository:#{project.full_path}:delete"] }
+ end
+
+ it_behaves_like 'an inaccessible'
+ it_behaves_like 'not a container repository factory'
+ end
+ end
+ end
+ end
+
+ context 'delete authorized as maintainer' do
+ let_it_be(:current_project) { create(:project) }
+ let_it_be(:current_user) { create(:user) }
+
+ let(:authentication_abilities) do
+ [:admin_container_image]
+ end
+
+ before_all do
+ current_project.add_maintainer(current_user)
+ end
+
+ it_behaves_like 'a valid token'
+
+ context 'allow to delete images' do
+ let(:current_params) do
+ { scopes: ["repository:#{current_project.full_path}:*"] }
+ end
+
+ it_behaves_like 'a deletable' do
+ let(:project) { current_project }
+ end
+ end
+
+ context 'allow to delete images since registry 2.7' do
+ let(:current_params) do
+ { scopes: ["repository:#{current_project.full_path}:delete"] }
+ end
+
+ it_behaves_like 'a deletable since registry 2.7' do
+ let(:project) { current_project }
+ end
+ end
+ end
+
+ context 'build authorized as user' do
+ let_it_be(:current_project) { create(:project) }
+ let_it_be(:current_user) { create(:user) }
+
+ let(:authentication_abilities) do
+ [:build_read_container_image, :build_create_container_image, :build_destroy_container_image]
+ end
+
+ before_all do
+ current_project.add_developer(current_user)
+ end
+
+ context 'allow to use offline_token' do
+ let(:current_params) do
+ { offline_token: true }
+ end
+
+ it_behaves_like 'an authenticated'
+ end
+
+ it_behaves_like 'a valid token'
+
+ context 'allow to pull and push images' do
+ let(:current_params) do
+ { scopes: ["repository:#{current_project.full_path}:pull,push"] }
+ end
+
+ it_behaves_like 'a pullable and pushable' do
+ let(:project) { current_project }
+ end
+
+ it_behaves_like 'container repository factory' do
+ let(:project) { current_project }
+ end
+ end
+
+ context 'allow to delete images since registry 2.7' do
+ let(:current_params) do
+ { scopes: ["repository:#{current_project.full_path}:delete"] }
+ end
+
+ it_behaves_like 'a deletable since registry 2.7' do
+ let(:project) { current_project }
+ end
+ end
+
+ context 'disallow to delete images' do
+ let(:current_params) do
+ { scopes: ["repository:#{current_project.full_path}:*"] }
+ end
+
+ it_behaves_like 'an inaccessible' do
+ let(:project) { current_project }
+ end
+ end
+
+ context 'for other projects' do
+ context 'when pulling' do
+ let(:current_params) do
+ { scopes: ["repository:#{project.full_path}:pull"] }
+ end
+
+ context 'allow for public' do
+ let_it_be(:project) { create(:project, :public) }
+
+ it_behaves_like 'a pullable'
+ it_behaves_like 'not a container repository factory'
+ end
+
+ shared_examples 'pullable for being team member' do
+ context 'when you are not member' do
+ it_behaves_like 'an inaccessible'
+ it_behaves_like 'not a container repository factory'
+ end
+
+ context 'when you are member' do
+ before_all do
+ project.add_developer(current_user)
+ end
+
+ it_behaves_like 'a pullable'
+ it_behaves_like 'not a container repository factory'
+ end
+
+ context 'when you are owner' do
+ let_it_be(:project) { create(:project, namespace: current_user.namespace) }
+
+ it_behaves_like 'a pullable'
+ it_behaves_like 'not a container repository factory'
+ end
+ end
+
+ context 'for private' do
+ let_it_be(:project) { create(:project, :private) }
+
+ it_behaves_like 'pullable for being team member'
+
+ context 'when you are admin' do
+ let_it_be(:current_user) { create(:admin) }
+
+ context 'when you are not member' do
+ it_behaves_like 'an inaccessible'
+ it_behaves_like 'not a container repository factory'
+ end
+
+ context 'when you are member' do
+ before_all do
+ project.add_developer(current_user)
+ end
+
+ it_behaves_like 'a pullable'
+ it_behaves_like 'not a container repository factory'
+ end
+
+ context 'when you are owner' do
+ let_it_be(:project) { create(:project, namespace: current_user.namespace) }
+
+ it_behaves_like 'a pullable'
+ it_behaves_like 'not a container repository factory'
+ end
+ end
+ end
+ end
+
+ context 'when pushing' do
+ let(:current_params) do
+ { scopes: ["repository:#{project.full_path}:push"] }
+ end
+
+ context 'disallow for all' do
+ context 'when you are member' do
+ let_it_be(:project) { create(:project, :public) }
+
+ before_all do
+ project.add_developer(current_user)
+ end
+
+ it_behaves_like 'an inaccessible'
+ it_behaves_like 'not a container repository factory'
+ end
+
+ context 'when you are owner' do
+ let_it_be(:project) { create(:project, :public, namespace: current_user.namespace) }
+
+ it_behaves_like 'an inaccessible'
+ it_behaves_like 'not a container repository factory'
+ end
+ end
+ end
+ end
+
+ context 'for project without container registry' do
+ let_it_be(:project) { create(:project, :public, container_registry_enabled: false) }
+
+ before do
+ project.update!(container_registry_enabled: false)
+ end
+
+ context 'disallow when pulling' do
+ let(:current_params) do
+ { scopes: ["repository:#{project.full_path}:pull"] }
+ end
+
+ it_behaves_like 'an inaccessible'
+ it_behaves_like 'not a container repository factory'
+ end
+ end
+
+ context 'for project that disables repository' do
+ let_it_be(:project) { create(:project, :public, :repository_disabled) }
+
+ context 'disallow when pulling' do
+ let(:current_params) do
+ { scopes: ["repository:#{project.full_path}:pull"] }
+ end
+
+ it_behaves_like 'an inaccessible'
+ it_behaves_like 'not a container repository factory'
+ end
+ end
+ end
+
+ context 'registry catalog browsing authorized as admin' do
+ let_it_be(:current_user) { create(:user, :admin) }
+ let_it_be(:project) { create(:project, :public) }
+
+ let(:current_params) do
+ { scopes: ["registry:catalog:*"] }
+ end
+
+ it_behaves_like 'a browsable'
+ end
+
+ context 'support for multiple scopes' do
+ let_it_be(:internal_project) { create(:project, :internal) }
+ let_it_be(:private_project) { create(:project, :private) }
+
+ let(:current_params) do
+ {
+ scopes: [
+ "repository:#{internal_project.full_path}:pull",
+ "repository:#{private_project.full_path}:pull"
+ ]
+ }
+ end
+
+ context 'user has access to all projects' do
+ let_it_be(:current_user) { create(:user, :admin) }
+
+ before do
+ enable_admin_mode!(current_user)
+ end
+
+ it_behaves_like 'a browsable' do
+ let(:access) do
+ [
+ { 'type' => 'repository',
+ 'name' => internal_project.full_path,
+ 'actions' => ['pull'] },
+ { 'type' => 'repository',
+ 'name' => private_project.full_path,
+ 'actions' => ['pull'] }
+ ]
+ end
+ end
+ end
+
+ context 'user only has access to internal project' do
+ let_it_be(:current_user) { create(:user) }
+
+ it_behaves_like 'a browsable' do
+ let(:access) do
+ [
+ { 'type' => 'repository',
+ 'name' => internal_project.full_path,
+ 'actions' => ['pull'] }
+ ]
+ end
+ end
+ end
+
+ context 'anonymous access is rejected' do
+ let(:current_user) { nil }
+
+ it_behaves_like 'a forbidden'
+ end
+ end
+
+ context 'unauthorized' do
+ context 'disallow to use scope-less authentication' do
+ it_behaves_like 'a forbidden'
+ it_behaves_like 'not a container repository factory'
+ end
+
+ context 'for invalid scope' do
+ let(:current_params) do
+ { scopes: ['invalid:aa:bb'] }
+ end
+
+ it_behaves_like 'a forbidden'
+ it_behaves_like 'not a container repository factory'
+ end
+
+ context 'for private project' do
+ let_it_be(:project) { create(:project, :private) }
+
+ let(:current_params) do
+ { scopes: ["repository:#{project.full_path}:pull"] }
+ end
+
+ it_behaves_like 'a forbidden'
+ end
+
+ context 'for public project' do
+ let_it_be(:project) { create(:project, :public) }
+
+ context 'when pulling and pushing' do
+ let(:current_params) do
+ { scopes: ["repository:#{project.full_path}:pull,push"] }
+ end
+
+ it_behaves_like 'a pullable'
+ it_behaves_like 'not a container repository factory'
+ end
+
+ context 'when pushing' do
+ let(:current_params) do
+ { scopes: ["repository:#{project.full_path}:push"] }
+ end
+
+ it_behaves_like 'a forbidden'
+ it_behaves_like 'not a container repository factory'
+ end
+ end
+
+ context 'for registry catalog' do
+ let(:current_params) do
+ { scopes: ["registry:catalog:*"] }
+ end
+
+ it_behaves_like 'a forbidden'
+ it_behaves_like 'not a container repository factory'
+ end
+ end
+
+ context 'for deploy tokens' do
+ let(:current_params) do
+ { scopes: ["repository:#{project.full_path}:pull"] }
+ end
+
+ context 'when deploy token has read and write registry as scopes' do
+ let(:current_user) { create(:deploy_token, write_registry: true, projects: [project]) }
+
+ shared_examples 'able to login' do
+ context 'registry provides read_container_image authentication_abilities' do
+ let(:current_params) { {} }
+ let(:authentication_abilities) { [:read_container_image] }
+
+ it_behaves_like 'an authenticated'
+ end
+ end
+
+ context 'for public project' do
+ let_it_be(:project) { create(:project, :public) }
+
+ context 'when pulling' do
+ it_behaves_like 'a pullable'
+ end
+
+ context 'when pushing' do
+ let(:current_params) do
+ { scopes: ["repository:#{project.full_path}:push"] }
+ end
+
+ it_behaves_like 'a pushable'
+ end
+
+ it_behaves_like 'able to login'
+ end
+
+ context 'for internal project' do
+ let_it_be(:project) { create(:project, :internal) }
+
+ context 'when pulling' do
+ it_behaves_like 'a pullable'
+ end
+
+ context 'when pushing' do
+ let(:current_params) do
+ { scopes: ["repository:#{project.full_path}:push"] }
+ end
+
+ it_behaves_like 'a pushable'
+ end
+
+ it_behaves_like 'able to login'
+ end
+
+ context 'for private project' do
+ let_it_be(:project) { create(:project, :private) }
+
+ context 'when pulling' do
+ it_behaves_like 'a pullable'
+ end
+
+ context 'when pushing' do
+ let(:current_params) do
+ { scopes: ["repository:#{project.full_path}:push"] }
+ end
+
+ it_behaves_like 'a pushable'
+ end
+
+ it_behaves_like 'able to login'
+ end
+ end
+
+ context 'when deploy token does not have read_registry scope' do
+ let(:current_user) { create(:deploy_token, projects: [project], read_registry: false) }
+
+ shared_examples 'unable to login' do
+ context 'registry provides no container authentication_abilities' do
+ let(:current_params) { {} }
+ let(:authentication_abilities) { [] }
+
+ it_behaves_like 'a forbidden'
+ end
+
+ context 'registry provides inapplicable container authentication_abilities' do
+ let(:current_params) { {} }
+ let(:authentication_abilities) { [:download_code] }
+
+ it_behaves_like 'a forbidden'
+ end
+ end
+
+ context 'for public project' do
+ let_it_be(:project) { create(:project, :public) }
+
+ context 'when pulling' do
+ it_behaves_like 'a pullable'
+ end
+
+ it_behaves_like 'unable to login'
+ end
+
+ context 'for internal project' do
+ let_it_be(:project) { create(:project, :internal) }
+
+ context 'when pulling' do
+ it_behaves_like 'an inaccessible'
+ end
+
+ it_behaves_like 'unable to login'
+ end
+
+ context 'for private project' do
+ let_it_be(:project) { create(:project, :internal) }
+
+ context 'when pulling' do
+ it_behaves_like 'an inaccessible'
+ end
+
+ context 'when logging in' do
+ let(:current_params) { {} }
+ let(:authentication_abilities) { [] }
+
+ it_behaves_like 'a forbidden'
+ end
+
+ it_behaves_like 'unable to login'
+ end
+ end
+
+ context 'when deploy token is not related to the project' do
+ let_it_be(:current_user) { create(:deploy_token, read_registry: false) }
+
+ context 'for public project' do
+ let_it_be(:project) { create(:project, :public) }
+
+ context 'when pulling' do
+ it_behaves_like 'a pullable'
+ end
+ end
+
+ context 'for internal project' do
+ let_it_be(:project) { create(:project, :internal) }
+
+ context 'when pulling' do
+ it_behaves_like 'an inaccessible'
+ end
+ end
+
+ context 'for private project' do
+ let_it_be(:project) { create(:project, :internal) }
+
+ context 'when pulling' do
+ it_behaves_like 'an inaccessible'
+ end
+ end
+ end
+
+ context 'when deploy token has been revoked' do
+ let(:current_user) { create(:deploy_token, :revoked, projects: [project]) }
+
+ context 'for public project' do
+ let_it_be(:project) { create(:project, :public) }
+
+ it_behaves_like 'a pullable'
+ end
+
+ context 'for internal project' do
+ let_it_be(:project) { create(:project, :internal) }
+
+ it_behaves_like 'an inaccessible'
+ end
+
+ context 'for private project' do
+ let_it_be(:project) { create(:project, :internal) }
+
+ it_behaves_like 'an inaccessible'
+ end
+ end
+ end
+
+ context 'user authorization' do
+ let_it_be(:current_user) { create(:user) }
+
+ context 'with multiple scopes' do
+ let_it_be(:project) { create(:project) }
+
+ context 'allow developer to push images' do
+ before_all do
+ project.add_developer(current_user)
+ end
+
+ let(:current_params) do
+ { scopes: ["repository:#{project.full_path}:push"] }
+ end
+
+ it_behaves_like 'a pushable'
+ it_behaves_like 'container repository factory'
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/services/incident_shared_examples.rb b/spec/support/shared_examples/services/incident_shared_examples.rb
index 39c22ac8aa3..9fced12b543 100644
--- a/spec/support/shared_examples/services/incident_shared_examples.rb
+++ b/spec/support/shared_examples/services/incident_shared_examples.rb
@@ -11,11 +11,13 @@
#
# include_examples 'incident issue'
RSpec.shared_examples 'incident issue' do
- let(:label_properties) { attributes_for(:label, :incident) }
-
it 'has incident as issue type' do
expect(issue.issue_type).to eq('incident')
end
+end
+
+RSpec.shared_examples 'has incident label' do
+ let(:label_properties) { attributes_for(:label, :incident) }
it 'has exactly one incident label' do
expect(issue.labels).to be_one do |label|
diff --git a/spec/support/shared_examples/services/metrics/dashboard_shared_examples.rb b/spec/support/shared_examples/services/metrics/dashboard_shared_examples.rb
index 1501a2a0f52..b6c33eac7b4 100644
--- a/spec/support/shared_examples/services/metrics/dashboard_shared_examples.rb
+++ b/spec/support/shared_examples/services/metrics/dashboard_shared_examples.rb
@@ -46,7 +46,7 @@ RSpec.shared_examples 'refreshes cache when dashboard_version is changed' do
allow(service).to receive(:dashboard_version).and_return('1', '2')
end
- expect(File).to receive(:read).twice.and_call_original
+ expect_file_read(Rails.root.join(described_class::DASHBOARD_PATH)).twice.and_call_original
service = described_class.new(*service_params)
diff --git a/spec/support/shared_examples/services/packages_shared_examples.rb b/spec/support/shared_examples/services/packages_shared_examples.rb
index 7987f2c296b..70d29b4bc99 100644
--- a/spec/support/shared_examples/services/packages_shared_examples.rb
+++ b/spec/support/shared_examples/services/packages_shared_examples.rb
@@ -14,6 +14,24 @@ RSpec.shared_examples 'assigns build to package' do
end
end
+RSpec.shared_examples 'assigns build to package file' do
+ context 'with build info' do
+ let(:job) { create(:ci_build, user: user) }
+ let(:params) { super().merge(build: job) }
+
+ it 'assigns the pipeline to the package' do
+ package_file = subject
+
+ expect(package_file.package_file_build_infos).to be_present
+ expect(package_file.pipelines.first).to eq job.pipeline
+ end
+
+ it 'creates a new PackageFileBuildInfo record' do
+ expect { subject }.to change { Packages::PackageFileBuildInfo.count }.by(1)
+ end
+ end
+end
+
RSpec.shared_examples 'assigns the package creator' do
it 'assigns the package creator' do
subject
diff --git a/spec/support/shared_examples/workers/concerns/reenqueuer_shared_examples.rb b/spec/support/shared_examples/workers/concerns/reenqueuer_shared_examples.rb
index 37f44f98cda..1b09b5fe613 100644
--- a/spec/support/shared_examples/workers/concerns/reenqueuer_shared_examples.rb
+++ b/spec/support/shared_examples/workers/concerns/reenqueuer_shared_examples.rb
@@ -10,6 +10,10 @@ RSpec.shared_examples 'reenqueuer' do
expect(subject.lease_timeout).to be_a(ActiveSupport::Duration)
end
+ it 'uses the :none deduplication strategy' do
+ expect(subject.class.get_deduplicate_strategy).to eq(:none)
+ end
+
describe '#perform' do
it 'tries to obtain a lease' do
expect_to_obtain_exclusive_lease(subject.lease_key)
diff --git a/spec/support/shared_examples/workers/project_export_shared_examples.rb b/spec/support/shared_examples/workers/project_export_shared_examples.rb
new file mode 100644
index 00000000000..a9bcc3f4f7c
--- /dev/null
+++ b/spec/support/shared_examples/workers/project_export_shared_examples.rb
@@ -0,0 +1,82 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'export worker' do
+ describe '#perform' do
+ let!(:user) { create(:user) }
+ let!(:project) { create(:project) }
+
+ before do
+ allow_next_instance_of(described_class) do |job|
+ allow(job).to receive(:jid).and_return(SecureRandom.hex(8))
+ end
+ end
+
+ context 'when it succeeds' do
+ it 'calls the ExportService' do
+ expect_next_instance_of(::Projects::ImportExport::ExportService) do |service|
+ expect(service).to receive(:execute)
+ end
+
+ subject.perform(user.id, project.id, { 'klass' => 'Gitlab::ImportExport::AfterExportStrategies::DownloadNotificationStrategy' })
+ end
+
+ context 'export job' do
+ before do
+ allow_next_instance_of(::Projects::ImportExport::ExportService) do |service|
+ allow(service).to receive(:execute)
+ end
+ end
+
+ it 'creates an export job record for the project' do
+ expect { subject.perform(user.id, project.id, {}) }.to change { project.export_jobs.count }.from(0).to(1)
+ end
+
+ it 'sets the export job status to started' do
+ expect_next_instance_of(ProjectExportJob) do |job|
+ expect(job).to receive(:start)
+ end
+
+ subject.perform(user.id, project.id, {})
+ end
+
+ it 'sets the export job status to finished' do
+ expect_next_instance_of(ProjectExportJob) do |job|
+ expect(job).to receive(:finish)
+ end
+
+ subject.perform(user.id, project.id, {})
+ end
+ end
+ end
+
+ context 'when it fails' do
+ it 'does not raise an exception when strategy is invalid' do
+ expect(::Projects::ImportExport::ExportService).not_to receive(:new)
+
+ expect { subject.perform(user.id, project.id, { 'klass' => 'Whatever' }) }.not_to raise_error
+ end
+
+ it 'does not raise error when project cannot be found' do
+ expect { subject.perform(user.id, non_existing_record_id, {}) }.not_to raise_error
+ end
+
+ it 'does not raise error when user cannot be found' do
+ expect { subject.perform(non_existing_record_id, project.id, {}) }.not_to raise_error
+ end
+ end
+ end
+
+ describe 'sidekiq options' do
+ it 'disables retry' do
+ expect(described_class.sidekiq_options['retry']).to eq(false)
+ end
+
+ it 'disables dead' do
+ expect(described_class.sidekiq_options['dead']).to eq(false)
+ end
+
+ it 'sets default status expiration' do
+ expect(described_class.sidekiq_options['status_expiration']).to eq(StuckExportJobsWorker::EXPORT_JOBS_EXPIRATION)
+ end
+ end
+end
diff --git a/spec/support/snowplow.rb b/spec/support/snowplow.rb
index b67fa96fab8..0d6102f1705 100644
--- a/spec/support/snowplow.rb
+++ b/spec/support/snowplow.rb
@@ -9,12 +9,15 @@ RSpec.configure do |config|
# WebMock is set up to allow requests to `localhost`
host = 'localhost'
+ allow_any_instance_of(Gitlab::Tracking::Destinations::ProductAnalytics).to receive(:event)
+
allow_any_instance_of(Gitlab::Tracking::Destinations::Snowplow)
.to receive(:emitter)
.and_return(SnowplowTracker::Emitter.new(host, buffer_size: buffer_size))
stub_application_setting(snowplow_enabled: true)
+ allow(SnowplowTracker::SelfDescribingJson).to receive(:new).and_call_original
allow(Gitlab::Tracking).to receive(:event).and_call_original # rubocop:disable RSpec/ExpectGitlabTracking
end