summaryrefslogtreecommitdiff
path: root/spec/support
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-03-16 18:18:33 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2021-03-16 18:18:33 +0000
commitf64a639bcfa1fc2bc89ca7db268f594306edfd7c (patch)
treea2c3c2ebcc3b45e596949db485d6ed18ffaacfa1 /spec/support
parentbfbc3e0d6583ea1a91f627528bedc3d65ba4b10f (diff)
downloadgitlab-ce-f64a639bcfa1fc2bc89ca7db268f594306edfd7c.tar.gz
Add latest changes from gitlab-org/gitlab@13-10-stable-eev13.10.0-rc40
Diffstat (limited to 'spec/support')
-rw-r--r--spec/support/capybara.rb24
-rw-r--r--spec/support/gitlab_experiment.rb22
-rw-r--r--spec/support/graphql/resolver_factories.rb40
-rw-r--r--spec/support/helpers/cycle_analytics_helpers.rb4
-rw-r--r--spec/support/helpers/database/database_helpers.rb56
-rw-r--r--spec/support/helpers/dependency_proxy_helpers.rb4
-rw-r--r--spec/support/helpers/design_management_test_helpers.rb2
-rw-r--r--spec/support/helpers/features/releases_helpers.rb105
-rw-r--r--spec/support/helpers/gpg_helpers.rb4
-rw-r--r--spec/support/helpers/graphql_helpers.rb200
-rw-r--r--spec/support/helpers/javascript_fixtures_helpers.rb1
-rw-r--r--spec/support/helpers/notification_helpers.rb4
-rw-r--r--spec/support/helpers/stub_object_storage.rb2
-rw-r--r--spec/support/helpers/test_env.rb24
-rw-r--r--spec/support/matchers/background_migrations_matchers.rb20
-rw-r--r--spec/support/matchers/email_matcher.rb19
-rw-r--r--spec/support/matchers/graphql_matchers.rb9
-rw-r--r--spec/support/services/issues/move_and_clone_services_shared_examples.rb22
-rw-r--r--spec/support/services/service_response_shared_examples.rb25
-rw-r--r--spec/support/shared_contexts/features/error_tracking_shared_context.rb2
-rw-r--r--spec/support/shared_contexts/issuable/merge_request_shared_context.rb64
-rw-r--r--spec/support/shared_contexts/navbar_structure_context.rb11
-rw-r--r--spec/support/shared_contexts/policies/group_policy_shared_context.rb4
-rw-r--r--spec/support/shared_contexts/policies/project_policy_shared_context.rb6
-rw-r--r--spec/support/shared_contexts/policies/project_policy_table_shared_context.rb2
-rw-r--r--spec/support/shared_contexts/requests/api/npm_packages_shared_context.rb9
-rw-r--r--spec/support/shared_contexts/security_and_compliance_permissions_shared_context.rb33
-rw-r--r--spec/support/shared_examples/alert_notification_service_shared_examples.rb15
-rw-r--r--spec/support/shared_examples/banzai/filters/emoji_shared_examples.rb46
-rw-r--r--spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/controllers/create_notes_rate_limit_shared_examples.rb44
-rw-r--r--spec/support/shared_examples/controllers/snippet_blob_shared_examples.rb32
-rw-r--r--spec/support/shared_examples/features/comment_and_close_button_shared_examples.rb29
-rw-r--r--spec/support/shared_examples/features/discussion_comments_shared_example.rb216
-rw-r--r--spec/support/shared_examples/features/issuable_invite_members_shared_examples.rb10
-rw-r--r--spec/support/shared_examples/features/project_upload_files_shared_examples.rb34
-rw-r--r--spec/support/shared_examples/features/variable_list_shared_examples.rb26
-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.rb6
-rw-r--r--spec/support/shared_examples/graphql/mutations/can_mutate_spammable_examples.rb8
-rw-r--r--spec/support/shared_examples/graphql/notes_on_noteables_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/graphql/types/gitlab_style_deprecations_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/helpers/issuable_description_templates_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/lib/gitlab/cycle_analytics/event_shared_examples.rb1
-rw-r--r--spec/support/shared_examples/lib/gitlab/usage_data_counters/issuable_activity_shared_examples.rb (renamed from spec/support/shared_examples/lib/gitlab/usage_data_counters/issue_activity_shared_examples.rb)12
-rw-r--r--spec/support/shared_examples/lib/sentry/client_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/metrics/active_record_subscriber_shared_examples.rb137
-rw-r--r--spec/support/shared_examples/models/application_setting_shared_examples.rb15
-rw-r--r--spec/support/shared_examples/models/boards/user_preferences_shared_examples.rb68
-rw-r--r--spec/support/shared_examples/models/chat_service_shared_examples.rb11
-rw-r--r--spec/support/shared_examples/models/concerns/timebox_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/models/email_format_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/models/slack_mattermost_notifications_shared_examples.rb241
-rw-r--r--spec/support/shared_examples/models/wiki_shared_examples.rb46
-rw-r--r--spec/support/shared_examples/quick_actions/issuable/time_tracking_quick_action_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/requests/api/conan_packages_shared_examples.rb106
-rw-r--r--spec/support/shared_examples/requests/api/graphql/group_and_project_boards_query_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/requests/api/graphql/packages/group_and_project_packages_list_shared_examples.rb51
-rw-r--r--spec/support/shared_examples/requests/api/logging_application_context_shared_examples.rb16
-rw-r--r--spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb593
-rw-r--r--spec/support/shared_examples/requests/api/rubygems_packages_shared_examples.rb194
-rw-r--r--spec/support/shared_examples/service_desk_issue_templates_examples.rb12
-rw-r--r--spec/support/shared_examples/services/boards/update_boards_shared_examples.rb83
-rw-r--r--spec/support/shared_examples/services/packages/maven/metadata_shared_examples.rb57
-rw-r--r--spec/support/snowplow.rb19
-rw-r--r--spec/support/stub_snowplow.rb23
66 files changed, 2149 insertions, 748 deletions
diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb
index db198ac9808..be2b41d6997 100644
--- a/spec/support/capybara.rb
+++ b/spec/support/capybara.rb
@@ -79,8 +79,30 @@ Capybara.register_driver :chrome do |app|
)
end
+Capybara.register_driver :firefox do |app|
+ capabilities = Selenium::WebDriver::Remote::Capabilities.firefox(
+ log: {
+ level: :trace
+ }
+ )
+
+ options = Selenium::WebDriver::Firefox::Options.new(log_level: :trace)
+
+ options.add_argument("--window-size=#{CAPYBARA_WINDOW_SIZE.join(',')}")
+
+ # Run headless by default unless WEBDRIVER_HEADLESS specified
+ options.add_argument("--headless") unless ENV['WEBDRIVER_HEADLESS'] =~ /^(false|no|0)$/i
+
+ Capybara::Selenium::Driver.new(
+ app,
+ browser: :firefox,
+ desired_capabilities: capabilities,
+ options: options
+ )
+end
+
Capybara.server = :puma_via_workhorse
-Capybara.javascript_driver = :chrome
+Capybara.javascript_driver = ENV.fetch('WEBDRIVER', :chrome).to_sym
Capybara.default_max_wait_time = timeout
Capybara.ignore_hidden_elements = true
Capybara.default_normalize_ws = true
diff --git a/spec/support/gitlab_experiment.rb b/spec/support/gitlab_experiment.rb
index 45ae9958c52..bd0c88f8049 100644
--- a/spec/support/gitlab_experiment.rb
+++ b/spec/support/gitlab_experiment.rb
@@ -2,15 +2,31 @@
# Require the provided spec helper and matchers.
require 'gitlab/experiment/rspec'
+require_relative 'stub_snowplow'
# This is a temporary fix until we have a larger discussion around the
# challenges raised in https://gitlab.com/gitlab-org/gitlab/-/issues/300104
-class ApplicationExperiment < Gitlab::Experiment # rubocop:disable Gitlab/NamespacedClass
+require Rails.root.join('app', 'experiments', 'application_experiment')
+class ApplicationExperiment # rubocop:disable Gitlab/NamespacedClass
def initialize(...)
super(...)
Feature.persist_used!(feature_flag_name)
end
+
+ def should_track?
+ true
+ end
end
-# Disable all caching for experiments in tests.
-Gitlab::Experiment::Configuration.cache = nil
+RSpec.configure do |config|
+ config.include StubSnowplow, :experiment
+
+ # Disable all caching for experiments in tests.
+ config.before do
+ allow(Gitlab::Experiment::Configuration).to receive(:cache).and_return(nil)
+ end
+
+ config.before(:each, :experiment) do
+ stub_snowplow
+ end
+end
diff --git a/spec/support/graphql/resolver_factories.rb b/spec/support/graphql/resolver_factories.rb
new file mode 100644
index 00000000000..8188f17cc43
--- /dev/null
+++ b/spec/support/graphql/resolver_factories.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+module Graphql
+ module ResolverFactories
+ def new_resolver(resolved_value = 'Resolved value', method: :resolve)
+ case method
+ when :resolve
+ simple_resolver(resolved_value)
+ when :find_object
+ find_object_resolver(resolved_value)
+ else
+ raise "Cannot build a resolver for #{method}"
+ end
+ end
+
+ private
+
+ def simple_resolver(resolved_value = 'Resolved value')
+ Class.new(Resolvers::BaseResolver) do
+ define_method :resolve do |**_args|
+ resolved_value
+ end
+ end
+ end
+
+ def find_object_resolver(resolved_value = 'Found object')
+ Class.new(Resolvers::BaseResolver) do
+ include ::Gitlab::Graphql::Authorize::AuthorizeResource
+
+ def resolve(**args)
+ authorized_find!(**args)
+ end
+
+ define_method :find_object do |**_args|
+ resolved_value
+ end
+ end
+ end
+ end
+end
diff --git a/spec/support/helpers/cycle_analytics_helpers.rb b/spec/support/helpers/cycle_analytics_helpers.rb
index a90cbbf3bd3..14041ad0ac6 100644
--- a/spec/support/helpers/cycle_analytics_helpers.rb
+++ b/spec/support/helpers/cycle_analytics_helpers.rb
@@ -15,7 +15,7 @@ module CycleAnalyticsHelpers
end
def toggle_dropdown(field)
- page.within("[data-testid='#{field}']") do
+ page.within("[data-testid*='#{field}']") do
find('.dropdown-toggle').click
wait_for_requests
@@ -26,7 +26,7 @@ module CycleAnalyticsHelpers
def select_dropdown_option_by_value(name, value, elem = '.dropdown-item')
toggle_dropdown name
- page.find("[data-testid='#{name}'] .dropdown-menu").find("#{elem}[value='#{value}']").click
+ page.find("[data-testid*='#{name}'] .dropdown-menu").find("#{elem}[value='#{value}']").click
end
def create_commit_referencing_issue(issue, branch_name: generate(:branch))
diff --git a/spec/support/helpers/database/database_helpers.rb b/spec/support/helpers/database/database_helpers.rb
index b8d7ea3662f..db093bcef85 100644
--- a/spec/support/helpers/database/database_helpers.rb
+++ b/spec/support/helpers/database/database_helpers.rb
@@ -5,11 +5,65 @@ module Database
# 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)
+ ActiveRecord::Base.connection.execute(<<~SQL.squish)
CREATE TABLE #{view}_copy (LIKE #{view});
DROP VIEW #{view};
ALTER TABLE #{view}_copy RENAME TO #{view};
SQL
end
+
+ # Set statement timeout temporarily.
+ # Useful when testing query timeouts.
+ #
+ # Note that this method cannot restore the timeout if a query
+ # was canceled due to e.g. a statement timeout.
+ # Refrain from using this transaction in these situations.
+ #
+ # @param timeout - Statement timeout in seconds
+ #
+ # Example:
+ #
+ # with_statement_timeout(0.1) do
+ # model.select('pg_sleep(0.11)')
+ # end
+ def with_statement_timeout(timeout)
+ # Force a positive value and a minimum of 1ms for very small values.
+ timeout = (timeout * 1000).abs.ceil
+
+ raise ArgumentError, 'Using a timeout of `0` means to disable statement timeout.' if timeout == 0
+
+ previous_timeout = ActiveRecord::Base.connection
+ .exec_query('SHOW statement_timeout')[0].fetch('statement_timeout')
+
+ set_statement_timeout("#{timeout}ms")
+
+ yield
+ ensure
+ begin
+ set_statement_timeout(previous_timeout)
+ rescue ActiveRecord::StatementInvalid
+ # After a transaction was canceled/aborted due to e.g. a statement
+ # timeout commands are ignored and will raise in PG::InFailedSqlTransaction.
+ # We can safely ignore this error because the statement timeout was set
+ # for the currrent transaction which will be closed anyway.
+ end
+ end
+
+ # Set statement timeout for the current transaction.
+ #
+ # Note, that it does not restore the previous statement timeout.
+ # Use `with_statement_timeout` instead.
+ #
+ # @param timeout - Statement timeout in seconds
+ #
+ # Example:
+ #
+ # set_statement_timeout(0.1)
+ # model.select('pg_sleep(0.11)')
+ def set_statement_timeout(timeout)
+ ActiveRecord::Base.connection.execute(
+ format(%(SET LOCAL statement_timeout = '%s'), timeout)
+ )
+ end
end
end
diff --git a/spec/support/helpers/dependency_proxy_helpers.rb b/spec/support/helpers/dependency_proxy_helpers.rb
index ebb849628bf..0d8f56906e3 100644
--- a/spec/support/helpers/dependency_proxy_helpers.rb
+++ b/spec/support/helpers/dependency_proxy_helpers.rb
@@ -18,11 +18,11 @@ module DependencyProxyHelpers
.to_return(status: status, body: body || manifest, headers: headers)
end
- def stub_manifest_head(image, tag, status: 200, body: nil, digest: '123456')
+ def stub_manifest_head(image, tag, status: 200, body: nil, headers: {})
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 } )
+ .to_return(status: status, body: body, headers: headers )
end
def stub_blob_download(image, blob_sha, status = 200, body = '123456')
diff --git a/spec/support/helpers/design_management_test_helpers.rb b/spec/support/helpers/design_management_test_helpers.rb
index db217250b17..be723a47521 100644
--- a/spec/support/helpers/design_management_test_helpers.rb
+++ b/spec/support/helpers/design_management_test_helpers.rb
@@ -35,7 +35,7 @@ module DesignManagementTestHelpers
def act_on_designs(designs, &block)
issue = designs.first.issue
- version = build(:design_version, :empty, issue: issue).tap { |v| v.save!(validate: false) }
+ version = build(:design_version, designs_count: 0, issue: issue).tap { |v| v.save!(validate: false) }
designs.each do |d|
yield.create!(design: d, version: version)
end
diff --git a/spec/support/helpers/features/releases_helpers.rb b/spec/support/helpers/features/releases_helpers.rb
index 44087f71cfa..9cce9c4882d 100644
--- a/spec/support/helpers/features/releases_helpers.rb
+++ b/spec/support/helpers/features/releases_helpers.rb
@@ -1,9 +1,6 @@
# frozen_string_literal: true
-# These helpers fill fields on the "New Release" and
-# "Edit Release" pages. They use the keyboard to navigate
-# from one field to the next and assume that when
-# they are called, the field to be filled out is already focused.
+# These helpers fill fields on the "New Release" and "Edit Release" pages.
#
# Usage:
# describe "..." do
@@ -18,97 +15,65 @@ module Spec
module Helpers
module Features
module ReleasesHelpers
- # Returns the element that currently has keyboard focus.
- # Reminder that this returns a Selenium::WebDriver::Element
- # _not_ a Capybara::Node::Element
- def focused_element
- page.driver.browser.switch_to.active_element
- end
-
- def fill_tag_name(tag_name, and_tab: true)
- expect(focused_element).to eq(find_field('Tag name').native)
+ def select_new_tag_name(tag_name)
+ page.within '[data-testid="tag-name-field"]' do
+ find('button').click
- focused_element.send_keys(tag_name)
+ wait_for_all_requests
- focused_element.send_keys(:tab) if and_tab
- end
+ find('input[aria-label="Search or create tag"]').set(tag_name)
- def select_create_from(branch_name, and_tab: true)
- expect(focused_element).to eq(find('[data-testid="create-from-field"] button').native)
+ wait_for_all_requests
- focused_element.send_keys(:enter)
+ click_button("Create tag #{tag_name}")
+ end
+ end
- # Wait for the dropdown to be rendered
- page.find('.ref-selector .dropdown-menu')
+ def select_create_from(branch_name)
+ page.within '[data-testid="create-from-field"]' do
+ find('button').click
- # Pressing Enter in the search box shouldn't submit the form
- focused_element.send_keys(branch_name, :enter)
+ wait_for_all_requests
- # Wait for the search to return
- page.find('.ref-selector .dropdown-item', text: branch_name, match: :first)
+ find('input[aria-label="Search branches, tags, and commits"]').set(branch_name)
- focused_element.send_keys(:arrow_down, :enter)
+ wait_for_all_requests
- focused_element.send_keys(:tab) if and_tab
+ click_button("#{branch_name}")
+ end
end
- def fill_release_title(release_title, and_tab: true)
- expect(focused_element).to eq(find_field('Release title').native)
-
- focused_element.send_keys(release_title)
-
- focused_element.send_keys(:tab) if and_tab
+ def fill_release_title(release_title)
+ fill_in('Release title', with: release_title)
end
- def select_milestone(milestone_title, and_tab: true)
- expect(focused_element).to eq(find('[data-testid="milestones-field"] button').native)
-
- focused_element.send_keys(:enter)
+ def select_milestone(milestone_title)
+ page.within '[data-testid="milestones-field"]' do
+ find('button').click
- # Wait for the dropdown to be rendered
- page.find('.milestone-combobox .dropdown-menu')
+ wait_for_all_requests
- # Clear any existing input
- focused_element.attribute('value').length.times { focused_element.send_keys(:backspace) }
+ find('input[aria-label="Search Milestones"]').set(milestone_title)
- # Pressing Enter in the search box shouldn't submit the form
- focused_element.send_keys(milestone_title, :enter)
+ wait_for_all_requests
- # Wait for the search to return
- page.find('.milestone-combobox .dropdown-item', text: milestone_title, match: :first)
-
- focused_element.send_keys(:arrow_down, :arrow_down, :enter)
-
- focused_element.send_keys(:tab) if and_tab
+ find('button', text: milestone_title, match: :first).click
+ end
end
- def fill_release_notes(release_notes, and_tab: true)
- expect(focused_element).to eq(find_field('Release notes').native)
-
- focused_element.send_keys(release_notes)
-
- # Tab past the links at the bottom of the editor
- focused_element.send_keys(:tab, :tab, :tab) if and_tab
+ def fill_release_notes(release_notes)
+ fill_in('Release notes', with: release_notes)
end
- def fill_asset_link(link, and_tab: true)
- expect(focused_element['id']).to start_with('asset-url-')
-
- focused_element.send_keys(link[:url], :tab, link[:title], :tab, link[:type])
-
- # Tab past the "Remove asset link" button
- focused_element.send_keys(:tab, :tab) if and_tab
+ def fill_asset_link(link)
+ all('input[name="asset-url"]').last.set(link[:url])
+ all('input[name="asset-link-name"]').last.set(link[:title])
+ all('select[name="asset-type"]').last.find("option[value=\"#{link[:type]}\"").select_option
end
# Click "Add another link" and tab back to the beginning of the new row
def add_another_asset_link
- expect(focused_element).to eq(find_button('Add another link').native)
-
- focused_element.send_keys(:enter,
- [:shift, :tab],
- [:shift, :tab],
- [:shift, :tab],
- [:shift, :tab])
+ click_button('Add another link')
end
end
end
diff --git a/spec/support/helpers/gpg_helpers.rb b/spec/support/helpers/gpg_helpers.rb
index 389e5818dbe..813c6176317 100644
--- a/spec/support/helpers/gpg_helpers.rb
+++ b/spec/support/helpers/gpg_helpers.rb
@@ -279,6 +279,10 @@ module GpgHelpers
KEY
end
+ def primary_keyid2
+ fingerprint2[-16..-1]
+ end
+
def fingerprint2
'C447A6F6BFD9CEF8FB371785571625A930241179'
end
diff --git a/spec/support/helpers/graphql_helpers.rb b/spec/support/helpers/graphql_helpers.rb
index 46d0c13dc18..75d9508f470 100644
--- a/spec/support/helpers/graphql_helpers.rb
+++ b/spec/support/helpers/graphql_helpers.rb
@@ -16,32 +16,130 @@ module GraphqlHelpers
underscored_field_name.to_s.camelize(:lower)
end
- # Run a loader's named resolver in a way that closely mimics the framework.
+ def self.deep_fieldnamerize(map)
+ map.to_h do |k, v|
+ [fieldnamerize(k), v.is_a?(Hash) ? deep_fieldnamerize(v) : v]
+ end
+ end
+
+ # Run this resolver exactly as it would be called in the framework. This
+ # includes all authorization hooks, all argument processing and all result
+ # wrapping.
+ # see: GraphqlHelpers#resolve_field
+ def resolve(
+ resolver_class, # [Class[<= BaseResolver]] The resolver at test.
+ obj: nil, # [Any] The BaseObject#object for the resolver (available as `#object` in the resolver).
+ args: {}, # [Hash] The arguments to the resolver (using client names).
+ ctx: {}, # [#to_h] The current context values.
+ schema: GitlabSchema, # [GraphQL::Schema] Schema to use during execution.
+ parent: :not_given, # A GraphQL query node to be passed as the `:parent` extra.
+ lookahead: :not_given # A GraphQL lookahead object to be passed as the `:lookahead` extra.
+ )
+ # All resolution goes through fields, so we need to create one here that
+ # uses our resolver. Thankfully, apart from the field name, resolvers
+ # contain all the configuration needed to define one.
+ field_options = resolver_class.field_options.merge(
+ owner: resolver_parent,
+ name: 'field_value'
+ )
+ field = ::Types::BaseField.new(**field_options)
+
+ # All mutations accept a single `:input` argument. Wrap arguments here.
+ # See the unwrapping below in GraphqlHelpers#resolve_field
+ args = { input: args } if resolver_class <= ::Mutations::BaseMutation && !args.key?(:input)
+
+ resolve_field(field, obj,
+ args: args,
+ ctx: ctx,
+ schema: schema,
+ object_type: resolver_parent,
+ extras: { parent: parent, lookahead: lookahead })
+ end
+
+ # Resolve the value of a field on an object.
+ #
+ # Use this method to test individual fields within type specs.
+ #
+ # e.g.
+ #
+ # issue = create(:issue)
+ # user = issue.author
+ # project = issue.project
+ #
+ # resolve_field(:author, issue, current_user: user, object_type: ::Types::IssueType)
+ # resolve_field(:issue, project, args: { iid: issue.iid }, current_user: user, object_type: ::Types::ProjectType)
+ #
+ # The `object_type` defaults to the `described_class`, so when called from type specs,
+ # the above can be written as:
#
- # First the `ready?` method is called. If it turns out that the resolver is not
- # ready, then the early return is returned instead.
+ # # In project_type_spec.rb
+ # resolve_field(:author, issue, current_user: user)
#
- # Then the resolve method is called.
- 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) }
+ # # In issue_type_spec.rb
+ # resolve_field(:issue, project, args: { iid: issue.iid }, current_user: user)
+ #
+ # NB: Arguments are passed from the client's perspective. If there is an argument
+ # `foo` aliased as `bar`, then we would pass `args: { bar: the_value }`, and
+ # types are checked before resolution.
+ def resolve_field(
+ field, # An instance of `BaseField`, or the name of a field on the current described_class
+ object, # The current object of the `BaseObject` this field 'belongs' to
+ args: {}, # Field arguments (keys will be fieldnamerized)
+ ctx: {}, # Context values (important ones are :current_user)
+ extras: {}, # Stub values for field extras (parent and lookahead)
+ current_user: :not_given, # The current user (specified explicitly, overrides ctx[:current_user])
+ schema: GitlabSchema, # A specific schema instance
+ object_type: described_class # The `BaseObject` type this field belongs to
+ )
+ field = to_base_field(field, object_type)
+ ctx[:current_user] = current_user unless current_user == :not_given
+ query = GraphQL::Query.new(schema, context: ctx.to_h)
+ extras[:lookahead] = negative_lookahead if extras[:lookahead] == :not_given && field.extras.include?(:lookahead)
+
+ query_ctx = query.context
+
+ mock_extras(query_ctx, **extras)
+
+ parent = object_type.authorized_new(object, query_ctx)
+ raise UnauthorizedObject unless parent
+
+ # TODO: This will need to change when we move to the interpreter:
+ # At that point, arguments will be a plain ruby hash rather than
+ # an Arguments object
+ # see: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/27536
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/210556
+ arguments = field.to_graphql.arguments_class.new(
+ GraphqlHelpers.deep_fieldnamerize(args),
+ context: query_ctx,
+ defaults_used: []
+ )
+
+ # we enable the request store so we can track gitaly calls.
+ ::Gitlab::WithRequestStore.with_request_store do
+ # TODO: This will need to change when we move to the interpreter - at that
+ # point we will call `field#resolve`
+
+ # Unwrap the arguments to mutations. This pairs with the wrapping in GraphqlHelpers#resolve
+ # If arguments are not wrapped first, then arguments processing will raise.
+ # If arguments are not unwrapped here, then the resolve method of the mutation will raise argument errors.
+ arguments = arguments.to_kwargs[:input] if field.resolver && field.resolver <= ::Mutations::BaseMutation
- return early_return unless ready
+ field.resolve_field(parent, arguments, query_ctx)
+ end
+ end
- resolver.resolve(**args)
+ def mock_extras(context, parent: :not_given, lookahead: :not_given)
+ allow(context).to receive(:parent).and_return(parent) unless parent == :not_given
+ allow(context).to receive(:lookahead).and_return(lookahead) unless lookahead == :not_given
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
+ # a synthetic BaseObject type to be used in resolver specs. See `GraphqlHelpers#resolve`
+ def resolver_parent
+ @resolver_parent ||= fresh_object_type('ResolverParent')
+ end
- args.transform_keys do |k|
- definitions[GraphqlHelpers.fieldnamerize(k)]&.keyword || k
- end
+ def fresh_object_type(name = 'Object')
+ Class.new(::Types::BaseObject) { graphql_name name }
end
def resolver_instance(resolver_class, obj: nil, ctx: {}, field: nil, schema: GitlabSchema)
@@ -124,9 +222,9 @@ module GraphqlHelpers
lazy_vals.is_a?(Array) ? lazy_vals.map { |val| sync(val) } : sync(lazy_vals)
end
- def graphql_query_for(name, attributes = {}, fields = nil)
+ def graphql_query_for(name, args = {}, selection = nil)
type = GitlabSchema.types['Query'].fields[GraphqlHelpers.fieldnamerize(name)]&.type
- wrap_query(query_graphql_field(name, attributes, fields, type))
+ wrap_query(query_graphql_field(name, args, selection, type))
end
def wrap_query(query)
@@ -171,25 +269,6 @@ module GraphqlHelpers
::Gitlab::Utils::MergeHash.merge(Array.wrap(variables).map(&:to_h)).to_json
end
- 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)
- 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
#
# prepare_input_for_mutation({ 'my_key' => 1 })
@@ -558,24 +637,26 @@ module GraphqlHelpers
end
end
- def execute_query(query_type)
- schema = Class.new(GraphQL::Schema) do
- use GraphQL::Pagination::Connections
- use Gitlab::Graphql::Authorize
- use Gitlab::Graphql::Pagination::Connections
-
- lazy_resolve ::Gitlab::Graphql::Lazy, :force
-
- query(query_type)
- end
+ # assumes query_string to be let-bound in the current context
+ def execute_query(query_type, schema: empty_schema, graphql: query_string)
+ schema.query(query_type)
schema.execute(
- query_string,
+ graphql,
context: { current_user: user },
variables: {}
)
end
+ def empty_schema
+ Class.new(GraphQL::Schema) do
+ use GraphQL::Pagination::Connections
+ use Gitlab::Graphql::Pagination::Connections
+
+ lazy_resolve ::Gitlab::Graphql::Lazy, :force
+ end
+ end
+
# A lookahead that selects everything
def positive_lookahead
double(selects?: true).tap do |selection|
@@ -589,6 +670,23 @@ module GraphqlHelpers
allow(selection).to receive(:selection).and_return(selection)
end
end
+
+ private
+
+ def to_base_field(name_or_field, object_type)
+ case name_or_field
+ when ::Types::BaseField
+ name_or_field
+ else
+ field_by_name(name_or_field, object_type)
+ end
+ end
+
+ def field_by_name(name, object_type)
+ name = ::GraphqlHelpers.fieldnamerize(name)
+
+ object_type.fields[name] || (raise ArgumentError, "Unknown field #{name} for #{described_class.graphql_name}")
+ end
end
# This warms our schema, doing this as part of loading the helpers to avoid
diff --git a/spec/support/helpers/javascript_fixtures_helpers.rb b/spec/support/helpers/javascript_fixtures_helpers.rb
index 2224af88ab9..09425c3742a 100644
--- a/spec/support/helpers/javascript_fixtures_helpers.rb
+++ b/spec/support/helpers/javascript_fixtures_helpers.rb
@@ -12,6 +12,7 @@ module JavaScriptFixturesHelpers
included do |base|
base.around do |example|
# pick an arbitrary date from the past, so tests are not time dependent
+ # Also see spec/frontend/__helpers__/fake_date/jest.js
Timecop.freeze(Time.utc(2015, 7, 3, 10)) { example.run }
raise NoMethodError.new('You need to set `response` for the fixture generator! This will automatically happen with `type: :controller` or `type: :request`.', 'response') unless respond_to?(:response)
diff --git a/spec/support/helpers/notification_helpers.rb b/spec/support/helpers/notification_helpers.rb
index aee57b452fe..6066f4ec3bf 100644
--- a/spec/support/helpers/notification_helpers.rb
+++ b/spec/support/helpers/notification_helpers.rb
@@ -3,10 +3,10 @@
module NotificationHelpers
extend self
- def send_notifications(*new_mentions)
+ def send_notifications(*new_mentions, current_user: @u_disabled)
mentionable.description = new_mentions.map(&:to_reference).join(' ')
- notification.send(notification_method, mentionable, new_mentions, @u_disabled)
+ notification.send(notification_method, mentionable, new_mentions, current_user)
end
def create_global_setting_for(user, level)
diff --git a/spec/support/helpers/stub_object_storage.rb b/spec/support/helpers/stub_object_storage.rb
index 0d0ac171baa..56177d445d6 100644
--- a/spec/support/helpers/stub_object_storage.rb
+++ b/spec/support/helpers/stub_object_storage.rb
@@ -114,7 +114,7 @@ module StubObjectStorage
end
def stub_object_storage_multipart_init(endpoint, upload_id = "upload_id")
- stub_request(:post, %r{\A#{endpoint}tmp/uploads/[a-z0-9-]*\?uploads\z})
+ stub_request(:post, %r{\A#{endpoint}tmp/uploads/[%A-Za-z0-9-]*\?uploads\z})
.to_return status: 200, body: <<-EOS.strip_heredoc
<?xml version="1.0" encoding="UTF-8"?>
<InitiateMultipartUploadResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
diff --git a/spec/support/helpers/test_env.rb b/spec/support/helpers/test_env.rb
index 2d71662b0eb..266c0e18ccd 100644
--- a/spec/support/helpers/test_env.rb
+++ b/spec/support/helpers/test_env.rb
@@ -77,7 +77,8 @@ module TestEnv
'sha-starting-with-large-number' => '8426165',
'invalid-utf8-diff-paths' => '99e4853',
'compare-with-merge-head-source' => 'f20a03d',
- 'compare-with-merge-head-target' => '2f1e176'
+ 'compare-with-merge-head-target' => '2f1e176',
+ 'trailers' => 'f0a5ed6'
}.freeze
# gitlab-test-fork is a fork of gitlab-fork, but we don't necessarily
@@ -172,8 +173,13 @@ module TestEnv
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" }
+ { 'default' => repos_path },
+ force: true,
+ options: {
+ internal_socket_dir: File.join(gitaly_dir, "internal_gitaly2"),
+ gitaly_socket: "gitaly2.socket",
+ config_filename: "gitaly2.config.toml"
+ }
)
Gitlab::SetupHelper::Praefect.create_configuration(gitaly_dir, { 'praefect' => repos_path }, force: true)
end
@@ -186,7 +192,17 @@ module TestEnv
end
def gitaly_dir
- File.dirname(gitaly_socket_path)
+ socket_path = gitaly_socket_path
+ socket_path = File.expand_path(gitaly_socket_path) if expand_path?
+
+ File.dirname(socket_path)
+ end
+
+ # Linux fails with "bind: invalid argument" if a UNIX socket path exceeds 108 characters:
+ # https://github.com/golang/go/issues/6895. We use absolute paths in CI to ensure
+ # that changes in the current working directory don't affect GRPC reconnections.
+ def expand_path?
+ !!ENV['CI']
end
def start_gitaly(gitaly_dir)
diff --git a/spec/support/matchers/background_migrations_matchers.rb b/spec/support/matchers/background_migrations_matchers.rb
index 0144a044f6c..08bbbcc7438 100644
--- a/spec/support/matchers/background_migrations_matchers.rb
+++ b/spec/support/matchers/background_migrations_matchers.rb
@@ -1,7 +1,17 @@
# frozen_string_literal: true
+RSpec::Matchers.define :be_background_migration_with_arguments do |arguments|
+ define_method :matches? do |migration|
+ expect do
+ Gitlab::BackgroundMigration.perform(migration, arguments)
+ end.not_to raise_error
+ end
+end
+
RSpec::Matchers.define :be_scheduled_delayed_migration do |delay, *expected|
- match do |migration|
+ define_method :matches? do |migration|
+ expect(migration).to be_background_migration_with_arguments(expected)
+
BackgroundMigrationWorker.jobs.any? do |job|
job['args'] == [migration, expected] &&
job['at'].to_i == (delay.to_i + Time.now.to_i)
@@ -16,7 +26,9 @@ RSpec::Matchers.define :be_scheduled_delayed_migration do |delay, *expected|
end
RSpec::Matchers.define :be_scheduled_migration do |*expected|
- match do |migration|
+ define_method :matches? do |migration|
+ expect(migration).to be_background_migration_with_arguments(expected)
+
BackgroundMigrationWorker.jobs.any? do |job|
args = job['args'].size == 1 ? [BackgroundMigrationWorker.jobs[0]['args'][0], []] : job['args']
args == [migration, expected]
@@ -29,7 +41,9 @@ RSpec::Matchers.define :be_scheduled_migration do |*expected|
end
RSpec::Matchers.define :be_scheduled_migration_with_multiple_args do |*expected|
- match do |migration|
+ define_method :matches? do |migration|
+ expect(migration).to be_background_migration_with_arguments(expected)
+
BackgroundMigrationWorker.jobs.any? do |job|
args = job['args'].size == 1 ? [BackgroundMigrationWorker.jobs[0]['args'][0], []] : job['args']
args[0] == migration && compare_args(args, expected)
diff --git a/spec/support/matchers/email_matcher.rb b/spec/support/matchers/email_matcher.rb
new file mode 100644
index 00000000000..36cf3e0e871
--- /dev/null
+++ b/spec/support/matchers/email_matcher.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+RSpec::Matchers.define :have_text_part_content do |expected|
+ match do |actual|
+ @actual = actual.text_part.body.to_s
+ expect(@actual).to include(expected)
+ end
+
+ diffable
+end
+
+RSpec::Matchers.define :have_html_part_content do |expected|
+ match do |actual|
+ @actual = actual.html_part.body.to_s
+ expect(@actual).to include(expected)
+ end
+
+ diffable
+end
diff --git a/spec/support/matchers/graphql_matchers.rb b/spec/support/matchers/graphql_matchers.rb
index 8c4ba387a74..565c21e0f85 100644
--- a/spec/support/matchers/graphql_matchers.rb
+++ b/spec/support/matchers/graphql_matchers.rb
@@ -1,5 +1,7 @@
# frozen_string_literal: true
+RSpec::Matchers.define_negated_matcher :be_nullable, :be_non_null
+
RSpec::Matchers.define :require_graphql_authorizations do |*expected|
match do |klass|
permissions = if klass.respond_to?(:required_permissions)
@@ -90,7 +92,7 @@ RSpec::Matchers.define :have_graphql_arguments do |*expected|
@names ||= Array.wrap(expected).map { |name| GraphqlHelpers.fieldnamerize(name) }
if field.type.try(:ancestors)&.include?(GraphQL::Types::Relay::BaseConnection)
- @names | %w(after before first last)
+ @names | %w[after before first last]
else
@names
end
@@ -103,9 +105,10 @@ RSpec::Matchers.define :have_graphql_arguments do |*expected|
end
failure_message do |field|
- names = expected_names(field)
+ names = expected_names(field).inspect
+ args = field.arguments.keys.inspect
- "expected that #{field.name} would have the following fields: #{names.inspect}, but it has #{field.arguments.keys.inspect}."
+ "expected that #{field.name} would have the following arguments: #{names}, but it has #{args}."
end
end
diff --git a/spec/support/services/issues/move_and_clone_services_shared_examples.rb b/spec/support/services/issues/move_and_clone_services_shared_examples.rb
new file mode 100644
index 00000000000..2b2e90c0461
--- /dev/null
+++ b/spec/support/services/issues/move_and_clone_services_shared_examples.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'copy or reset relative position' do
+ before do
+ # ensure we have a relative position and it is known
+ old_issue.update!(relative_position: 1000)
+ end
+
+ context 'when moved to a project within same group hierarchy' do
+ it 'does not reset the relative_position' do
+ expect(subject.relative_position).to eq(1000)
+ end
+ end
+
+ context 'when moved to a project in a different group hierarchy' do
+ let_it_be(:new_project) { create(:project, group: create(:group)) }
+
+ it 'does reset the relative_position' do
+ expect(subject.relative_position).to be_nil
+ end
+ end
+end
diff --git a/spec/support/services/service_response_shared_examples.rb b/spec/support/services/service_response_shared_examples.rb
new file mode 100644
index 00000000000..186627347fb
--- /dev/null
+++ b/spec/support/services/service_response_shared_examples.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'returning an error service response' do |message: nil|
+ it 'returns an error service response' do
+ result = subject
+
+ expect(result).to be_error
+
+ if message
+ expect(result.message).to eq(message)
+ end
+ end
+end
+
+RSpec.shared_examples 'returning a success service response' do |message: nil|
+ it 'returns a success service response' do
+ result = subject
+
+ expect(result).to be_success
+
+ if message
+ expect(result.message).to eq(message)
+ end
+ end
+end
diff --git a/spec/support/shared_contexts/features/error_tracking_shared_context.rb b/spec/support/shared_contexts/features/error_tracking_shared_context.rb
index 1f4eb3a6df9..f04111e0ce0 100644
--- a/spec/support/shared_contexts/features/error_tracking_shared_context.rb
+++ b/spec/support/shared_contexts/features/error_tracking_shared_context.rb
@@ -9,7 +9,7 @@ RSpec.shared_context 'sentry error tracking context feature' do
let_it_be(:issue_response) { Gitlab::Json.parse(issue_response_body) }
let_it_be(:event_response_body) { fixture_file('sentry/issue_latest_event_sample_response.json') }
let_it_be(:event_response) { Gitlab::Json.parse(event_response_body) }
- let(:sentry_api_urls) { Sentry::ApiUrls.new(project_error_tracking_settings.api_url) }
+ let(:sentry_api_urls) { ErrorTracking::SentryClient::ApiUrls.new(project_error_tracking_settings.api_url) }
let(:issue_id) { issue_response['id'] }
let(:issue_seen) { 1.year.ago.utc }
let(:formatted_issue_seen) { issue_seen.strftime("%Y-%m-%d %-l:%M:%S%p %Z") }
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 0fee170a35d..debcd9a3054 100644
--- a/spec/support/shared_contexts/issuable/merge_request_shared_context.rb
+++ b/spec/support/shared_contexts/issuable/merge_request_shared_context.rb
@@ -1,63 +1,23 @@
# frozen_string_literal: true
-RSpec.shared_context 'open merge request show action' do
+RSpec.shared_context '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) }
- let(:forked_project) { fork_project(project, user, repository: true) }
- let(:unlink_project) { Projects::UnlinkForkService.new(forked_project, user) }
- let(:note) { create(:note_on_merge_request, project: project, noteable: closed_merge_request) }
-
- let(:closed_merge_request) do
- create(:closed_merge_request,
- source_project: forked_project,
- target_project: project,
- author: user)
- end
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project, :public, :repository) }
+ let_it_be(:merge_request) { create(:merge_request, :opened, source_project: project, author: user) }
+ let_it_be(:note) { create(:note_on_merge_request, project: project, noteable: merge_request) }
before do
+ allow(view).to receive(:experiment_enabled?).and_return(false)
+ allow(view).to receive(:current_user).and_return(user)
assign(:project, project)
- assign(:merge_request, closed_merge_request)
- assign(:commits_count, 0)
+ assign(:merge_request, merge_request)
assign(:note, note)
- assign(:noteable, closed_merge_request)
- assign(:notes, [])
- assign(:pipelines, Ci::Pipeline.none)
- assign(:issuable_sidebar, serialize_issuable_sidebar(user, project, closed_merge_request))
-
- preload_view_requirements(closed_merge_request, note)
+ assign(:noteable, merge_request)
+ assign(:pipelines, [])
+ assign(:issuable_sidebar, serialize_issuable_sidebar(user, project, merge_request))
- allow(view).to receive_messages(current_user: user,
- can?: true,
- current_application_settings: Gitlab::CurrentSettings.current_application_settings)
+ preload_view_requirements(merge_request, note)
end
end
diff --git a/spec/support/shared_contexts/navbar_structure_context.rb b/spec/support/shared_contexts/navbar_structure_context.rb
index 3fd4f2698e9..671c0cdf79c 100644
--- a/spec/support/shared_contexts/navbar_structure_context.rb
+++ b/spec/support/shared_contexts/navbar_structure_context.rb
@@ -5,7 +5,7 @@ RSpec.shared_context 'project navbar structure' do
{
nav_item: _('Analytics'),
nav_sub_items: [
- _('CI / CD'),
+ _('CI/CD'),
(_('Code Review') if Gitlab.ee?),
(_('Merge Request') if Gitlab.ee?),
_('Repository'),
@@ -63,7 +63,7 @@ RSpec.shared_context 'project navbar structure' do
nav_sub_items: []
},
{
- nav_item: _('CI / CD'),
+ nav_item: _('CI/CD'),
nav_sub_items: [
_('Pipelines'),
s_('Pipelines|Editor'),
@@ -111,7 +111,7 @@ RSpec.shared_context 'project navbar structure' do
_('Webhooks'),
_('Access Tokens'),
_('Repository'),
- _('CI / CD'),
+ _('CI/CD'),
_('Operations')
].compact
}
@@ -124,7 +124,8 @@ RSpec.shared_context 'group navbar structure' do
{
nav_item: _('Analytics'),
nav_sub_items: [
- _('Contribution')
+ _('Contribution'),
+ _('DevOps Adoption')
]
}
end
@@ -137,7 +138,7 @@ RSpec.shared_context 'group navbar structure' do
_('Integrations'),
_('Projects'),
_('Repository'),
- _('CI / CD'),
+ _('CI/CD'),
_('Packages & Registries'),
_('Webhooks')
]
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 e7bc1450601..b0d7274269b 100644
--- a/spec/support/shared_contexts/policies/group_policy_shared_context.rb
+++ b/spec/support/shared_contexts/policies/group_policy_shared_context.rb
@@ -18,12 +18,12 @@ RSpec.shared_context 'GroupPolicy context' do
]
end
- let(:read_group_permissions) { %i[read_label read_list read_milestone read_board] }
+ let(:read_group_permissions) { %i[read_label read_issue_board_list read_milestone read_issue_board] }
let(:reporter_permissions) do
%i[
admin_label
- admin_board
+ admin_issue_board
read_container_image
read_metrics_dashboard_annotation
read_prometheus
diff --git a/spec/support/shared_contexts/policies/project_policy_shared_context.rb b/spec/support/shared_contexts/policies/project_policy_shared_context.rb
index 3016494ac8d..266c8d5ee84 100644
--- a/spec/support/shared_contexts/policies/project_policy_shared_context.rb
+++ b/spec/support/shared_contexts/policies/project_policy_shared_context.rb
@@ -16,8 +16,8 @@ RSpec.shared_context 'ProjectPolicy context' do
let(:base_guest_permissions) do
%i[
award_emoji create_issue create_merge_request_in create_note
- create_project read_board read_issue read_issue_iid read_issue_link
- read_label read_list read_milestone read_note read_project
+ create_project read_issue_board read_issue read_issue_iid read_issue_link
+ read_label read_issue_board_list read_milestone read_note read_project
read_project_for_iids read_project_member read_release read_snippet
read_wiki upload_file
]
@@ -25,7 +25,7 @@ RSpec.shared_context 'ProjectPolicy context' do
let(:base_reporter_permissions) do
%i[
- admin_issue admin_issue_link admin_label admin_list create_snippet
+ admin_issue admin_issue_link admin_label admin_issue_board_list create_snippet
download_code download_wiki_code fork_project metrics_dashboard
read_build read_commit_status read_confidential_issues
read_container_image read_deployment read_environment read_merge_request
diff --git a/spec/support/shared_contexts/policies/project_policy_table_shared_context.rb b/spec/support/shared_contexts/policies/project_policy_table_shared_context.rb
index 8c9a60fa703..fbd82fbbe31 100644
--- a/spec/support/shared_contexts/policies/project_policy_table_shared_context.rb
+++ b/spec/support/shared_contexts/policies/project_policy_table_shared_context.rb
@@ -356,7 +356,7 @@ RSpec.shared_context 'ProjectPolicyTable context' do
:private | :anonymous | 0
end
- # :snippet_level, :project_level, :feature_access_level, :membership, :expected_count
+ # :snippet_level, :project_level, :feature_access_level, :membership, :admin_mode, :expected_count
def permission_table_for_project_snippet_access
:public | :public | :enabled | :admin | true | 1
:public | :public | :enabled | :admin | false | 1
diff --git a/spec/support/shared_contexts/requests/api/npm_packages_shared_context.rb b/spec/support/shared_contexts/requests/api/npm_packages_shared_context.rb
index 60a29d78084..815108be447 100644
--- a/spec/support/shared_contexts/requests/api/npm_packages_shared_context.rb
+++ b/spec/support/shared_contexts/requests/api/npm_packages_shared_context.rb
@@ -5,8 +5,9 @@ RSpec.shared_context 'npm api setup' do
include HttpBasicAuthHelpers
let_it_be(:user, reload: true) { create(:user) }
- let_it_be(:group) { create(:group) }
- let_it_be(:project, reload: true) { create(:project, :public, namespace: group) }
+ let_it_be(:group) { create(:group, name: 'test-group') }
+ let_it_be(:namespace) { group }
+ let_it_be(:project, reload: true) { create(:project, :public, namespace: namespace) }
let_it_be(:package, reload: true) { create(:npm_package, project: project, name: "@#{group.path}/scoped_package") }
let_it_be(:token) { create(:oauth_access_token, scopes: 'api', resource_owner: user) }
let_it_be(:personal_access_token) { create(:personal_access_token, user: user) }
@@ -22,6 +23,10 @@ RSpec.shared_context 'set package name from package name type' do
case package_name_type
when :scoped_naming_convention
"@#{group.path}/scoped-package"
+ when :scoped_no_naming_convention
+ '@any-scope/scoped-package'
+ when :unscoped
+ 'unscoped-package'
when :non_existing
'non-existing-package'
end
diff --git a/spec/support/shared_contexts/security_and_compliance_permissions_shared_context.rb b/spec/support/shared_contexts/security_and_compliance_permissions_shared_context.rb
new file mode 100644
index 00000000000..dc5195e4b01
--- /dev/null
+++ b/spec/support/shared_contexts/security_and_compliance_permissions_shared_context.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+RSpec.shared_context '"Security & Compliance" permissions' do
+ let(:project_instance) { an_instance_of(Project) }
+ let(:user_instance) { an_instance_of(User) }
+ let(:before_request_defined) { false }
+ let(:valid_request) {}
+
+ def self.before_request(&block)
+ return unless block
+
+ let(:before_request_call) { instance_exec(&block) }
+ let(:before_request_defined) { true }
+ end
+
+ before do
+ allow(Ability).to receive(:allowed?).and_call_original
+ allow(Ability).to receive(:allowed?).with(user_instance, :access_security_and_compliance, project_instance).and_return(true)
+ end
+
+ context 'when the "Security & Compliance" feature is disabled' do
+ subject { response }
+
+ before do
+ before_request_call if before_request_defined
+
+ allow(Ability).to receive(:allowed?).with(user_instance, :access_security_and_compliance, project_instance).and_return(false)
+ valid_request
+ end
+
+ it { is_expected.to have_gitlab_http_status(:not_found) }
+ end
+end
diff --git a/spec/support/shared_examples/alert_notification_service_shared_examples.rb b/spec/support/shared_examples/alert_notification_service_shared_examples.rb
index 7bd6df8c608..fc935effe0e 100644
--- a/spec/support/shared_examples/alert_notification_service_shared_examples.rb
+++ b/spec/support/shared_examples/alert_notification_service_shared_examples.rb
@@ -27,3 +27,18 @@ RSpec.shared_examples 'Alert Notification Service sends no notifications' do |ht
end
end
end
+
+RSpec.shared_examples 'creates status-change system note for an auto-resolved alert' do
+ it 'has 2 new system notes' do
+ expect { subject }.to change(Note, :count).by(2)
+ expect(Note.last.note).to include('Resolved')
+ end
+end
+
+# Requires `source` to be defined
+RSpec.shared_examples 'creates single system note based on the source of the alert' do
+ it 'has one new system note' do
+ expect { subject }.to change(Note, :count).by(1)
+ expect(Note.last.note).to include(source)
+ end
+end
diff --git a/spec/support/shared_examples/banzai/filters/emoji_shared_examples.rb b/spec/support/shared_examples/banzai/filters/emoji_shared_examples.rb
new file mode 100644
index 00000000000..da305f5ccaa
--- /dev/null
+++ b/spec/support/shared_examples/banzai/filters/emoji_shared_examples.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'emoji filter' do
+ it 'keeps whitespace intact' do
+ doc = filter("This deserves a #{emoji_name}, big time.")
+
+ expect(doc.to_html).to match(/^This deserves a <gl-emoji.+>, big time\.\z/)
+ end
+
+ it 'does not match emoji in a string' do
+ doc = filter("'2a00:a4c0#{emoji_name}:1'")
+
+ expect(doc.css('gl-emoji')).to be_empty
+ end
+
+ it 'ignores non existent/unsupported emoji' do
+ exp = '<p>:foo:</p>'
+ doc = filter(exp)
+
+ expect(doc.to_html).to eq(exp)
+ end
+
+ it 'matches with adjacent text' do
+ doc = filter("#{emoji_name.delete(':')} (#{emoji_name})")
+
+ expect(doc.css('gl-emoji').size).to eq 1
+ end
+
+ it 'does not match emoji in a pre tag' do
+ doc = filter("<p><pre>#{emoji_name}</pre></p>")
+
+ expect(doc.css('img')).to be_empty
+ end
+
+ it 'does not match emoji in code tag' do
+ doc = filter("<p><code>#{emoji_name} wow</code></p>")
+
+ expect(doc.css('img')).to be_empty
+ end
+
+ it 'does not match emoji in tt tag' do
+ doc = filter("<p><tt>#{emoji_name} yes!</tt></p>")
+
+ expect(doc.css('img')).to be_empty
+ end
+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 7f49d20c83e..9c8006ce4f1 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
@@ -9,6 +9,8 @@ RSpec.shared_examples 'multiple issue boards' do
login_as(user)
+ stub_feature_flags(board_new_list: false)
+
visit boards_path
wait_for_requests
end
diff --git a/spec/support/shared_examples/controllers/create_notes_rate_limit_shared_examples.rb b/spec/support/shared_examples/controllers/create_notes_rate_limit_shared_examples.rb
new file mode 100644
index 00000000000..74a98c20383
--- /dev/null
+++ b/spec/support/shared_examples/controllers/create_notes_rate_limit_shared_examples.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+#
+# Requires a context containing:
+# - user
+# - params
+# - request_full_path
+
+RSpec.shared_examples 'request exceeding rate limit' do
+ before do
+ stub_application_setting(notes_create_limit: 2)
+ 2.times { post :create, params: params }
+ end
+
+ it 'prevents from creating more notes', :request_store do
+ expect { post :create, params: params }
+ .to change { Note.count }.by(0)
+
+ expect(response).to have_gitlab_http_status(:too_many_requests)
+ expect(response.body).to eq(_('This endpoint has been requested too many times. Try again later.'))
+ end
+
+ it 'logs the event in auth.log' do
+ attributes = {
+ message: 'Application_Rate_Limiter_Request',
+ env: :notes_create_request_limit,
+ remote_ip: '0.0.0.0',
+ request_method: 'POST',
+ path: request_full_path,
+ user_id: user.id,
+ username: user.username
+ }
+
+ expect(Gitlab::AuthLogger).to receive(:error).with(attributes).once
+ post :create, params: params
+ end
+
+ it 'allows user in allow-list to create notes, even if the case is different' do
+ user.update_attribute(:username, user.username.titleize)
+ stub_application_setting(notes_create_limit_allowlist: ["#{user.username.downcase}"])
+
+ post :create, params: params
+ expect(response).to have_gitlab_http_status(:found)
+ end
+end
diff --git a/spec/support/shared_examples/controllers/snippet_blob_shared_examples.rb b/spec/support/shared_examples/controllers/snippet_blob_shared_examples.rb
index c3e8f807afb..62aaec85162 100644
--- a/spec/support/shared_examples/controllers/snippet_blob_shared_examples.rb
+++ b/spec/support/shared_examples/controllers/snippet_blob_shared_examples.rb
@@ -17,6 +17,38 @@ RSpec.shared_examples 'raw snippet blob' do
end
end
+ context 'Content Disposition' do
+ context 'when the disposition is inline' do
+ let(:inline) { true }
+
+ it 'returns inline in the content disposition header' do
+ subject
+
+ expect(response.header['Content-Disposition']).to eq('inline')
+ end
+ end
+
+ context 'when the disposition is attachment' do
+ let(:inline) { false }
+
+ it 'returns attachment plus the filename in the content disposition header' do
+ subject
+
+ expect(response.header['Content-Disposition']).to match "attachment; filename=\"#{filepath}\""
+ end
+
+ context 'when the feature flag attachment_with_filename is disabled' do
+ it 'returns just attachment in the disposition header' do
+ stub_feature_flags(attachment_with_filename: false)
+
+ subject
+
+ expect(response.header['Content-Disposition']).to eq 'attachment'
+ end
+ end
+ end
+ end
+
context 'with invalid file path' do
let(:filepath) { 'doesnotexist' }
diff --git a/spec/support/shared_examples/features/comment_and_close_button_shared_examples.rb b/spec/support/shared_examples/features/comment_and_close_button_shared_examples.rb
deleted file mode 100644
index 4ee2840ed9f..00000000000
--- a/spec/support/shared_examples/features/comment_and_close_button_shared_examples.rb
+++ /dev/null
@@ -1,29 +0,0 @@
-# frozen_string_literal: true
-
-RSpec.shared_examples 'page with comment and close button' do |button_text|
- context 'when remove_comment_close_reopen feature flag is enabled' do
- before do
- stub_feature_flags(remove_comment_close_reopen: true)
- setup
- end
-
- it "does not show #{button_text} button" do
- within '.note-form-actions' do
- expect(page).not_to have_button(button_text)
- end
- end
- end
-
- context 'when remove_comment_close_reopen feature flag is disabled' do
- before do
- stub_feature_flags(remove_comment_close_reopen: false)
- setup
- end
-
- it "shows #{button_text} button" do
- within '.note-form-actions' do
- expect(page).to have_button(button_text)
- end
- end
- end
-end
diff --git a/spec/support/shared_examples/features/discussion_comments_shared_example.rb b/spec/support/shared_examples/features/discussion_comments_shared_example.rb
index 6bebd59ed70..86ba2821c78 100644
--- a/spec/support/shared_examples/features/discussion_comments_shared_example.rb
+++ b/spec/support/shared_examples/features/discussion_comments_shared_example.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-RSpec.shared_examples 'thread comments' do |resource_name|
+RSpec.shared_examples 'thread comments for commit and snippet' do |resource_name|
let(:form_selector) { '.js-main-target-form' }
let(:dropdown_selector) { "#{form_selector} .comment-type-dropdown" }
let(:toggle_selector) { "#{dropdown_selector} .dropdown-toggle" }
@@ -24,23 +24,6 @@ RSpec.shared_examples 'thread comments' do |resource_name|
expect(new_comment).not_to have_selector '.discussion'
end
- if resource_name == 'issue'
- it "clicking 'Comment & close #{resource_name}' will post a comment and close the #{resource_name}" do
- find("#{form_selector} .note-textarea").send_keys(comment)
-
- click_button 'Comment & close issue'
-
- wait_for_all_requests
-
- expect(page).to have_content(comment)
- expect(page).to have_content "@#{user.username} closed"
-
- new_comment = all(comments_selector).last
-
- expect(new_comment).not_to have_selector '.discussion'
- end
- end
-
describe 'when the toggle is clicked' do
before do
find("#{form_selector} .note-textarea").send_keys(comment)
@@ -110,33 +93,172 @@ RSpec.shared_examples 'thread comments' do |resource_name|
end
it 'updates the submit button text and closes the dropdown' do
- button = find(submit_selector)
+ expect(find(submit_selector).value).to eq 'Start thread'
- # on issues page, the submit input is a <button>, on other pages it is <input>
- if button.tag_name == 'button'
- expect(find(submit_selector)).to have_content 'Start thread'
- else
- expect(find(submit_selector).value).to eq 'Start thread'
+ expect(page).not_to have_selector menu_selector
+ end
+
+ describe 'creating a thread' do
+ before do
+ find(submit_selector).click
+ wait_for_requests
+
+ find(comments_selector, match: :first)
end
- expect(page).not_to have_selector menu_selector
+ def submit_reply(text)
+ find("#{comments_selector} .js-vue-discussion-reply").click
+ find("#{comments_selector} .note-textarea").send_keys(text)
+
+ find("#{comments_selector} .js-comment-button").click
+ wait_for_requests
+ end
+
+ it 'clicking "Start thread" will post a thread' do
+ expect(page).to have_content(comment)
+
+ new_comment = all(comments_selector).last
+
+ expect(new_comment).to have_selector('.discussion')
+ end
end
- if resource_name =~ /(issue|merge request)/
- it 'updates the close button text' do
- expect(find(close_selector)).to have_content "Start thread & close #{resource_name}"
+ describe 'when opening the menu' do
+ before do
+ find(toggle_selector).click
+ end
+
+ it 'has "Start thread" selected' do
+ find("#{menu_selector} li", match: :first)
+ items = all("#{menu_selector} li")
+
+ expect(items.first).to have_content 'Comment'
+ expect(items.first).not_to have_selector '[data-testid="check-icon"]'
+ expect(items.first['class']).not_to match 'droplab-item-selected'
+
+ expect(items.last).to have_content 'Start thread'
+ expect(items.last).to have_selector '[data-testid="check-icon"]'
+ expect(items.last['class']).to match 'droplab-item-selected'
end
- it 'typing does not change the close button text' do
- find("#{form_selector} .note-textarea").send_keys('b')
+ describe 'when selecting "Comment"' do
+ before do
+ find("#{menu_selector} li", match: :first).click
+ end
+
+ it 'updates the submit button text and closes the dropdown' do
+ button = find(submit_selector)
+
+ expect(button.value).to eq 'Comment'
+
+ expect(page).not_to have_selector menu_selector
+ end
- expect(find(close_selector)).to have_content "Start thread & close #{resource_name}"
+ it 'has "Comment" selected when opening the menu', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/196825' do
+ find(toggle_selector).click
+
+ find("#{menu_selector} li", match: :first)
+ items = all("#{menu_selector} li")
+
+ aggregate_failures do
+ expect(items.first).to have_content 'Comment'
+ expect(items.first).to have_selector '[data-testid="check-icon"]'
+ expect(items.first['class']).to match 'droplab-item-selected'
+
+ expect(items.last).to have_content 'Start thread'
+ expect(items.last).not_to have_selector '[data-testid="check-icon"]'
+ expect(items.last['class']).not_to match 'droplab-item-selected'
+ end
+ end
end
end
+ end
+ end
+end
+
+RSpec.shared_examples 'thread comments for issue, epic and merge request' do |resource_name|
+ let(:form_selector) { '.js-main-target-form' }
+ let(:dropdown_selector) { "#{form_selector} [data-testid='comment-button']" }
+ let(:submit_button_selector) { "#{dropdown_selector} .split-content-button" }
+ let(:toggle_selector) { "#{dropdown_selector} .dropdown-toggle-split" }
+ let(:menu_selector) { "#{dropdown_selector} .dropdown-menu" }
+ let(:close_selector) { "#{form_selector} .btn-comment-and-close" }
+ let(:comments_selector) { '.timeline > .note.timeline-entry' }
+ let(:comment) { 'My comment' }
+
+ it 'clicking "Comment" will post a comment' do
+ expect(page).to have_selector toggle_selector
+
+ find("#{form_selector} .note-textarea").send_keys(comment)
+
+ find(submit_button_selector).click
+
+ expect(page).to have_content(comment)
+
+ new_comment = all(comments_selector).last
+
+ expect(new_comment).not_to have_selector '.discussion'
+ end
+
+ if resource_name == 'issue'
+ it "clicking 'Comment & close #{resource_name}' will post a comment and close the #{resource_name}" do
+ find("#{form_selector} .note-textarea").send_keys(comment)
+
+ click_button 'Comment & close issue'
+
+ wait_for_all_requests
+
+ expect(page).to have_content(comment)
+ expect(page).to have_content "@#{user.username} closed"
+
+ new_comment = all(comments_selector).last
+
+ expect(new_comment).not_to have_selector '.discussion'
+ end
+ end
+
+ describe 'when the toggle is clicked' do
+ before do
+ find("#{form_selector} .note-textarea").send_keys(comment)
+
+ find(toggle_selector).click
+ end
+
+ it 'has a "Comment" item (selected by default) and "Start thread" item' do
+ expect(page).to have_selector menu_selector
+
+ find("#{menu_selector} li", match: :first)
+ items = all("#{menu_selector} li")
+
+ expect(page).to have_selector("#{dropdown_selector}[data-track-label='comment_button']")
+
+ expect(items.first).to have_content 'Comment'
+ expect(items.first).to have_content "Add a general comment to this #{resource_name}."
+
+ expect(items.last).to have_content 'Start thread'
+ expect(items.last).to have_content "Discuss a specific suggestion or question#{' that needs to be resolved' if resource_name == 'merge request'}."
+ end
+
+ it 'closes the menu when clicking the toggle or body' do
+ find(toggle_selector).click
+
+ expect(page).not_to have_selector menu_selector
+
+ find(toggle_selector).click
+ find("#{form_selector} .note-textarea").click
+
+ expect(page).not_to have_selector menu_selector
+ end
+
+ describe 'when selecting "Start thread"' do
+ before do
+ find("#{menu_selector} li", match: :first)
+ all("#{menu_selector} li").last.click
+ end
describe 'creating a thread' do
before do
- find(submit_selector).click
+ find(submit_button_selector).click
wait_for_requests
find(comments_selector, match: :first)
@@ -146,6 +268,7 @@ RSpec.shared_examples 'thread comments' do |resource_name|
find("#{comments_selector} .js-vue-discussion-reply").click
find("#{comments_selector} .note-textarea").send_keys(text)
+ # .js-comment-button here refers to the reply button in note_form.vue
find("#{comments_selector} .js-comment-button").click
wait_for_requests
end
@@ -228,13 +351,11 @@ RSpec.shared_examples 'thread comments' do |resource_name|
find("#{menu_selector} li", match: :first)
items = all("#{menu_selector} li")
+ expect(page).to have_selector("#{dropdown_selector}[data-track-label='start_thread_button']")
+
expect(items.first).to have_content 'Comment'
- expect(items.first).not_to have_selector '[data-testid="check-icon"]'
- expect(items.first['class']).not_to match 'droplab-item-selected'
expect(items.last).to have_content 'Start thread'
- expect(items.last).to have_selector '[data-testid="check-icon"]'
- expect(items.last['class']).to match 'droplab-item-selected'
end
describe 'when selecting "Comment"' do
@@ -243,14 +364,9 @@ RSpec.shared_examples 'thread comments' do |resource_name|
end
it 'updates the submit button text and closes the dropdown' do
- button = find(submit_selector)
+ button = find(submit_button_selector)
- # on issues page, the submit input is a <button>, on other pages it is <input>
- if button.tag_name == 'button'
- expect(button).to have_content 'Comment'
- else
- expect(button.value).to eq 'Comment'
- end
+ expect(button).to have_content 'Comment'
expect(page).not_to have_selector menu_selector
end
@@ -267,21 +383,17 @@ RSpec.shared_examples 'thread comments' do |resource_name|
end
end
- it 'has "Comment" selected when opening the menu', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/196825' do
+ it 'has "Comment" selected when opening the menu' do
find(toggle_selector).click
find("#{menu_selector} li", match: :first)
items = all("#{menu_selector} li")
- aggregate_failures do
- expect(items.first).to have_content 'Comment'
- expect(items.first).to have_selector '[data-testid="check-icon"]'
- expect(items.first['class']).to match 'droplab-item-selected'
+ expect(page).to have_selector("#{dropdown_selector}[data-track-label='comment_button']")
- expect(items.last).to have_content 'Start thread'
- expect(items.last).not_to have_selector '[data-testid="check-icon"]'
- expect(items.last['class']).not_to match 'droplab-item-selected'
- end
+ expect(items.first).to have_content 'Comment'
+
+ expect(items.last).to have_content 'Start thread'
end
end
end
diff --git a/spec/support/shared_examples/features/issuable_invite_members_shared_examples.rb b/spec/support/shared_examples/features/issuable_invite_members_shared_examples.rb
index 3fec1a56c0c..7a32f61d4fa 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
@@ -1,11 +1,7 @@
# frozen_string_literal: true
RSpec.shared_examples 'issuable invite members experiments' do
- context 'when invite_members_version_a experiment is enabled' do
- before do
- stub_experiment_for_subject(invite_members_version_a: true)
- end
-
+ context 'when a privileged user can invite' do
it 'shows a link for inviting members and follows through to the members page' do
project.add_maintainer(user)
visit issuable_path
@@ -51,9 +47,9 @@ RSpec.shared_examples 'issuable invite members experiments' do
end
end
- context 'when no invite members experiments are enabled' do
+ context 'when invite_members_version_b experiment is disabled' do
it 'shows author in assignee dropdown and no invite link' do
- project.add_maintainer(user)
+ project.add_developer(user)
visit issuable_path
find('.block.assignee .edit-link').click
diff --git a/spec/support/shared_examples/features/project_upload_files_shared_examples.rb b/spec/support/shared_examples/features/project_upload_files_shared_examples.rb
index 25203fa3182..00d3bd08218 100644
--- a/spec/support/shared_examples/features/project_upload_files_shared_examples.rb
+++ b/spec/support/shared_examples/features/project_upload_files_shared_examples.rb
@@ -3,7 +3,13 @@
RSpec.shared_examples 'it uploads and commit a new text file' do
it 'uploads and commit a new text file', :js do
find('.add-to-tree').click
- click_link('Upload file')
+
+ page.within('.dropdown-menu') do
+ click_link('Upload file')
+
+ wait_for_requests
+ end
+
drop_in_dropzone(File.join(Rails.root, 'spec', 'fixtures', 'doc_sample.txt'))
page.within('#modal-upload-blob') do
@@ -29,7 +35,13 @@ end
RSpec.shared_examples 'it uploads and commit a new image file' do
it 'uploads and commit a new image file', :js do
find('.add-to-tree').click
- click_link('Upload file')
+
+ page.within('.dropdown-menu') do
+ click_link('Upload file')
+
+ wait_for_requests
+ end
+
drop_in_dropzone(File.join(Rails.root, 'spec', 'fixtures', 'logo_sample.svg'))
page.within('#modal-upload-blob') do
@@ -82,3 +94,21 @@ RSpec.shared_examples 'it uploads and commit a new file to a forked project' do
expect(page).to have_content('Sed ut perspiciatis unde omnis')
end
end
+
+RSpec.shared_examples 'uploads and commits a new text file via "upload file" button' do
+ it 'uploads and commits a new text file via "upload file" button', :js do
+ find('[data-testid="upload-file-button"]').click
+
+ attach_file('upload_file', File.join(Rails.root, 'spec', 'fixtures', 'doc_sample.txt'), make_visible: true)
+
+ page.within('#details-modal-upload-blob') do
+ fill_in(:commit_message, with: 'New commit message')
+ end
+
+ click_button('Upload file')
+
+ expect(page).to have_content('New commit message')
+ expect(page).to have_content('Lorem ipsum dolor sit amet')
+ expect(page).to have_content('Sed ut perspiciatis unde omnis')
+ end
+end
diff --git a/spec/support/shared_examples/features/variable_list_shared_examples.rb b/spec/support/shared_examples/features/variable_list_shared_examples.rb
index e0d169c6868..2fd88b610e9 100644
--- a/spec/support/shared_examples/features/variable_list_shared_examples.rb
+++ b/spec/support/shared_examples/features/variable_list_shared_examples.rb
@@ -2,7 +2,7 @@
RSpec.shared_examples 'variable list' do
it 'shows a list of variables' do
- page.within('.ci-variable-table') do
+ page.within('[data-testid="ci-variable-table"]') do
expect(find('.js-ci-variable-row:nth-child(1) td[data-label="Key"]').text).to eq(variable.key)
end
end
@@ -16,7 +16,7 @@ RSpec.shared_examples 'variable list' do
wait_for_requests
- page.within('.ci-variable-table') do
+ page.within('[data-testid="ci-variable-table"]') do
expect(find('.js-ci-variable-row:nth-child(1) td[data-label="Key"]').text).to eq('key')
end
end
@@ -30,7 +30,7 @@ RSpec.shared_examples 'variable list' do
wait_for_requests
- page.within('.ci-variable-table') do
+ page.within('[data-testid="ci-variable-table"]') do
expect(find('.js-ci-variable-row:nth-child(1) td[data-label="Key"]').text).to eq('key')
expect(find('.js-ci-variable-row:nth-child(1) td[data-label="Protected"] svg[data-testid="mobile-issue-close-icon"]')).to be_present
end
@@ -45,14 +45,14 @@ RSpec.shared_examples 'variable list' do
wait_for_requests
- page.within('.ci-variable-table') do
+ page.within('[data-testid="ci-variable-table"]') do
expect(find('.js-ci-variable-row:nth-child(1) td[data-label="Key"]').text).to eq('key')
expect(find('.js-ci-variable-row:nth-child(1) td[data-label="Masked"] svg[data-testid="close-icon"]')).to be_present
end
end
it 'reveals and hides variables' do
- page.within('.ci-variable-table') do
+ page.within('[data-testid="ci-variable-table"]') do
expect(first('.js-ci-variable-row td[data-label="Key"]').text).to eq(variable.key)
expect(page).to have_content('*' * 17)
@@ -72,7 +72,7 @@ RSpec.shared_examples 'variable list' do
it 'deletes a variable' do
expect(page).to have_selector('.js-ci-variable-row', count: 1)
- page.within('.ci-variable-table') do
+ page.within('[data-testid="ci-variable-table"]') do
click_button('Edit')
end
@@ -86,7 +86,7 @@ RSpec.shared_examples 'variable list' do
end
it 'edits a variable' do
- page.within('.ci-variable-table') do
+ page.within('[data-testid="ci-variable-table"]') do
click_button('Edit')
end
@@ -102,7 +102,7 @@ RSpec.shared_examples 'variable list' do
end
it 'edits a variable to be unmasked' do
- page.within('.ci-variable-table') do
+ page.within('[data-testid="ci-variable-table"]') do
click_button('Edit')
end
@@ -115,13 +115,13 @@ RSpec.shared_examples 'variable list' do
wait_for_requests
- page.within('.ci-variable-table') do
+ page.within('[data-testid="ci-variable-table"]') do
expect(find('.js-ci-variable-row:nth-child(1) td[data-label="Masked"] svg[data-testid="close-icon"]')).to be_present
end
end
it 'edits a variable to be masked' do
- page.within('.ci-variable-table') do
+ page.within('[data-testid="ci-variable-table"]') do
click_button('Edit')
end
@@ -133,7 +133,7 @@ RSpec.shared_examples 'variable list' do
wait_for_requests
- page.within('.ci-variable-table') do
+ page.within('[data-testid="ci-variable-table"]') do
click_button('Edit')
end
@@ -143,7 +143,7 @@ RSpec.shared_examples 'variable list' do
click_button('Update variable')
end
- page.within('.ci-variable-table') do
+ page.within('[data-testid="ci-variable-table"]') do
expect(find('.js-ci-variable-row:nth-child(1) td[data-label="Masked"] svg[data-testid="mobile-issue-close-icon"]')).to be_present
end
end
@@ -211,7 +211,7 @@ RSpec.shared_examples 'variable list' do
expect(page).to have_selector('.js-ci-variable-row', count: 3)
# Remove the `akey` variable
- page.within('.ci-variable-table') do
+ page.within('[data-testid="ci-variable-table"]') do
page.within('.js-ci-variable-row:first-child') do
click_button('Edit')
end
diff --git a/spec/support/shared_examples/graphql/mutation_shared_examples.rb b/spec/support/shared_examples/graphql/mutation_shared_examples.rb
index 84ebd4852b9..51d52cbb901 100644
--- a/spec/support/shared_examples/graphql/mutation_shared_examples.rb
+++ b/spec/support/shared_examples/graphql/mutation_shared_examples.rb
@@ -48,6 +48,6 @@ RSpec.shared_examples 'a mutation that returns errors in the response' do |error
it do
post_graphql_mutation(mutation, current_user: current_user)
- expect(mutation_response['errors']).to eq(errors)
+ expect(mutation_response['errors']).to match_array(errors)
end
end
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 2b93d174653..2e3a3ce6b41 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
@@ -66,9 +66,7 @@ RSpec.shared_examples 'boards create mutation' do
context 'when the Boards::CreateService returns an error response' do
before do
- allow_next_instance_of(Boards::CreateService) do |service|
- allow(service).to receive(:execute).and_return(ServiceResponse.error(message: 'There was an error.'))
- end
+ params[:name] = ''
end
it 'does not create a board' do
@@ -80,7 +78,7 @@ RSpec.shared_examples 'boards create mutation' do
expect(mutation_response).to have_key('board')
expect(mutation_response['board']).to be_nil
- expect(mutation_response['errors'].first).to eq('There was an error.')
+ expect(mutation_response['errors'].first).to eq('There was an error when creating a board.')
end
end
end
diff --git a/spec/support/shared_examples/graphql/mutations/can_mutate_spammable_examples.rb b/spec/support/shared_examples/graphql/mutations/can_mutate_spammable_examples.rb
index d294f034d2e..bb4270d7db6 100644
--- a/spec/support/shared_examples/graphql/mutations/can_mutate_spammable_examples.rb
+++ b/spec/support/shared_examples/graphql/mutations/can_mutate_spammable_examples.rb
@@ -21,14 +21,14 @@ RSpec.shared_examples 'a mutation which can mutate a spammable' do
end
end
- describe "#with_spam_action_fields" do
+ describe "#with_spam_action_response_fields" do
it 'resolves with spam action fields' do
subject
# NOTE: We do not need to assert on the specific values of spam action fields here, we only need
- # to verify that #with_spam_action_fields was invoked and that the fields are present in the
- # response. The specific behavior of #with_spam_action_fields is covered in the
- # CanMutateSpammable unit tests.
+ # to verify that #with_spam_action_response_fields was invoked and that the fields are present in the
+ # response. The specific behavior of #with_spam_action_response_fields is covered in the
+ # HasSpamActionResponseFields unit tests.
expect(mutation_response.keys)
.to include('spam', 'spamLogId', 'needsCaptchaResponse', 'captchaSiteKey')
end
diff --git a/spec/support/shared_examples/graphql/notes_on_noteables_shared_examples.rb b/spec/support/shared_examples/graphql/notes_on_noteables_shared_examples.rb
index 41b7da07d2d..0d2e9f6ec8c 100644
--- a/spec/support/shared_examples/graphql/notes_on_noteables_shared_examples.rb
+++ b/spec/support/shared_examples/graphql/notes_on_noteables_shared_examples.rb
@@ -17,7 +17,7 @@ RSpec.shared_context 'exposing regular notes on a noteable in GraphQL' do
notes {
edges {
node {
- #{all_graphql_fields_for('Note')}
+ #{all_graphql_fields_for('Note', max_depth: 1)}
}
}
}
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 269e9170906..bc091a678e2 100644
--- a/spec/support/shared_examples/graphql/types/gitlab_style_deprecations_shared_examples.rb
+++ b/spec/support/shared_examples/graphql/types/gitlab_style_deprecations_shared_examples.rb
@@ -6,7 +6,7 @@ RSpec.shared_examples 'Gitlab-style deprecations' do
expect { subject(deprecation_reason: 'foo') }.to raise_error(
ArgumentError,
'Use `deprecated` property instead of `deprecation_reason`. ' \
- 'See https://docs.gitlab.com/ee/development/api_graphql_styleguide.html#deprecating-fields-and-enum-values'
+ 'See https://docs.gitlab.com/ee/development/api_graphql_styleguide.html#deprecating-fields-arguments-and-enum-values'
)
end
diff --git a/spec/support/shared_examples/helpers/issuable_description_templates_shared_examples.rb b/spec/support/shared_examples/helpers/issuable_description_templates_shared_examples.rb
index 9e8c96d576a..47e34b21036 100644
--- a/spec/support/shared_examples/helpers/issuable_description_templates_shared_examples.rb
+++ b/spec/support/shared_examples/helpers/issuable_description_templates_shared_examples.rb
@@ -23,11 +23,11 @@ RSpec.shared_examples 'project issuable templates' do
end
it 'returns only md files as issue templates' do
- expect(helper.issuable_templates(project, 'issue')).to eq(templates('issue', project))
+ expect(helper.issuable_templates(project, 'issue')).to eq(expected_templates('issue'))
end
it 'returns only md files as merge_request templates' do
- expect(helper.issuable_templates(project, 'merge_request')).to eq(templates('merge_request', project))
+ expect(helper.issuable_templates(project, 'merge_request')).to eq(expected_templates('merge_request'))
end
end
diff --git a/spec/support/shared_examples/lib/gitlab/cycle_analytics/event_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/cycle_analytics/event_shared_examples.rb
index 145a7290ac8..7d341d79bae 100644
--- a/spec/support/shared_examples/lib/gitlab/cycle_analytics/event_shared_examples.rb
+++ b/spec/support/shared_examples/lib/gitlab/cycle_analytics/event_shared_examples.rb
@@ -8,6 +8,7 @@ RSpec.shared_examples_for 'value stream analytics event' do
it { expect(described_class.identifier).to be_a_kind_of(Symbol) }
it { expect(instance.object_type.ancestors).to include(ApplicationRecord) }
it { expect(instance).to respond_to(:timestamp_projection) }
+ it { expect(instance).to respond_to(:markdown_description) }
it { expect(instance.column_list).to be_a_kind_of(Array) }
describe '#apply_query_customization' do
diff --git a/spec/support/shared_examples/lib/gitlab/usage_data_counters/issue_activity_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/usage_data_counters/issuable_activity_shared_examples.rb
index edd9b6cdf37..aa6e64a3820 100644
--- a/spec/support/shared_examples/lib/gitlab/usage_data_counters/issue_activity_shared_examples.rb
+++ b/spec/support/shared_examples/lib/gitlab/usage_data_counters/issuable_activity_shared_examples.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-RSpec.shared_examples 'a tracked issue edit event' do |event|
+RSpec.shared_examples 'a daily tracked issuable event' do
before do
stub_application_setting(usage_ping_enabled: true)
end
@@ -25,3 +25,13 @@ RSpec.shared_examples 'a tracked issue edit event' do |event|
expect(track_action(author: nil)).to be_nil
end
end
+
+RSpec.shared_examples 'does not track when feature flag is disabled' do |feature_flag|
+ context "when feature flag #{feature_flag} is disabled" do
+ it 'does not track action' do
+ stub_feature_flags(feature_flag => false)
+
+ expect(track_action(author: user1)).to be_nil
+ end
+ end
+end
diff --git a/spec/support/shared_examples/lib/sentry/client_shared_examples.rb b/spec/support/shared_examples/lib/sentry/client_shared_examples.rb
index 4221708b55c..d73c7b6848d 100644
--- a/spec/support/shared_examples/lib/sentry/client_shared_examples.rb
+++ b/spec/support/shared_examples/lib/sentry/client_shared_examples.rb
@@ -26,7 +26,7 @@ RSpec.shared_examples 'no Sentry redirects' do |http_method|
end
it 'does not follow redirects' do
- expect { subject }.to raise_exception(Sentry::Client::Error, 'Sentry response status code: 302')
+ expect { subject }.to raise_exception(ErrorTracking::SentryClient::Error, 'Sentry response status code: 302')
expect(redirect_req_stub).to have_been_requested
expect(redirected_req_stub).not_to have_been_requested
end
@@ -53,7 +53,7 @@ RSpec.shared_examples 'maps Sentry exceptions' do |http_method|
it do
expect { subject }
- .to raise_exception(Sentry::Client::Error, message)
+ .to raise_exception(ErrorTracking::SentryClient::Error, message)
end
end
end
diff --git a/spec/support/shared_examples/metrics/active_record_subscriber_shared_examples.rb b/spec/support/shared_examples/metrics/active_record_subscriber_shared_examples.rb
new file mode 100644
index 00000000000..7bf2456c548
--- /dev/null
+++ b/spec/support/shared_examples/metrics/active_record_subscriber_shared_examples.rb
@@ -0,0 +1,137 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'store ActiveRecord info in RequestStore' do |db_role|
+ it 'prevents db counters from leaking to the next transaction' do
+ 2.times do
+ Gitlab::WithRequestStore.with_request_store do
+ subscriber.sql(event)
+
+ if db_role == :primary
+ expect(described_class.db_counter_payload).to eq(
+ db_count: record_query ? 1 : 0,
+ db_write_count: record_write_query ? 1 : 0,
+ db_cached_count: record_cached_query ? 1 : 0,
+ db_primary_cached_count: record_cached_query ? 1 : 0,
+ db_primary_count: record_query ? 1 : 0,
+ db_primary_duration_s: record_query ? 0.002 : 0,
+ db_replica_cached_count: 0,
+ db_replica_count: 0,
+ db_replica_duration_s: 0.0
+ )
+ elsif db_role == :replica
+ expect(described_class.db_counter_payload).to eq(
+ db_count: record_query ? 1 : 0,
+ db_write_count: record_write_query ? 1 : 0,
+ db_cached_count: record_cached_query ? 1 : 0,
+ db_primary_cached_count: 0,
+ db_primary_count: 0,
+ db_primary_duration_s: 0.0,
+ db_replica_cached_count: record_cached_query ? 1 : 0,
+ db_replica_count: record_query ? 1 : 0,
+ db_replica_duration_s: record_query ? 0.002 : 0
+ )
+ else
+ expect(described_class.db_counter_payload).to eq(
+ db_count: record_query ? 1 : 0,
+ db_write_count: record_write_query ? 1 : 0,
+ db_cached_count: record_cached_query ? 1 : 0
+ )
+ end
+ end
+ end
+ end
+end
+
+RSpec.shared_examples 'record ActiveRecord metrics in a metrics transaction' do |db_role|
+ it 'increments only db counters' do
+ if record_query
+ expect(transaction).to receive(:increment).with(:gitlab_transaction_db_count_total, 1)
+ expect(transaction).to receive(:increment).with("gitlab_transaction_db_#{db_role}_count_total".to_sym, 1) if db_role
+ else
+ expect(transaction).not_to receive(:increment).with(:gitlab_transaction_db_count_total, 1)
+ expect(transaction).not_to receive(:increment).with("gitlab_transaction_db_#{db_role}_count_total".to_sym, 1) if db_role
+ end
+
+ if record_write_query
+ expect(transaction).to receive(:increment).with(:gitlab_transaction_db_write_count_total, 1)
+ else
+ expect(transaction).not_to receive(:increment).with(:gitlab_transaction_db_write_count_total, 1)
+ end
+
+ if record_cached_query
+ expect(transaction).to receive(:increment).with(:gitlab_transaction_db_cached_count_total, 1)
+ expect(transaction).to receive(:increment).with("gitlab_transaction_db_#{db_role}_cached_count_total".to_sym, 1) if db_role
+ else
+ expect(transaction).not_to receive(:increment).with(:gitlab_transaction_db_cached_count_total, 1)
+ expect(transaction).not_to receive(:increment).with("gitlab_transaction_db_#{db_role}_cached_count_total".to_sym, 1) if db_role
+ end
+
+ subscriber.sql(event)
+ end
+
+ it 'observes sql_duration metric' do
+ if record_query
+ expect(transaction).to receive(:observe).with(:gitlab_sql_duration_seconds, 0.002)
+ expect(transaction).to receive(:observe).with("gitlab_sql_#{db_role}_duration_seconds".to_sym, 0.002) if db_role
+ else
+ expect(transaction).not_to receive(:observe)
+ end
+
+ subscriber.sql(event)
+ end
+end
+
+RSpec.shared_examples 'record ActiveRecord metrics' do |db_role|
+ context 'when both web and background transaction are available' do
+ let(:transaction) { double('Gitlab::Metrics::WebTransaction') }
+ let(:background_transaction) { double('Gitlab::Metrics::WebTransaction') }
+
+ before do
+ allow(::Gitlab::Metrics::WebTransaction).to receive(:current)
+ .and_return(transaction)
+ allow(::Gitlab::Metrics::BackgroundTransaction).to receive(:current)
+ .and_return(background_transaction)
+ allow(transaction).to receive(:increment)
+ allow(transaction).to receive(:observe)
+ end
+
+ it_behaves_like 'record ActiveRecord metrics in a metrics transaction', db_role
+
+ it 'captures the metrics for web only' do
+ expect(background_transaction).not_to receive(:observe)
+ expect(background_transaction).not_to receive(:increment)
+
+ subscriber.sql(event)
+ end
+ end
+
+ context 'when web transaction is available' do
+ let(:transaction) { double('Gitlab::Metrics::WebTransaction') }
+
+ before do
+ allow(::Gitlab::Metrics::WebTransaction).to receive(:current)
+ .and_return(transaction)
+ allow(::Gitlab::Metrics::BackgroundTransaction).to receive(:current)
+ .and_return(nil)
+ allow(transaction).to receive(:increment)
+ allow(transaction).to receive(:observe)
+ end
+
+ it_behaves_like 'record ActiveRecord metrics in a metrics transaction', db_role
+ end
+
+ context 'when background transaction is available' do
+ let(:transaction) { double('Gitlab::Metrics::BackgroundTransaction') }
+
+ before do
+ allow(::Gitlab::Metrics::WebTransaction).to receive(:current)
+ .and_return(nil)
+ allow(::Gitlab::Metrics::BackgroundTransaction).to receive(:current)
+ .and_return(transaction)
+ allow(transaction).to receive(:increment)
+ allow(transaction).to receive(:observe)
+ end
+
+ it_behaves_like 'record ActiveRecord metrics in a metrics transaction', db_role
+ end
+end
diff --git a/spec/support/shared_examples/models/application_setting_shared_examples.rb b/spec/support/shared_examples/models/application_setting_shared_examples.rb
index 92fd4363134..60a02d85a1e 100644
--- a/spec/support/shared_examples/models/application_setting_shared_examples.rb
+++ b/spec/support/shared_examples/models/application_setting_shared_examples.rb
@@ -289,6 +289,7 @@ RSpec.shared_examples 'application settings examples' do
describe '#pick_repository_storage' do
before do
+ allow(Gitlab.config.repositories.storages).to receive(:keys).and_return(%w(default backup))
allow(setting).to receive(:repository_storages_weighted).and_return({ 'default' => 20, 'backup' => 80 })
end
@@ -304,15 +305,19 @@ RSpec.shared_examples 'application settings examples' do
describe '#normalized_repository_storage_weights' do
using RSpec::Parameterized::TableSyntax
- where(:storages, :normalized) do
- { 'default' => 0, 'backup' => 100 } | { 'default' => 0.0, 'backup' => 1.0 }
- { 'default' => 100, 'backup' => 100 } | { 'default' => 0.5, 'backup' => 0.5 }
- { 'default' => 20, 'backup' => 80 } | { 'default' => 0.2, 'backup' => 0.8 }
- { 'default' => 0, 'backup' => 0 } | { 'default' => 0.0, 'backup' => 0.0 }
+ where(:config_storages, :storages, :normalized) do
+ %w(default backup) | { 'default' => 0, 'backup' => 100 } | { 'default' => 0.0, 'backup' => 1.0 }
+ %w(default backup) | { 'default' => 100, 'backup' => 100 } | { 'default' => 0.5, 'backup' => 0.5 }
+ %w(default backup) | { 'default' => 20, 'backup' => 80 } | { 'default' => 0.2, 'backup' => 0.8 }
+ %w(default backup) | { 'default' => 0, 'backup' => 0 } | { 'default' => 0.0, 'backup' => 0.0 }
+ %w(default) | { 'default' => 0, 'backup' => 100 } | { 'default' => 0.0 }
+ %w(default) | { 'default' => 100, 'backup' => 100 } | { 'default' => 1.0 }
+ %w(default) | { 'default' => 20, 'backup' => 80 } | { 'default' => 1.0 }
end
with_them do
before do
+ allow(Gitlab.config.repositories.storages).to receive(:keys).and_return(config_storages)
allow(setting).to receive(:repository_storages_weighted).and_return(storages)
end
diff --git a/spec/support/shared_examples/models/boards/user_preferences_shared_examples.rb b/spec/support/shared_examples/models/boards/user_preferences_shared_examples.rb
new file mode 100644
index 00000000000..766aeac9476
--- /dev/null
+++ b/spec/support/shared_examples/models/boards/user_preferences_shared_examples.rb
@@ -0,0 +1,68 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'list_preferences_for user' do |list_factory, list_id_attribute|
+ subject { create(list_factory) } # rubocop:disable Rails/SaveBang
+
+ let_it_be(:user) { create(:user) }
+
+ describe '#preferences_for' do
+ context 'when user is nil' do
+ it 'returns not persisted preferences' do
+ preferences = subject.preferences_for(nil)
+
+ expect(preferences).not_to be_persisted
+ expect(preferences[list_id_attribute]).to eq(subject.id)
+ expect(preferences.user_id).to be_nil
+ end
+ end
+
+ context 'when a user preference already exists' do
+ before do
+ subject.update_preferences_for(user, collapsed: true)
+ end
+
+ it 'loads preference for user' do
+ preferences = subject.preferences_for(user)
+
+ expect(preferences).to be_persisted
+ expect(preferences.collapsed).to eq(true)
+ end
+ end
+
+ context 'when preferences for user does not exist' do
+ it 'returns not persisted preferences' do
+ preferences = subject.preferences_for(user)
+
+ expect(preferences).not_to be_persisted
+ expect(preferences.user_id).to eq(user.id)
+ expect(preferences.public_send(list_id_attribute)).to eq(subject.id)
+ end
+ end
+ end
+
+ describe '#update_preferences_for' do
+ context 'when user is present' do
+ context 'when there are no preferences for user' do
+ it 'creates new user preferences' do
+ expect { subject.update_preferences_for(user, collapsed: true) }.to change { subject.preferences.count }.by(1)
+ expect(subject.preferences_for(user).collapsed).to eq(true)
+ end
+ end
+
+ context 'when there are preferences for user' do
+ it 'updates user preferences' do
+ subject.update_preferences_for(user, collapsed: false)
+
+ expect { subject.update_preferences_for(user, collapsed: true) }.not_to change { subject.preferences.count }
+ expect(subject.preferences_for(user).collapsed).to eq(true)
+ end
+ end
+
+ context 'when user is nil' do
+ it 'does not create user preferences' do
+ expect { subject.update_preferences_for(nil, collapsed: true) }.not_to change { subject.preferences.count }
+ end
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/models/chat_service_shared_examples.rb b/spec/support/shared_examples/models/chat_service_shared_examples.rb
index ad237ad9f49..59e249bb865 100644
--- a/spec/support/shared_examples/models/chat_service_shared_examples.rb
+++ b/spec/support/shared_examples/models/chat_service_shared_examples.rb
@@ -53,9 +53,13 @@ RSpec.shared_examples "chat service" do |service_name|
end
it "calls #{service_name} API" do
- subject.execute(sample_data)
+ result = subject.execute(sample_data)
- expect(WebMock).to have_requested(:post, webhook_url).with { |req| req.body =~ /\A{"#{content_key}":.+}\Z/ }.once
+ expect(result).to be(true)
+ expect(WebMock).to have_requested(:post, webhook_url).once.with { |req|
+ json_body = Gitlab::Json.parse(req.body).with_indifferent_access
+ expect(json_body).to include(payload)
+ }
end
end
@@ -67,7 +71,8 @@ RSpec.shared_examples "chat service" do |service_name|
it "does not call #{service_name} API" do
result = subject.execute(sample_data)
- expect(result).to be_falsy
+ expect(result).to be(false)
+ expect(WebMock).not_to have_requested(:post, webhook_url)
end
end
diff --git a/spec/support/shared_examples/models/concerns/timebox_shared_examples.rb b/spec/support/shared_examples/models/concerns/timebox_shared_examples.rb
index f91e4bd8cf7..68142e667a4 100644
--- a/spec/support/shared_examples/models/concerns/timebox_shared_examples.rb
+++ b/spec/support/shared_examples/models/concerns/timebox_shared_examples.rb
@@ -18,7 +18,7 @@ RSpec.shared_examples 'a timebox' do |timebox_type|
context 'with a project' do
it_behaves_like 'AtomicInternalId' do
let(:internal_id_attribute) { :iid }
- let(:instance) { build(timebox_type, *timebox_args, project: build(:project), group: nil) }
+ let(:instance) { build(timebox_type, *timebox_args, project: create(:project), group: nil) }
let(:scope) { :project }
let(:scope_attrs) { { project: instance.project } }
let(:usage) { timebox_table_name }
@@ -28,7 +28,7 @@ RSpec.shared_examples 'a timebox' do |timebox_type|
context 'with a group' do
it_behaves_like 'AtomicInternalId' do
let(:internal_id_attribute) { :iid }
- let(:instance) { build(timebox_type, *timebox_args, project: nil, group: build(:group)) }
+ let(:instance) { build(timebox_type, *timebox_args, project: nil, group: create(:group)) }
let(:scope) { :group }
let(:scope_attrs) { { namespace: instance.group } }
let(:usage) { timebox_table_name }
diff --git a/spec/support/shared_examples/models/email_format_shared_examples.rb b/spec/support/shared_examples/models/email_format_shared_examples.rb
index a8115e440a4..77ded168637 100644
--- a/spec/support/shared_examples/models/email_format_shared_examples.rb
+++ b/spec/support/shared_examples/models/email_format_shared_examples.rb
@@ -6,7 +6,7 @@
# Note: You have access to `email_value` which is the email address value
# being currently tested).
-RSpec.shared_examples 'an object with email-formated attributes' do |*attributes|
+RSpec.shared_examples 'an object with email-formatted attributes' do |*attributes|
attributes.each do |attribute|
describe "specifically its :#{attribute} attribute" do
%w[
@@ -45,7 +45,7 @@ RSpec.shared_examples 'an object with email-formated attributes' do |*attributes
end
end
-RSpec.shared_examples 'an object with RFC3696 compliant email-formated attributes' do |*attributes|
+RSpec.shared_examples 'an object with RFC3696 compliant email-formatted attributes' do |*attributes|
attributes.each do |attribute|
describe "specifically its :#{attribute} attribute" do
%w[
diff --git a/spec/support/shared_examples/models/slack_mattermost_notifications_shared_examples.rb b/spec/support/shared_examples/models/slack_mattermost_notifications_shared_examples.rb
index a1867e1ce39..71a76121d38 100644
--- a/spec/support/shared_examples/models/slack_mattermost_notifications_shared_examples.rb
+++ b/spec/support/shared_examples/models/slack_mattermost_notifications_shared_examples.rb
@@ -7,7 +7,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do |service_name|
let(:webhook_url) { 'https://example.gitlab.com' }
def execute_with_options(options)
- receive(:new).with(webhook_url, options.merge(http_client: SlackService::Notifier::HTTPClient))
+ receive(:new).with(webhook_url, options.merge(http_client: SlackMattermost::Notifier::HTTPClient))
.and_return(double(:slack_service).as_null_object)
end
@@ -66,193 +66,180 @@ RSpec.shared_examples 'slack or mattermost notifications' do |service_name|
end
describe "#execute" do
- let(:user) { create(:user) }
- let(:project) { create(:project, :repository, :wiki_repo) }
- let(:username) { 'slack_username' }
- let(:channel) { 'slack_channel' }
- let(:issue_service_options) { { title: 'Awesome issue', description: 'please fix' } }
+ let_it_be(:project) { create(:project, :repository, :wiki_repo) }
+ let_it_be(:user) { create(:user) }
- let(:data) do
- Gitlab::DataBuilder::Push.build_sample(project, user)
- end
+ let(:chat_service) { described_class.new( { project: project, webhook: webhook_url, branches_to_be_notified: 'all' }.merge(chat_service_params)) }
+ let(:chat_service_params) { {} }
+ let(:data) { Gitlab::DataBuilder::Push.build_sample(project, user) }
let!(:stubbed_resolved_hostname) do
stub_full_request(webhook_url, method: :post).request_pattern.uri_pattern.to_s
end
- before do
- allow(chat_service).to receive_messages(
- project: project,
- project_id: project.id,
- service_hook: true,
- webhook: webhook_url
- )
+ subject(:execute_service) { chat_service.execute(data) }
- issue_service = Issues::CreateService.new(project, user, issue_service_options)
- @issue = issue_service.execute
- @issues_sample_data = issue_service.hook_data(@issue, 'open')
-
- project.add_developer(user)
- opts = {
- title: 'Awesome merge_request',
- description: 'please fix',
- source_branch: 'feature',
- target_branch: 'master'
- }
- merge_service = MergeRequests::CreateService.new(project,
- user, opts)
- @merge_request = merge_service.execute
- @merge_sample_data = merge_service.hook_data(@merge_request,
- 'open')
-
- opts = {
- title: "Awesome wiki_page",
- content: "Some text describing some thing or another",
- format: "md",
- message: "user created page: Awesome wiki_page"
- }
-
- @wiki_page = create(:wiki_page, wiki: project.wiki, **opts)
- @wiki_page_sample_data = Gitlab::DataBuilder::WikiPage.build(@wiki_page, user, 'create')
- end
-
- it "calls #{service_name} API for push events" do
- chat_service.execute(data)
-
- expect(WebMock).to have_requested(:post, stubbed_resolved_hostname).once
- end
+ shared_examples 'calls the service API with the event message' do |event_message|
+ specify do
+ expect_next_instance_of(Slack::Messenger) do |messenger|
+ expect(messenger).to receive(:ping).with(event_message, anything).and_call_original
+ end
- it "calls #{service_name} API for issue events" do
- chat_service.execute(@issues_sample_data)
+ execute_service
- expect(WebMock).to have_requested(:post, stubbed_resolved_hostname).once
+ expect(WebMock).to have_requested(:post, stubbed_resolved_hostname).once
+ end
end
- it "calls #{service_name} API for merge requests events" do
- chat_service.execute(@merge_sample_data)
+ context 'with username for slack configured' do
+ let(:chat_service_params) { { username: 'slack_username' } }
+
+ it 'uses the username as an option' do
+ expect(Slack::Messenger).to execute_with_options(username: 'slack_username')
- expect(WebMock).to have_requested(:post, stubbed_resolved_hostname).once
+ execute_service
+ end
end
- it "calls #{service_name} API for wiki page events" do
- chat_service.execute(@wiki_page_sample_data)
+ context 'push events' do
+ let(:data) { Gitlab::DataBuilder::Push.build_sample(project, user) }
- expect(WebMock).to have_requested(:post, stubbed_resolved_hostname).once
- end
+ it_behaves_like 'calls the service API with the event message', /pushed to branch/
- it "calls #{service_name} API for deployment events" do
- deployment_event_data = { object_kind: 'deployment' }
+ context 'with event channel' do
+ let(:chat_service_params) { { push_channel: 'random' } }
- chat_service.execute(deployment_event_data)
+ it 'uses the right channel for push event' do
+ expect(Slack::Messenger).to execute_with_options(channel: ['random'])
- expect(WebMock).to have_requested(:post, stubbed_resolved_hostname).once
+ execute_service
+ end
+ end
end
- it 'uses the username as an option for slack when configured' do
- allow(chat_service).to receive(:username).and_return(username)
-
- expect(Slack::Messenger).to execute_with_options(username: username)
+ context 'tag_push events' do
+ let(:oldrev) { Gitlab::Git::BLANK_SHA }
+ let(:newrev) { '8a2a6eb295bb170b34c24c76c49ed0e9b2eaf34b' } # gitlab-test: git rev-parse refs/tags/v1.1.0
+ let(:ref) { 'refs/tags/v1.1.0' }
+ let(:data) { Git::TagHooksService.new(project, user, change: { oldrev: oldrev, newrev: newrev, ref: ref }).send(:push_data) }
- chat_service.execute(data)
+ it_behaves_like 'calls the service API with the event message', /pushed new tag/
end
- it 'uses the channel as an option when it is configured' do
- allow(chat_service).to receive(:channel).and_return(channel)
- expect(Slack::Messenger).to execute_with_options(channel: [channel])
- chat_service.execute(data)
- end
+ context 'issue events' do
+ let_it_be(:issue) { create(:issue) }
+ let(:data) { issue.to_hook_data(user) }
- context "event channels" do
- it "uses the right channel for push event" do
- chat_service.update!(push_channel: "random")
+ it_behaves_like 'calls the service API with the event message', /Issue (.*?) opened by/
- expect(Slack::Messenger).to execute_with_options(channel: ['random'])
+ context 'whith event channel' do
+ let(:chat_service_params) { { issue_channel: 'random' } }
- chat_service.execute(data)
- end
+ it 'uses the right channel for issue event' do
+ expect(Slack::Messenger).to execute_with_options(channel: ['random'])
- it "uses the right channel for merge request event" do
- chat_service.update!(merge_request_channel: "random")
+ execute_service
+ end
- expect(Slack::Messenger).to execute_with_options(channel: ['random'])
+ context 'for confidential issues' do
+ before_all do
+ issue.update!(confidential: true)
+ end
- chat_service.execute(@merge_sample_data)
- end
+ it 'falls back to issue channel' do
+ expect(Slack::Messenger).to execute_with_options(channel: ['random'])
+
+ execute_service
+ end
- it "uses the right channel for issue event" do
- chat_service.update!(issue_channel: "random")
+ context 'and confidential_issue_channel is defined' do
+ let(:chat_service_params) { { issue_channel: 'random', confidential_issue_channel: 'confidential' } }
- expect(Slack::Messenger).to execute_with_options(channel: ['random'])
+ it 'uses the confidential issue channel when it is defined' do
+ expect(Slack::Messenger).to execute_with_options(channel: ['confidential'])
- chat_service.execute(@issues_sample_data)
+ execute_service
+ end
+ end
+ end
end
+ end
+
+ context 'merge request events' do
+ let_it_be(:merge_request) { create(:merge_request) }
+ let(:data) { merge_request.to_hook_data(user) }
- context 'for confidential issues' do
- let(:issue_service_options) { { title: 'Secret', confidential: true } }
+ it_behaves_like 'calls the service API with the event message', /opened merge request/
- it "uses confidential issue channel" do
- chat_service.update!(confidential_issue_channel: 'confidential')
+ context 'with event channel' do
+ let(:chat_service_params) { { merge_request_channel: 'random' } }
- expect(Slack::Messenger).to execute_with_options(channel: ['confidential'])
+ it 'uses the right channel for merge request event' do
+ expect(Slack::Messenger).to execute_with_options(channel: ['random'])
- chat_service.execute(@issues_sample_data)
+ execute_service
end
+ end
+ end
+
+ context 'wiki page events' do
+ let_it_be(:wiki_page) { create(:wiki_page, wiki: project.wiki, message: 'user created page: Awesome wiki_page') }
+ let(:data) { Gitlab::DataBuilder::WikiPage.build(wiki_page, user, 'create') }
- it 'falls back to issue channel' do
- chat_service.update!(issue_channel: 'fallback_channel')
+ it_behaves_like 'calls the service API with the event message', / created (.*?)wikis\/(.*?)|wiki page> in/
- expect(Slack::Messenger).to execute_with_options(channel: ['fallback_channel'])
+ context 'with event channel' do
+ let(:chat_service_params) { { wiki_page_channel: 'random' } }
- chat_service.execute(@issues_sample_data)
+ it 'uses the right channel for wiki event' do
+ expect(Slack::Messenger).to execute_with_options(channel: ['random'])
+
+ execute_service
end
end
+ end
- it "uses the right channel for wiki event" do
- chat_service.update!(wiki_page_channel: "random")
-
- expect(Slack::Messenger).to execute_with_options(channel: ['random'])
+ context 'deployment events' do
+ let_it_be(:deployment) { create(:deployment) }
+ let(:data) { Gitlab::DataBuilder::Deployment.build(deployment) }
- chat_service.execute(@wiki_page_sample_data)
- end
+ it_behaves_like 'calls the service API with the event message', /Deploy to (.*?) created/
+ end
- context "note event" do
- let(:issue_note) do
- create(:note_on_issue, project: project, note: "issue note")
- end
+ context 'note event' do
+ let_it_be(:issue_note) { create(:note_on_issue, project: project, note: "issue note") }
+ let(:data) { Gitlab::DataBuilder::Note.build(issue_note, user) }
- it "uses the right channel" do
- chat_service.update!(note_channel: "random")
+ it_behaves_like 'calls the service API with the event message', /commented on issue/
- note_data = Gitlab::DataBuilder::Note.build(issue_note, user)
+ context 'with event channel' do
+ let(:chat_service_params) { { note_channel: 'random' } }
+ it 'uses the right channel' do
expect(Slack::Messenger).to execute_with_options(channel: ['random'])
- chat_service.execute(note_data)
+ execute_service
end
context 'for confidential notes' do
- before do
- issue_note.noteable.update!(confidential: true)
+ before_all do
+ issue_note.update!(confidential: true)
end
- it "uses confidential channel" do
- chat_service.update!(confidential_note_channel: "confidential")
-
- note_data = Gitlab::DataBuilder::Note.build(issue_note, user)
-
- expect(Slack::Messenger).to execute_with_options(channel: ['confidential'])
+ it 'falls back to note channel' do
+ expect(Slack::Messenger).to execute_with_options(channel: ['random'])
- chat_service.execute(note_data)
+ execute_service
end
- it 'falls back to note channel' do
- chat_service.update!(note_channel: "fallback_channel")
-
- note_data = Gitlab::DataBuilder::Note.build(issue_note, user)
+ context 'and confidential_note_channel is defined' do
+ let(:chat_service_params) { { note_channel: 'random', confidential_note_channel: 'confidential' } }
- expect(Slack::Messenger).to execute_with_options(channel: ['fallback_channel'])
+ it 'uses confidential channel' do
+ expect(Slack::Messenger).to execute_with_options(channel: ['confidential'])
- chat_service.execute(note_data)
+ execute_service
+ end
end
end
end
diff --git a/spec/support/shared_examples/models/wiki_shared_examples.rb b/spec/support/shared_examples/models/wiki_shared_examples.rb
index 89d30688b5c..abc6e3ecce8 100644
--- a/spec/support/shared_examples/models/wiki_shared_examples.rb
+++ b/spec/support/shared_examples/models/wiki_shared_examples.rb
@@ -354,27 +354,47 @@ RSpec.shared_examples 'wiki model' do
subject.repository.create_file(user, 'image.png', image, branch_name: subject.default_branch, message: 'add image')
end
- it 'returns the latest version of the file if it exists' do
- file = subject.find_file('image.png')
+ shared_examples 'find_file results' do
+ it 'returns the latest version of the file if it exists' do
+ file = subject.find_file('image.png')
- expect(file.mime_type).to eq('image/png')
- end
+ expect(file.mime_type).to eq('image/png')
+ end
+
+ it 'returns nil if the page does not exist' do
+ expect(subject.find_file('non-existent')).to eq(nil)
+ end
+
+ it 'returns a Gitlab::Git::WikiFile instance' do
+ file = subject.find_file('image.png')
+
+ expect(file).to be_a Gitlab::Git::WikiFile
+ end
- it 'returns nil if the page does not exist' do
- expect(subject.find_file('non-existent')).to eq(nil)
+ it 'returns the whole file' do
+ file = subject.find_file('image.png')
+ image.rewind
+
+ expect(file.raw_data.b).to eq(image.read.b)
+ end
end
- it 'returns a Gitlab::Git::WikiFile instance' do
- file = subject.find_file('image.png')
+ it_behaves_like 'find_file results'
+
+ context 'when load_content is disabled' do
+ it 'includes the file data in the Gitlab::Git::WikiFile' do
+ file = subject.find_file('image.png', load_content: false)
- expect(file).to be_a Gitlab::Git::WikiFile
+ expect(file.raw_data).to be_empty
+ end
end
- it 'returns the whole file' do
- file = subject.find_file('image.png')
- image.rewind
+ context 'when feature flag :gitaly_find_file is disabled' do
+ before do
+ stub_feature_flags(gitaly_find_file: false)
+ end
- expect(file.raw_data.b).to eq(image.read.b)
+ it_behaves_like 'find_file results'
end
end
diff --git a/spec/support/shared_examples/quick_actions/issuable/time_tracking_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/time_tracking_quick_action_shared_examples.rb
index 17fd2b836d3..92849ddf1cb 100644
--- a/spec/support/shared_examples/quick_actions/issuable/time_tracking_quick_action_shared_examples.rb
+++ b/spec/support/shared_examples/quick_actions/issuable/time_tracking_quick_action_shared_examples.rb
@@ -93,6 +93,6 @@ end
def submit_time(quick_action)
fill_in 'note[note]', with: quick_action
- find('.js-comment-submit-button').click
+ find('[data-testid="comment-button"]').click
wait_for_requests
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 49b6fc13900..54ea876bed2 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
@@ -1,63 +1,13 @@
# frozen_string_literal: true
RSpec.shared_examples 'conan ping endpoint' do
- it 'responds with 401 Unauthorized when no token provided' do
+ it 'responds with 200 OK when no token provided' do
get api(url)
- expect(response).to have_gitlab_http_status(:unauthorized)
- end
-
- it 'responds with 200 OK when valid token is provided' do
- jwt = build_jwt(personal_access_token)
- get api(url), headers: build_token_auth_header(jwt.encoded)
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response.headers['X-Conan-Server-Capabilities']).to eq("")
- end
-
- it 'responds with 200 OK when valid job token is provided' do
- jwt = build_jwt_from_job(job)
- get api(url), headers: build_token_auth_header(jwt.encoded)
-
expect(response).to have_gitlab_http_status(:ok)
expect(response.headers['X-Conan-Server-Capabilities']).to eq("")
end
- it 'responds with 200 OK when valid deploy token is provided' do
- jwt = build_jwt_from_deploy_token(deploy_token)
- get api(url), headers: build_token_auth_header(jwt.encoded)
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response.headers['X-Conan-Server-Capabilities']).to eq("")
- end
-
- it 'responds with 401 Unauthorized when invalid access token ID is provided' do
- jwt = build_jwt(double(id: 12345), user_id: personal_access_token.user_id)
- get api(url), headers: build_token_auth_header(jwt.encoded)
-
- expect(response).to have_gitlab_http_status(:unauthorized)
- end
-
- it 'responds with 401 Unauthorized when invalid user is provided' do
- jwt = build_jwt(personal_access_token, user_id: 12345)
- get api(url), headers: build_token_auth_header(jwt.encoded)
-
- expect(response).to have_gitlab_http_status(:unauthorized)
- end
-
- it 'responds with 401 Unauthorized when the provided JWT is signed with different secret' do
- jwt = build_jwt(personal_access_token, secret: SecureRandom.base64(32))
- get api(url), headers: build_token_auth_header(jwt.encoded)
-
- expect(response).to have_gitlab_http_status(:unauthorized)
- end
-
- it 'responds with 401 Unauthorized when invalid JWT is provided' do
- get api(url), headers: build_token_auth_header('invalid-jwt')
-
- expect(response).to have_gitlab_http_status(:unauthorized)
- end
-
context 'packages feature disabled' do
it 'responds with 404 Not Found' do
stub_packages_setting(enabled: false)
@@ -72,7 +22,10 @@ RSpec.shared_examples 'conan search endpoint' do
before do
project.update_column(:visibility_level, Gitlab::VisibilityLevel::PUBLIC)
- get api(url), headers: headers, params: params
+ # Do not pass the HTTP_AUTHORIZATION header,
+ # in order to test that this public project's packages
+ # are visible to anonymous search.
+ get api(url), params: params
end
subject { json_response['results'] }
@@ -109,6 +62,33 @@ RSpec.shared_examples 'conan authenticate endpoint' do
end
end
+ it 'responds with 401 Unauthorized when an invalid access token ID is provided' do
+ jwt = build_jwt(double(id: 12345), user_id: personal_access_token.user_id)
+ get api(url), headers: build_token_auth_header(jwt.encoded)
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+
+ it 'responds with 401 Unauthorized when invalid user is provided' do
+ jwt = build_jwt(personal_access_token, user_id: 12345)
+ get api(url), headers: build_token_auth_header(jwt.encoded)
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+
+ it 'responds with 401 Unauthorized when the provided JWT is signed with different secret' do
+ jwt = build_jwt(personal_access_token, secret: SecureRandom.base64(32))
+ get api(url), headers: build_token_auth_header(jwt.encoded)
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+
+ it 'responds with 401 UnauthorizedOK when invalid JWT is provided' do
+ get api(url), headers: build_token_auth_header('invalid-jwt')
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+
context 'when valid JWT access token is provided' do
it 'responds with 200' do
subject
@@ -507,19 +487,37 @@ RSpec.shared_examples 'delete package endpoint' do
end
end
+RSpec.shared_examples 'allows download with no token' do
+ context 'with no private token' do
+ let(:headers) { {} }
+
+ it 'returns 200' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+end
+
RSpec.shared_examples 'denies download with no token' do
context 'with no private token' do
let(:headers) { {} }
- it 'returns 400' do
+ it 'returns 404' do
subject
- expect(response).to have_gitlab_http_status(:unauthorized)
+ expect(response).to have_gitlab_http_status(:not_found)
end
end
end
RSpec.shared_examples 'a public project with packages' do
+ before do
+ project.update_column(:visibility_level, Gitlab::VisibilityLevel::PUBLIC)
+ end
+
+ it_behaves_like 'allows download with no token'
+
it 'returns the file' do
subject
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 54f4ba7ff73..274516cd87b 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
@@ -25,7 +25,7 @@ RSpec.shared_examples 'group and project boards query' do
board = create(:board, resource_parent: board_parent, name: 'A')
allow(Ability).to receive(:allowed?).and_call_original
- allow(Ability).to receive(:allowed?).with(user, :read_board, board).and_return(false)
+ allow(Ability).to receive(:allowed?).with(user, :read_issue_board, board).and_return(false)
post_graphql(query, current_user: current_user)
diff --git a/spec/support/shared_examples/requests/api/graphql/packages/group_and_project_packages_list_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/packages/group_and_project_packages_list_shared_examples.rb
new file mode 100644
index 00000000000..66fbfa798b0
--- /dev/null
+++ b/spec/support/shared_examples/requests/api/graphql/packages/group_and_project_packages_list_shared_examples.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'group and project packages query' do
+ include GraphqlHelpers
+
+ context 'when user has access to the resource' do
+ before do
+ resource.add_reporter(current_user)
+ post_graphql(query, current_user: current_user)
+ end
+
+ it_behaves_like 'a working graphql query'
+
+ it 'returns packages successfully' do
+ expect(package_names).to contain_exactly(
+ package.name,
+ maven_package.name,
+ debian_package.name,
+ composer_package.name
+ )
+ end
+
+ it 'deals with metadata' do
+ expect(target_shas).to contain_exactly(composer_metadatum.target_sha)
+ end
+ end
+
+ context 'when the user does not have access to the resource' do
+ before do
+ post_graphql(query, current_user: current_user)
+ end
+
+ it_behaves_like 'a working graphql query'
+
+ it 'returns nil' do
+ expect(packages).to be_nil
+ end
+ end
+
+ context 'when the user is not authenticated' do
+ before do
+ post_graphql(query)
+ end
+
+ it_behaves_like 'a working graphql query'
+
+ it 'returns nil' do
+ expect(packages).to be_nil
+ end
+ end
+end
diff --git a/spec/support/shared_examples/requests/api/logging_application_context_shared_examples.rb b/spec/support/shared_examples/requests/api/logging_application_context_shared_examples.rb
index 038ede884c8..4a71b696d57 100644
--- a/spec/support/shared_examples/requests/api/logging_application_context_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/logging_application_context_shared_examples.rb
@@ -22,3 +22,19 @@ RSpec.shared_examples 'storing arguments in the application context' do
hash.transform_keys! { |key| "meta.#{key}" }
end
end
+
+RSpec.shared_examples 'not executing any extra queries for the application context' do |expected_extra_queries = 0|
+ it 'does not execute more queries than without adding anything to the application context' do
+ # Call the subject once to memoize all factories being used for the spec, so they won't
+ # add any queries to the expectation.
+ subject_proc.call
+
+ expect do
+ allow(Gitlab::ApplicationContext).to receive(:push).and_call_original
+ subject_proc.call
+ end.to issue_same_number_of_queries_as {
+ allow(Gitlab::ApplicationContext).to receive(:push)
+ subject_proc.call
+ }.with_threshold(expected_extra_queries).ignoring_cached_queries
+ end
+end
diff --git a/spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb
index be051dcbb7b..c15c59e1a1d 100644
--- a/spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb
@@ -45,136 +45,234 @@ RSpec.shared_examples 'handling get metadata requests' do |scope: :project|
end
end
- where(:auth, :package_name_type, :request_forward, :visibility, :user_role, :expected_result, :expected_status) do
- nil | :scoped_naming_convention | true | 'PUBLIC' | nil | :accept | :ok
- nil | :scoped_naming_convention | false | 'PUBLIC' | nil | :accept | :ok
- nil | :non_existing | true | 'PUBLIC' | nil | :redirect | :redirected
- nil | :non_existing | false | 'PUBLIC' | nil | :reject | :not_found
- nil | :scoped_naming_convention | true | 'PRIVATE' | nil | :reject | :not_found
- nil | :scoped_naming_convention | false | 'PRIVATE' | nil | :reject | :not_found
- nil | :non_existing | true | 'PRIVATE' | nil | :redirect | :redirected
- nil | :non_existing | false | 'PRIVATE' | nil | :reject | :not_found
- nil | :scoped_naming_convention | true | 'INTERNAL' | nil | :reject | :not_found
- nil | :scoped_naming_convention | false | 'INTERNAL' | nil | :reject | :not_found
- nil | :non_existing | true | 'INTERNAL' | nil | :redirect | :redirected
- nil | :non_existing | false | 'INTERNAL' | nil | :reject | :not_found
-
- :oauth | :scoped_naming_convention | true | 'PUBLIC' | :guest | :accept | :ok
- :oauth | :scoped_naming_convention | true | 'PUBLIC' | :reporter | :accept | :ok
- :oauth | :scoped_naming_convention | false | 'PUBLIC' | :guest | :accept | :ok
- :oauth | :scoped_naming_convention | false | 'PUBLIC' | :reporter | :accept | :ok
- :oauth | :non_existing | true | 'PUBLIC' | :guest | :redirect | :redirected
- :oauth | :non_existing | true | 'PUBLIC' | :reporter | :redirect | :redirected
- :oauth | :non_existing | false | 'PUBLIC' | :guest | :reject | :not_found
- :oauth | :non_existing | false | 'PUBLIC' | :reporter | :reject | :not_found
- :oauth | :scoped_naming_convention | true | 'PRIVATE' | :guest | :reject | :forbidden
- :oauth | :scoped_naming_convention | true | 'PRIVATE' | :reporter | :accept | :ok
- :oauth | :scoped_naming_convention | false | 'PRIVATE' | :guest | :reject | :forbidden
- :oauth | :scoped_naming_convention | false | 'PRIVATE' | :reporter | :accept | :ok
- :oauth | :non_existing | true | 'PRIVATE' | :guest | :redirect | :redirected
- :oauth | :non_existing | true | 'PRIVATE' | :reporter | :redirect | :redirected
- :oauth | :non_existing | false | 'PRIVATE' | :guest | :reject | :forbidden
- :oauth | :non_existing | false | 'PRIVATE' | :reporter | :reject | :not_found
- :oauth | :scoped_naming_convention | true | 'INTERNAL' | :guest | :accept | :ok
- :oauth | :scoped_naming_convention | true | 'INTERNAL' | :reporter | :accept | :ok
- :oauth | :scoped_naming_convention | false | 'INTERNAL' | :guest | :accept | :ok
- :oauth | :scoped_naming_convention | false | 'INTERNAL' | :reporter | :accept | :ok
- :oauth | :non_existing | true | 'INTERNAL' | :guest | :redirect | :redirected
- :oauth | :non_existing | true | 'INTERNAL' | :reporter | :redirect | :redirected
- :oauth | :non_existing | false | 'INTERNAL' | :guest | :reject | :not_found
- :oauth | :non_existing | false | 'INTERNAL' | :reporter | :reject | :not_found
-
- :personal_access_token | :scoped_naming_convention | true | 'PUBLIC' | :guest | :accept | :ok
- :personal_access_token | :scoped_naming_convention | true | 'PUBLIC' | :reporter | :accept | :ok
- :personal_access_token | :scoped_naming_convention | false | 'PUBLIC' | :guest | :accept | :ok
- :personal_access_token | :scoped_naming_convention | false | 'PUBLIC' | :reporter | :accept | :ok
- :personal_access_token | :non_existing | true | 'PUBLIC' | :guest | :redirect | :redirected
- :personal_access_token | :non_existing | true | 'PUBLIC' | :reporter | :redirect | :redirected
- :personal_access_token | :non_existing | false | 'PUBLIC' | :guest | :reject | :not_found
- :personal_access_token | :non_existing | false | 'PUBLIC' | :reporter | :reject | :not_found
- :personal_access_token | :scoped_naming_convention | true | 'PRIVATE' | :guest | :reject | :forbidden
- :personal_access_token | :scoped_naming_convention | true | 'PRIVATE' | :reporter | :accept | :ok
- :personal_access_token | :scoped_naming_convention | false | 'PRIVATE' | :guest | :reject | :forbidden
- :personal_access_token | :scoped_naming_convention | false | 'PRIVATE' | :reporter | :accept | :ok
- :personal_access_token | :non_existing | true | 'PRIVATE' | :guest | :redirect | :redirected
- :personal_access_token | :non_existing | true | 'PRIVATE' | :reporter | :redirect | :redirected
- :personal_access_token | :non_existing | false | 'PRIVATE' | :guest | :reject | :forbidden
- :personal_access_token | :non_existing | false | 'PRIVATE' | :reporter | :reject | :not_found
- :personal_access_token | :scoped_naming_convention | true | 'INTERNAL' | :guest | :accept | :ok
- :personal_access_token | :scoped_naming_convention | true | 'INTERNAL' | :reporter | :accept | :ok
- :personal_access_token | :scoped_naming_convention | false | 'INTERNAL' | :guest | :accept | :ok
- :personal_access_token | :scoped_naming_convention | false | 'INTERNAL' | :reporter | :accept | :ok
- :personal_access_token | :non_existing | true | 'INTERNAL' | :guest | :redirect | :redirected
- :personal_access_token | :non_existing | true | 'INTERNAL' | :reporter | :redirect | :redirected
- :personal_access_token | :non_existing | false | 'INTERNAL' | :guest | :reject | :not_found
- :personal_access_token | :non_existing | false | 'INTERNAL' | :reporter | :reject | :not_found
-
- :job_token | :scoped_naming_convention | true | 'PUBLIC' | :developer | :accept | :ok
- :job_token | :scoped_naming_convention | false | 'PUBLIC' | :developer | :accept | :ok
- :job_token | :non_existing | true | 'PUBLIC' | :developer | :redirect | :redirected
- :job_token | :non_existing | false | 'PUBLIC' | :developer | :reject | :not_found
- :job_token | :scoped_naming_convention | true | 'PRIVATE' | :developer | :accept | :ok
- :job_token | :scoped_naming_convention | false | 'PRIVATE' | :developer | :accept | :ok
- :job_token | :non_existing | true | 'PRIVATE' | :developer | :redirect | :redirected
- :job_token | :non_existing | false | 'PRIVATE' | :developer | :reject | :not_found
- :job_token | :scoped_naming_convention | true | 'INTERNAL' | :developer | :accept | :ok
- :job_token | :scoped_naming_convention | false | 'INTERNAL' | :developer | :accept | :ok
- :job_token | :non_existing | true | 'INTERNAL' | :developer | :redirect | :redirected
- :job_token | :non_existing | false | 'INTERNAL' | :developer | :reject | :not_found
-
- :deploy_token | :scoped_naming_convention | true | 'PUBLIC' | nil | :accept | :ok
- :deploy_token | :scoped_naming_convention | false | 'PUBLIC' | nil | :accept | :ok
- :deploy_token | :non_existing | true | 'PUBLIC' | nil | :redirect | :redirected
- :deploy_token | :non_existing | false | 'PUBLIC' | nil | :reject | :not_found
- :deploy_token | :scoped_naming_convention | true | 'PRIVATE' | nil | :accept | :ok
- :deploy_token | :scoped_naming_convention | false | 'PRIVATE' | nil | :accept | :ok
- :deploy_token | :non_existing | true | 'PRIVATE' | nil | :redirect | :redirected
- :deploy_token | :non_existing | false | 'PRIVATE' | nil | :reject | :not_found
- :deploy_token | :scoped_naming_convention | true | 'INTERNAL' | nil | :accept | :ok
- :deploy_token | :scoped_naming_convention | false | 'INTERNAL' | nil | :accept | :ok
- :deploy_token | :non_existing | true | 'INTERNAL' | nil | :redirect | :redirected
- :deploy_token | :non_existing | false | 'INTERNAL' | nil | :reject | :not_found
- end
+ shared_examples 'handling all conditions' do
+ where(:auth, :package_name_type, :request_forward, :visibility, :user_role, :expected_result, :expected_status) do
+ nil | :scoped_naming_convention | true | :public | nil | :accept | :ok
+ nil | :scoped_naming_convention | false | :public | nil | :accept | :ok
+ nil | :scoped_no_naming_convention | true | :public | nil | :accept | :ok
+ nil | :scoped_no_naming_convention | false | :public | nil | :accept | :ok
+ nil | :unscoped | true | :public | nil | :accept | :ok
+ nil | :unscoped | false | :public | nil | :accept | :ok
+ nil | :non_existing | true | :public | nil | :redirect | :redirected
+ nil | :non_existing | false | :public | nil | :reject | :not_found
+ nil | :scoped_naming_convention | true | :private | nil | :reject | :not_found
+ nil | :scoped_naming_convention | false | :private | nil | :reject | :not_found
+ nil | :scoped_no_naming_convention | true | :private | nil | :reject | :not_found
+ nil | :scoped_no_naming_convention | false | :private | nil | :reject | :not_found
+ nil | :unscoped | true | :private | nil | :reject | :not_found
+ nil | :unscoped | false | :private | nil | :reject | :not_found
+ nil | :non_existing | true | :private | nil | :redirect | :redirected
+ nil | :non_existing | false | :private | nil | :reject | :not_found
+ nil | :scoped_naming_convention | true | :internal | nil | :reject | :not_found
+ nil | :scoped_naming_convention | false | :internal | nil | :reject | :not_found
+ nil | :scoped_no_naming_convention | true | :internal | nil | :reject | :not_found
+ nil | :scoped_no_naming_convention | false | :internal | nil | :reject | :not_found
+ nil | :unscoped | true | :internal | nil | :reject | :not_found
+ nil | :unscoped | false | :internal | nil | :reject | :not_found
+ nil | :non_existing | true | :internal | nil | :redirect | :redirected
+ nil | :non_existing | false | :internal | nil | :reject | :not_found
+
+ :oauth | :scoped_naming_convention | true | :public | :guest | :accept | :ok
+ :oauth | :scoped_naming_convention | true | :public | :reporter | :accept | :ok
+ :oauth | :scoped_naming_convention | false | :public | :guest | :accept | :ok
+ :oauth | :scoped_naming_convention | false | :public | :reporter | :accept | :ok
+ :oauth | :scoped_no_naming_convention | true | :public | :guest | :accept | :ok
+ :oauth | :scoped_no_naming_convention | true | :public | :reporter | :accept | :ok
+ :oauth | :scoped_no_naming_convention | false | :public | :guest | :accept | :ok
+ :oauth | :scoped_no_naming_convention | false | :public | :reporter | :accept | :ok
+ :oauth | :unscoped | true | :public | :guest | :accept | :ok
+ :oauth | :unscoped | true | :public | :reporter | :accept | :ok
+ :oauth | :unscoped | false | :public | :guest | :accept | :ok
+ :oauth | :unscoped | false | :public | :reporter | :accept | :ok
+ :oauth | :non_existing | true | :public | :guest | :redirect | :redirected
+ :oauth | :non_existing | true | :public | :reporter | :redirect | :redirected
+ :oauth | :non_existing | false | :public | :guest | :reject | :not_found
+ :oauth | :non_existing | false | :public | :reporter | :reject | :not_found
+ :oauth | :scoped_naming_convention | true | :private | :guest | :reject | :forbidden
+ :oauth | :scoped_naming_convention | true | :private | :reporter | :accept | :ok
+ :oauth | :scoped_naming_convention | false | :private | :guest | :reject | :forbidden
+ :oauth | :scoped_naming_convention | false | :private | :reporter | :accept | :ok
+ :oauth | :scoped_no_naming_convention | true | :private | :guest | :reject | :forbidden
+ :oauth | :scoped_no_naming_convention | true | :private | :reporter | :accept | :ok
+ :oauth | :scoped_no_naming_convention | false | :private | :guest | :reject | :forbidden
+ :oauth | :scoped_no_naming_convention | false | :private | :reporter | :accept | :ok
+ :oauth | :unscoped | true | :private | :guest | :reject | :forbidden
+ :oauth | :unscoped | true | :private | :reporter | :accept | :ok
+ :oauth | :unscoped | false | :private | :guest | :reject | :forbidden
+ :oauth | :unscoped | false | :private | :reporter | :accept | :ok
+ :oauth | :non_existing | true | :private | :guest | :redirect | :redirected
+ :oauth | :non_existing | true | :private | :reporter | :redirect | :redirected
+ :oauth | :non_existing | false | :private | :guest | :reject | :forbidden
+ :oauth | :non_existing | false | :private | :reporter | :reject | :not_found
+ :oauth | :scoped_naming_convention | true | :internal | :guest | :accept | :ok
+ :oauth | :scoped_naming_convention | true | :internal | :reporter | :accept | :ok
+ :oauth | :scoped_naming_convention | false | :internal | :guest | :accept | :ok
+ :oauth | :scoped_naming_convention | false | :internal | :reporter | :accept | :ok
+ :oauth | :scoped_no_naming_convention | true | :internal | :guest | :accept | :ok
+ :oauth | :scoped_no_naming_convention | true | :internal | :reporter | :accept | :ok
+ :oauth | :scoped_no_naming_convention | false | :internal | :guest | :accept | :ok
+ :oauth | :scoped_no_naming_convention | false | :internal | :reporter | :accept | :ok
+ :oauth | :unscoped | true | :internal | :guest | :accept | :ok
+ :oauth | :unscoped | true | :internal | :reporter | :accept | :ok
+ :oauth | :unscoped | false | :internal | :guest | :accept | :ok
+ :oauth | :unscoped | false | :internal | :reporter | :accept | :ok
+ :oauth | :non_existing | true | :internal | :guest | :redirect | :redirected
+ :oauth | :non_existing | true | :internal | :reporter | :redirect | :redirected
+ :oauth | :non_existing | false | :internal | :guest | :reject | :not_found
+ :oauth | :non_existing | false | :internal | :reporter | :reject | :not_found
+
+ :personal_access_token | :scoped_naming_convention | true | :public | :guest | :accept | :ok
+ :personal_access_token | :scoped_naming_convention | true | :public | :reporter | :accept | :ok
+ :personal_access_token | :scoped_naming_convention | false | :public | :guest | :accept | :ok
+ :personal_access_token | :scoped_naming_convention | false | :public | :reporter | :accept | :ok
+ :personal_access_token | :scoped_no_naming_convention | true | :public | :guest | :accept | :ok
+ :personal_access_token | :scoped_no_naming_convention | true | :public | :reporter | :accept | :ok
+ :personal_access_token | :scoped_no_naming_convention | false | :public | :guest | :accept | :ok
+ :personal_access_token | :scoped_no_naming_convention | false | :public | :reporter | :accept | :ok
+ :personal_access_token | :unscoped | true | :public | :guest | :accept | :ok
+ :personal_access_token | :unscoped | true | :public | :reporter | :accept | :ok
+ :personal_access_token | :unscoped | false | :public | :guest | :accept | :ok
+ :personal_access_token | :unscoped | false | :public | :reporter | :accept | :ok
+ :personal_access_token | :non_existing | true | :public | :guest | :redirect | :redirected
+ :personal_access_token | :non_existing | true | :public | :reporter | :redirect | :redirected
+ :personal_access_token | :non_existing | false | :public | :guest | :reject | :not_found
+ :personal_access_token | :non_existing | false | :public | :reporter | :reject | :not_found
+ :personal_access_token | :scoped_naming_convention | true | :private | :guest | :reject | :forbidden
+ :personal_access_token | :scoped_naming_convention | true | :private | :reporter | :accept | :ok
+ :personal_access_token | :scoped_naming_convention | false | :private | :guest | :reject | :forbidden
+ :personal_access_token | :scoped_naming_convention | false | :private | :reporter | :accept | :ok
+ :personal_access_token | :scoped_no_naming_convention | true | :private | :guest | :reject | :forbidden
+ :personal_access_token | :scoped_no_naming_convention | true | :private | :reporter | :accept | :ok
+ :personal_access_token | :scoped_no_naming_convention | false | :private | :guest | :reject | :forbidden
+ :personal_access_token | :scoped_no_naming_convention | false | :private | :reporter | :accept | :ok
+ :personal_access_token | :unscoped | true | :private | :guest | :reject | :forbidden
+ :personal_access_token | :unscoped | true | :private | :reporter | :accept | :ok
+ :personal_access_token | :unscoped | false | :private | :guest | :reject | :forbidden
+ :personal_access_token | :unscoped | false | :private | :reporter | :accept | :ok
+ :personal_access_token | :non_existing | true | :private | :guest | :redirect | :redirected
+ :personal_access_token | :non_existing | true | :private | :reporter | :redirect | :redirected
+ :personal_access_token | :non_existing | false | :private | :guest | :reject | :forbidden
+ :personal_access_token | :non_existing | false | :private | :reporter | :reject | :not_found
+ :personal_access_token | :scoped_naming_convention | true | :internal | :guest | :accept | :ok
+ :personal_access_token | :scoped_naming_convention | true | :internal | :reporter | :accept | :ok
+ :personal_access_token | :scoped_naming_convention | false | :internal | :guest | :accept | :ok
+ :personal_access_token | :scoped_naming_convention | false | :internal | :reporter | :accept | :ok
+ :personal_access_token | :scoped_no_naming_convention | true | :internal | :guest | :accept | :ok
+ :personal_access_token | :scoped_no_naming_convention | true | :internal | :reporter | :accept | :ok
+ :personal_access_token | :scoped_no_naming_convention | false | :internal | :guest | :accept | :ok
+ :personal_access_token | :scoped_no_naming_convention | false | :internal | :reporter | :accept | :ok
+ :personal_access_token | :unscoped | true | :internal | :guest | :accept | :ok
+ :personal_access_token | :unscoped | true | :internal | :reporter | :accept | :ok
+ :personal_access_token | :unscoped | false | :internal | :guest | :accept | :ok
+ :personal_access_token | :unscoped | false | :internal | :reporter | :accept | :ok
+ :personal_access_token | :non_existing | true | :internal | :guest | :redirect | :redirected
+ :personal_access_token | :non_existing | true | :internal | :reporter | :redirect | :redirected
+ :personal_access_token | :non_existing | false | :internal | :guest | :reject | :not_found
+ :personal_access_token | :non_existing | false | :internal | :reporter | :reject | :not_found
+
+ :job_token | :scoped_naming_convention | true | :public | :developer | :accept | :ok
+ :job_token | :scoped_naming_convention | false | :public | :developer | :accept | :ok
+ :job_token | :scoped_no_naming_convention | true | :public | :developer | :accept | :ok
+ :job_token | :scoped_no_naming_convention | false | :public | :developer | :accept | :ok
+ :job_token | :unscoped | true | :public | :developer | :accept | :ok
+ :job_token | :unscoped | false | :public | :developer | :accept | :ok
+ :job_token | :non_existing | true | :public | :developer | :redirect | :redirected
+ :job_token | :non_existing | false | :public | :developer | :reject | :not_found
+ :job_token | :scoped_naming_convention | true | :private | :developer | :accept | :ok
+ :job_token | :scoped_naming_convention | false | :private | :developer | :accept | :ok
+ :job_token | :scoped_no_naming_convention | true | :private | :developer | :accept | :ok
+ :job_token | :scoped_no_naming_convention | false | :private | :developer | :accept | :ok
+ :job_token | :unscoped | true | :private | :developer | :accept | :ok
+ :job_token | :unscoped | false | :private | :developer | :accept | :ok
+ :job_token | :non_existing | true | :private | :developer | :redirect | :redirected
+ :job_token | :non_existing | false | :private | :developer | :reject | :not_found
+ :job_token | :scoped_naming_convention | true | :internal | :developer | :accept | :ok
+ :job_token | :scoped_naming_convention | false | :internal | :developer | :accept | :ok
+ :job_token | :scoped_no_naming_convention | true | :internal | :developer | :accept | :ok
+ :job_token | :scoped_no_naming_convention | false | :internal | :developer | :accept | :ok
+ :job_token | :unscoped | true | :internal | :developer | :accept | :ok
+ :job_token | :unscoped | false | :internal | :developer | :accept | :ok
+ :job_token | :non_existing | true | :internal | :developer | :redirect | :redirected
+ :job_token | :non_existing | false | :internal | :developer | :reject | :not_found
+
+ :deploy_token | :scoped_naming_convention | true | :public | nil | :accept | :ok
+ :deploy_token | :scoped_naming_convention | false | :public | nil | :accept | :ok
+ :deploy_token | :scoped_no_naming_convention | true | :public | nil | :accept | :ok
+ :deploy_token | :scoped_no_naming_convention | false | :public | nil | :accept | :ok
+ :deploy_token | :unscoped | true | :public | nil | :accept | :ok
+ :deploy_token | :unscoped | false | :public | nil | :accept | :ok
+ :deploy_token | :non_existing | true | :public | nil | :redirect | :redirected
+ :deploy_token | :non_existing | false | :public | nil | :reject | :not_found
+ :deploy_token | :scoped_naming_convention | true | :private | nil | :accept | :ok
+ :deploy_token | :scoped_naming_convention | false | :private | nil | :accept | :ok
+ :deploy_token | :scoped_no_naming_convention | true | :private | nil | :accept | :ok
+ :deploy_token | :scoped_no_naming_convention | false | :private | nil | :accept | :ok
+ :deploy_token | :unscoped | true | :private | nil | :accept | :ok
+ :deploy_token | :unscoped | false | :private | nil | :accept | :ok
+ :deploy_token | :non_existing | true | :private | nil | :redirect | :redirected
+ :deploy_token | :non_existing | false | :private | nil | :reject | :not_found
+ :deploy_token | :scoped_naming_convention | true | :internal | nil | :accept | :ok
+ :deploy_token | :scoped_naming_convention | false | :internal | nil | :accept | :ok
+ :deploy_token | :scoped_no_naming_convention | true | :internal | nil | :accept | :ok
+ :deploy_token | :scoped_no_naming_convention | false | :internal | nil | :accept | :ok
+ :deploy_token | :unscoped | true | :internal | nil | :accept | :ok
+ :deploy_token | :unscoped | false | :internal | nil | :accept | :ok
+ :deploy_token | :non_existing | true | :internal | nil | :redirect | :redirected
+ :deploy_token | :non_existing | false | :internal | nil | :reject | :not_found
+ end
- with_them do
- include_context 'set package name from package name type'
-
- let(:headers) do
- case auth
- when :oauth
- build_token_auth_header(token.token)
- when :personal_access_token
- build_token_auth_header(personal_access_token.token)
- when :job_token
- build_token_auth_header(job.token)
- when :deploy_token
- build_token_auth_header(deploy_token.token)
- else
- {}
+ with_them do
+ include_context 'set package name from package name type'
+
+ let(:headers) do
+ case auth
+ when :oauth
+ build_token_auth_header(token.token)
+ when :personal_access_token
+ build_token_auth_header(personal_access_token.token)
+ when :job_token
+ build_token_auth_header(job.token)
+ when :deploy_token
+ build_token_auth_header(deploy_token.token)
+ else
+ {}
+ end
end
- end
- before do
- project.send("add_#{user_role}", user) if user_role
- project.update!(visibility: Gitlab::VisibilityLevel.const_get(visibility, false))
- package.update!(name: package_name) unless package_name == 'non-existing-package'
- stub_application_setting(npm_package_requests_forwarding: request_forward)
- end
+ before do
+ project.send("add_#{user_role}", user) if user_role
+ project.update!(visibility: visibility.to_s)
+ package.update!(name: package_name) unless package_name == 'non-existing-package'
+ stub_application_setting(npm_package_requests_forwarding: request_forward)
+ end
- example_name = "#{params[:expected_result]} metadata request"
- status = params[:expected_status]
+ example_name = "#{params[:expected_result]} metadata request"
+ status = params[:expected_status]
- if scope == :instance && params[:package_name_type] != :scoped_naming_convention
- if params[:request_forward]
- example_name = 'redirect metadata request'
- status = :redirected
- else
- example_name = 'reject metadata request'
- status = :not_found
+ if scope == :instance && params[:package_name_type] != :scoped_naming_convention
+ if params[:request_forward]
+ example_name = 'redirect metadata request'
+ status = :redirected
+ else
+ example_name = 'reject metadata request'
+ status = :not_found
+ end
end
+
+ it_behaves_like example_name, status: status
end
+ end
- it_behaves_like example_name, status: status
+ context 'with a group namespace' do
+ it_behaves_like 'handling all conditions'
+ end
+
+ if scope != :project
+ context 'with a user namespace' do
+ let_it_be(:namespace) { user.namespace }
+
+ it_behaves_like 'handling all conditions'
+ end
end
context 'with a developer' do
@@ -225,26 +323,44 @@ RSpec.shared_examples 'handling get dist tags requests' do |scope: :project|
shared_examples 'handling different package names, visibilities and user roles' do
where(:package_name_type, :visibility, :user_role, :expected_result, :expected_status) do
- :scoped_naming_convention | 'PUBLIC' | :anonymous | :accept | :ok
- :scoped_naming_convention | 'PUBLIC' | :guest | :accept | :ok
- :scoped_naming_convention | 'PUBLIC' | :reporter | :accept | :ok
- :non_existing | 'PUBLIC' | :anonymous | :reject | :not_found
- :non_existing | 'PUBLIC' | :guest | :reject | :not_found
- :non_existing | 'PUBLIC' | :reporter | :reject | :not_found
-
- :scoped_naming_convention | 'PRIVATE' | :anonymous | :reject | :not_found
- :scoped_naming_convention | 'PRIVATE' | :guest | :reject | :forbidden
- :scoped_naming_convention | 'PRIVATE' | :reporter | :accept | :ok
- :non_existing | 'PRIVATE' | :anonymous | :reject | :not_found
- :non_existing | 'PRIVATE' | :guest | :reject | :forbidden
- :non_existing | 'PRIVATE' | :reporter | :reject | :not_found
-
- :scoped_naming_convention | 'INTERNAL' | :anonymous | :reject | :not_found
- :scoped_naming_convention | 'INTERNAL' | :guest | :accept | :ok
- :scoped_naming_convention | 'INTERNAL' | :reporter | :accept | :ok
- :non_existing | 'INTERNAL' | :anonymous | :reject | :not_found
- :non_existing | 'INTERNAL' | :guest | :reject | :not_found
- :non_existing | 'INTERNAL' | :reporter | :reject | :not_found
+ :scoped_naming_convention | :public | :anonymous | :accept | :ok
+ :scoped_naming_convention | :public | :guest | :accept | :ok
+ :scoped_naming_convention | :public | :reporter | :accept | :ok
+ :scoped_no_naming_convention | :public | :anonymous | :accept | :ok
+ :scoped_no_naming_convention | :public | :guest | :accept | :ok
+ :scoped_no_naming_convention | :public | :reporter | :accept | :ok
+ :unscoped | :public | :anonymous | :accept | :ok
+ :unscoped | :public | :guest | :accept | :ok
+ :unscoped | :public | :reporter | :accept | :ok
+ :non_existing | :public | :anonymous | :reject | :not_found
+ :non_existing | :public | :guest | :reject | :not_found
+ :non_existing | :public | :reporter | :reject | :not_found
+
+ :scoped_naming_convention | :private | :anonymous | :reject | :not_found
+ :scoped_naming_convention | :private | :guest | :reject | :forbidden
+ :scoped_naming_convention | :private | :reporter | :accept | :ok
+ :scoped_no_naming_convention | :private | :anonymous | :reject | :not_found
+ :scoped_no_naming_convention | :private | :guest | :reject | :forbidden
+ :scoped_no_naming_convention | :private | :reporter | :accept | :ok
+ :unscoped | :private | :anonymous | :reject | :not_found
+ :unscoped | :private | :guest | :reject | :forbidden
+ :unscoped | :private | :reporter | :accept | :ok
+ :non_existing | :private | :anonymous | :reject | :not_found
+ :non_existing | :private | :guest | :reject | :forbidden
+ :non_existing | :private | :reporter | :reject | :not_found
+
+ :scoped_naming_convention | :internal | :anonymous | :reject | :not_found
+ :scoped_naming_convention | :internal | :guest | :accept | :ok
+ :scoped_naming_convention | :internal | :reporter | :accept | :ok
+ :scoped_no_naming_convention | :internal | :anonymous | :reject | :not_found
+ :scoped_no_naming_convention | :internal | :guest | :accept | :ok
+ :scoped_no_naming_convention | :internal | :reporter | :accept | :ok
+ :unscoped | :internal | :anonymous | :reject | :not_found
+ :unscoped | :internal | :guest | :accept | :ok
+ :unscoped | :internal | :reporter | :accept | :ok
+ :non_existing | :internal | :anonymous | :reject | :not_found
+ :non_existing | :internal | :guest | :reject | :not_found
+ :non_existing | :internal | :reporter | :reject | :not_found
end
with_them do
@@ -254,7 +370,7 @@ RSpec.shared_examples 'handling get dist tags requests' do |scope: :project|
before do
project.send("add_#{user_role}", user) unless anonymous
- project.update!(visibility: Gitlab::VisibilityLevel.const_get(visibility, false))
+ project.update!(visibility: visibility.to_s)
end
example_name = "#{params[:expected_result]} package tags request"
@@ -269,16 +385,30 @@ RSpec.shared_examples 'handling get dist tags requests' do |scope: :project|
end
end
- context 'with oauth token' do
- let(:headers) { build_token_auth_header(token.token) }
+ shared_examples 'handling all conditions' do
+ context 'with oauth token' do
+ let(:headers) { build_token_auth_header(token.token) }
+
+ it_behaves_like 'handling different package names, visibilities and user roles'
+ end
+
+ context 'with personal access token' do
+ let(:headers) { build_token_auth_header(personal_access_token.token) }
- it_behaves_like 'handling different package names, visibilities and user roles'
+ it_behaves_like 'handling different package names, visibilities and user roles'
+ end
end
- context 'with personal access token' do
- let(:headers) { build_token_auth_header(personal_access_token.token) }
+ context 'with a group namespace' do
+ it_behaves_like 'handling all conditions'
+ end
- it_behaves_like 'handling different package names, visibilities and user roles'
+ if scope != :project
+ context 'with a user namespace' do
+ let_it_be(:namespace) { user.namespace }
+
+ it_behaves_like 'handling all conditions'
+ end
end
end
@@ -303,26 +433,44 @@ RSpec.shared_examples 'handling create dist tag requests' do |scope: :project|
shared_examples 'handling different package names, visibilities and user roles' do
where(:package_name_type, :visibility, :user_role, :expected_result, :expected_status) do
- :scoped_naming_convention | 'PUBLIC' | :anonymous | :reject | :forbidden
- :scoped_naming_convention | 'PUBLIC' | :guest | :reject | :forbidden
- :scoped_naming_convention | 'PUBLIC' | :developer | :accept | :ok
- :non_existing | 'PUBLIC' | :anonymous | :reject | :forbidden
- :non_existing | 'PUBLIC' | :guest | :reject | :forbidden
- :non_existing | 'PUBLIC' | :developer | :reject | :not_found
-
- :scoped_naming_convention | 'PRIVATE' | :anonymous | :reject | :not_found
- :scoped_naming_convention | 'PRIVATE' | :guest | :reject | :forbidden
- :scoped_naming_convention | 'PRIVATE' | :developer | :accept | :ok
- :non_existing | 'PRIVATE' | :anonymous | :reject | :not_found
- :non_existing | 'PRIVATE' | :guest | :reject | :forbidden
- :non_existing | 'PRIVATE' | :developer | :reject | :not_found
-
- :scoped_naming_convention | 'INTERNAL' | :anonymous | :reject | :forbidden
- :scoped_naming_convention | 'INTERNAL' | :guest | :reject | :forbidden
- :scoped_naming_convention | 'INTERNAL' | :developer | :accept | :ok
- :non_existing | 'INTERNAL' | :anonymous | :reject | :forbidden
- :non_existing | 'INTERNAL' | :guest | :reject | :forbidden
- :non_existing | 'INTERNAL' | :developer | :reject | :not_found
+ :scoped_naming_convention | :public | :anonymous | :reject | :forbidden
+ :scoped_naming_convention | :public | :guest | :reject | :forbidden
+ :scoped_naming_convention | :public | :developer | :accept | :ok
+ :scoped_no_naming_convention | :public | :anonymous | :reject | :forbidden
+ :scoped_no_naming_convention | :public | :guest | :reject | :forbidden
+ :scoped_no_naming_convention | :public | :developer | :accept | :ok
+ :unscoped | :public | :anonymous | :reject | :forbidden
+ :unscoped | :public | :guest | :reject | :forbidden
+ :unscoped | :public | :developer | :accept | :ok
+ :non_existing | :public | :anonymous | :reject | :forbidden
+ :non_existing | :public | :guest | :reject | :forbidden
+ :non_existing | :public | :developer | :reject | :not_found
+
+ :scoped_naming_convention | :private | :anonymous | :reject | :not_found
+ :scoped_naming_convention | :private | :guest | :reject | :forbidden
+ :scoped_naming_convention | :private | :developer | :accept | :ok
+ :scoped_no_naming_convention | :private | :anonymous | :reject | :not_found
+ :scoped_no_naming_convention | :private | :guest | :reject | :forbidden
+ :scoped_no_naming_convention | :private | :developer | :accept | :ok
+ :unscoped | :private | :anonymous | :reject | :not_found
+ :unscoped | :private | :guest | :reject | :forbidden
+ :unscoped | :private | :developer | :accept | :ok
+ :non_existing | :private | :anonymous | :reject | :not_found
+ :non_existing | :private | :guest | :reject | :forbidden
+ :non_existing | :private | :developer | :reject | :not_found
+
+ :scoped_naming_convention | :internal | :anonymous | :reject | :forbidden
+ :scoped_naming_convention | :internal | :guest | :reject | :forbidden
+ :scoped_naming_convention | :internal | :developer | :accept | :ok
+ :scoped_no_naming_convention | :internal | :anonymous | :reject | :forbidden
+ :scoped_no_naming_convention | :internal | :guest | :reject | :forbidden
+ :scoped_no_naming_convention | :internal | :developer | :accept | :ok
+ :unscoped | :internal | :anonymous | :reject | :forbidden
+ :unscoped | :internal | :guest | :reject | :forbidden
+ :unscoped | :internal | :developer | :accept | :ok
+ :non_existing | :internal | :anonymous | :reject | :forbidden
+ :non_existing | :internal | :guest | :reject | :forbidden
+ :non_existing | :internal | :developer | :reject | :not_found
end
with_them do
@@ -332,7 +480,7 @@ RSpec.shared_examples 'handling create dist tag requests' do |scope: :project|
before do
project.send("add_#{user_role}", user) unless anonymous
- project.update!(visibility: Gitlab::VisibilityLevel.const_get(visibility, false))
+ project.update!(visibility: visibility.to_s)
end
example_name = "#{params[:expected_result]} create package tag request"
@@ -347,16 +495,30 @@ RSpec.shared_examples 'handling create dist tag requests' do |scope: :project|
end
end
- context 'with oauth token' do
- let(:headers) { build_token_auth_header(token.token) }
+ shared_examples 'handling all conditions' do
+ context 'with oauth token' do
+ let(:headers) { build_token_auth_header(token.token) }
- it_behaves_like 'handling different package names, visibilities and user roles'
+ it_behaves_like 'handling different package names, visibilities and user roles'
+ end
+
+ context 'with personal access token' do
+ let(:headers) { build_token_auth_header(personal_access_token.token) }
+
+ it_behaves_like 'handling different package names, visibilities and user roles'
+ end
end
- context 'with personal access token' do
- let(:headers) { build_token_auth_header(personal_access_token.token) }
+ context 'with a group namespace' do
+ it_behaves_like 'handling all conditions'
+ end
- it_behaves_like 'handling different package names, visibilities and user roles'
+ if scope != :project
+ context 'with a user namespace' do
+ let_it_be(:namespace) { user.namespace }
+
+ it_behaves_like 'handling all conditions'
+ end
end
end
@@ -379,19 +541,44 @@ RSpec.shared_examples 'handling delete dist tag requests' do |scope: :project|
shared_examples 'handling different package names, visibilities and user roles' do
where(:package_name_type, :visibility, :user_role, :expected_result, :expected_status) do
- :scoped_naming_convention | 'PUBLIC' | :anonymous | :reject | :forbidden
- :scoped_naming_convention | 'PUBLIC' | :guest | :reject | :forbidden
- :scoped_naming_convention | 'PUBLIC' | :maintainer | :accept | :ok
- :non_existing | 'PUBLIC' | :anonymous | :reject | :forbidden
- :non_existing | 'PUBLIC' | :guest | :reject | :forbidden
- :non_existing | 'PUBLIC' | :maintainer | :reject | :not_found
-
- :scoped_naming_convention | 'PRIVATE' | :anonymous | :reject | :not_found
- :scoped_naming_convention | 'PRIVATE' | :guest | :reject | :forbidden
- :scoped_naming_convention | 'PRIVATE' | :maintainer | :accept | :ok
- :non_existing | 'INTERNAL' | :anonymous | :reject | :forbidden
- :non_existing | 'INTERNAL' | :guest | :reject | :forbidden
- :non_existing | 'INTERNAL' | :maintainer | :reject | :not_found
+ :scoped_naming_convention | :public | :anonymous | :reject | :forbidden
+ :scoped_naming_convention | :public | :guest | :reject | :forbidden
+ :scoped_naming_convention | :public | :maintainer | :accept | :ok
+ :scoped_no_naming_convention | :public | :anonymous | :reject | :forbidden
+ :scoped_no_naming_convention | :public | :guest | :reject | :forbidden
+ :scoped_no_naming_convention | :public | :maintainer | :accept | :ok
+ :unscoped | :public | :anonymous | :reject | :forbidden
+ :unscoped | :public | :guest | :reject | :forbidden
+ :unscoped | :public | :maintainer | :accept | :ok
+ :non_existing | :public | :anonymous | :reject | :forbidden
+ :non_existing | :public | :guest | :reject | :forbidden
+ :non_existing | :public | :maintainer | :reject | :not_found
+
+ :scoped_naming_convention | :private | :anonymous | :reject | :not_found
+ :scoped_naming_convention | :private | :guest | :reject | :forbidden
+ :scoped_naming_convention | :private | :maintainer | :accept | :ok
+ :scoped_no_naming_convention | :private | :anonymous | :reject | :not_found
+ :scoped_no_naming_convention | :private | :guest | :reject | :forbidden
+ :scoped_no_naming_convention | :private | :maintainer | :accept | :ok
+ :unscoped | :private | :anonymous | :reject | :not_found
+ :unscoped | :private | :guest | :reject | :forbidden
+ :unscoped | :private | :maintainer | :accept | :ok
+ :non_existing | :private | :anonymous | :reject | :not_found
+ :non_existing | :private | :guest | :reject | :forbidden
+ :non_existing | :private | :maintainer | :reject | :not_found
+
+ :scoped_naming_convention | :internal | :anonymous | :reject | :forbidden
+ :scoped_naming_convention | :internal | :guest | :reject | :forbidden
+ :scoped_naming_convention | :internal | :maintainer | :accept | :ok
+ :scoped_no_naming_convention | :internal | :anonymous | :reject | :forbidden
+ :scoped_no_naming_convention | :internal | :guest | :reject | :forbidden
+ :scoped_no_naming_convention | :internal | :maintainer | :accept | :ok
+ :unscoped | :internal | :anonymous | :reject | :forbidden
+ :unscoped | :internal | :guest | :reject | :forbidden
+ :unscoped | :internal | :maintainer | :accept | :ok
+ :non_existing | :internal | :anonymous | :reject | :forbidden
+ :non_existing | :internal | :guest | :reject | :forbidden
+ :non_existing | :internal | :maintainer | :reject | :not_found
end
with_them do
@@ -401,7 +588,7 @@ RSpec.shared_examples 'handling delete dist tag requests' do |scope: :project|
before do
project.send("add_#{user_role}", user) unless anonymous
- project.update!(visibility: Gitlab::VisibilityLevel.const_get(visibility, false))
+ project.update!(visibility: visibility.to_s)
end
example_name = "#{params[:expected_result]} delete package tag request"
@@ -416,15 +603,29 @@ RSpec.shared_examples 'handling delete dist tag requests' do |scope: :project|
end
end
- context 'with oauth token' do
- let(:headers) { build_token_auth_header(token.token) }
+ shared_examples 'handling all conditions' do
+ context 'with oauth token' do
+ let(:headers) { build_token_auth_header(token.token) }
+
+ it_behaves_like 'handling different package names, visibilities and user roles'
+ end
+
+ context 'with personal access token' do
+ let(:headers) { build_token_auth_header(personal_access_token.token) }
- it_behaves_like 'handling different package names, visibilities and user roles'
+ it_behaves_like 'handling different package names, visibilities and user roles'
+ end
end
- context 'with personal access token' do
- let(:headers) { build_token_auth_header(personal_access_token.token) }
+ context 'with a group namespace' do
+ it_behaves_like 'handling all conditions'
+ end
- it_behaves_like 'handling different package names, visibilities and user roles'
+ if scope != :project
+ context 'with a user namespace' do
+ let_it_be(:namespace) { user.namespace }
+
+ it_behaves_like 'handling all conditions'
+ end
end
end
diff --git a/spec/support/shared_examples/requests/api/rubygems_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/rubygems_packages_shared_examples.rb
new file mode 100644
index 00000000000..15fb6611b90
--- /dev/null
+++ b/spec/support/shared_examples/requests/api/rubygems_packages_shared_examples.rb
@@ -0,0 +1,194 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'rejects rubygems packages access' do |user_type, status, add_member = true|
+ context "for user type #{user_type}" do
+ before do
+ project.send("add_#{user_type}", user) if add_member && user_type != :anonymous
+ end
+
+ it_behaves_like 'returning response status', status
+ end
+end
+
+RSpec.shared_examples 'process rubygems workhorse authorization' do |user_type, status, add_member = true|
+ context "for user type #{user_type}" do
+ before do
+ project.send("add_#{user_type}", user) if add_member && user_type != :anonymous
+ end
+
+ it_behaves_like 'returning response status', status
+
+ it 'has the proper content type' do
+ subject
+
+ expect(response.media_type).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
+ end
+
+ context 'with a request that bypassed gitlab-workhorse' do
+ let(:headers) do
+ { 'HTTP_AUTHORIZATION' => personal_access_token.token }
+ .merge(workhorse_headers)
+ .tap { |h| h.delete(Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER) }
+ end
+
+ before do
+ project.add_maintainer(user)
+ end
+
+ it_behaves_like 'returning response status', :forbidden
+ end
+ end
+end
+
+RSpec.shared_examples 'process rubygems upload' do |user_type, status, add_member = true|
+ RSpec.shared_examples 'creates rubygems package files' do
+ it 'creates package files', :aggregate_failures do
+ expect { subject }
+ .to change { project.packages.count }.by(1)
+ .and change { Packages::PackageFile.count }.by(1)
+ expect(response).to have_gitlab_http_status(status)
+
+ package_file = project.packages.last.package_files.reload.last
+ expect(package_file.file_name).to eq('package.gem')
+ end
+ end
+
+ context "for user type #{user_type}" do
+ before do
+ project.send("add_#{user_type}", user) if add_member && user_type != :anonymous
+ end
+
+ context 'with object storage disabled' do
+ before do
+ stub_package_file_object_storage(enabled: false)
+ end
+
+ context 'without a file from workhorse' do
+ let(:send_rewritten_field) { false }
+
+ it_behaves_like 'returning response status', :bad_request
+ end
+
+ context 'with correct params' do
+ it_behaves_like 'package workhorse uploads'
+ it_behaves_like 'creates rubygems package files'
+ it_behaves_like 'a package tracking event', 'API::RubygemPackages', 'push_package'
+ end
+ end
+
+ context 'with object storage enabled' do
+ let(:tmp_object) do
+ fog_connection.directories.new(key: 'packages').files.create( # rubocop:disable Rails/SaveBang
+ key: "tmp/uploads/#{file_name}",
+ body: 'content'
+ )
+ end
+
+ let(:fog_file) { fog_to_uploaded_file(tmp_object) }
+ let(:params) { { file: fog_file, 'file.remote_id' => file_name } }
+
+ context 'and direct upload enabled' do
+ let(:fog_connection) do
+ stub_package_file_object_storage(direct_upload: true)
+ end
+
+ it_behaves_like 'creates rubygems package files'
+
+ ['123123', '../../123123'].each do |remote_id|
+ context "with invalid remote_id: #{remote_id}" do
+ let(:params) do
+ {
+ file: fog_file,
+ 'file.remote_id' => remote_id
+ }
+ end
+
+ it_behaves_like 'returning response status', :forbidden
+ end
+ end
+ end
+
+ context 'and direct upload disabled' do
+ context 'and background upload disabled' do
+ let(:fog_connection) do
+ stub_package_file_object_storage(direct_upload: false, background_upload: false)
+ end
+
+ it_behaves_like 'creates rubygems package files'
+ end
+
+ context 'and background upload enabled' do
+ let(:fog_connection) do
+ stub_package_file_object_storage(direct_upload: false, background_upload: true)
+ end
+
+ it_behaves_like 'creates rubygems package files'
+ end
+ end
+ end
+ end
+end
+
+RSpec.shared_examples 'dependency endpoint success' do |user_type, status, add_member = true|
+ context "for user type #{user_type}" do
+ before do
+ project.send("add_#{user_type}", user) if add_member && user_type != :anonymous
+ end
+
+ raise 'Status is not :success' if status != :success
+
+ context 'with no params', :aggregate_failures do
+ it 'returns empty' do
+ subject
+
+ expect(response.body).to eq('200')
+ expect(response).to have_gitlab_http_status(status)
+ end
+ end
+
+ context 'with gems params' do
+ let(:params) { { gems: 'foo,bar' } }
+ let(:expected_response) { Marshal.dump(%w(result result)) }
+
+ it 'returns successfully', :aggregate_failures do
+ service_result = double('DependencyResolverService', execute: ServiceResponse.success(payload: 'result'))
+
+ expect(Packages::Rubygems::DependencyResolverService).to receive(:new).with(project, anything, gem_name: 'foo').and_return(service_result)
+ expect(Packages::Rubygems::DependencyResolverService).to receive(:new).with(project, anything, gem_name: 'bar').and_return(service_result)
+
+ subject
+
+ expect(response.body).to eq(expected_response) # rubocop:disable Security/MarshalLoad
+ expect(response).to have_gitlab_http_status(status)
+ end
+
+ it 'rejects if the service fails', :aggregate_failures do
+ service_result = double('DependencyResolverService', execute: ServiceResponse.error(message: 'rejected', http_status: :bad_request))
+
+ expect(Packages::Rubygems::DependencyResolverService).to receive(:new).with(project, anything, gem_name: 'foo').and_return(service_result)
+
+ subject
+
+ expect(response.body).to match(/rejected/)
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+ end
+ end
+end
+
+RSpec.shared_examples 'Rubygems gem download' do |user_type, status, add_member = true|
+ context "for user type #{user_type}" do
+ before do
+ project.send("add_#{user_type}", user) if add_member && user_type != :anonymous
+ end
+
+ it 'returns the gem', :aggregate_failures do
+ subject
+
+ expect(response.media_type).to eq('application/octet-stream')
+ expect(response).to have_gitlab_http_status(status)
+ end
+
+ it_behaves_like 'a package tracking event', described_class.name, 'pull_package'
+ end
+end
diff --git a/spec/support/shared_examples/service_desk_issue_templates_examples.rb b/spec/support/shared_examples/service_desk_issue_templates_examples.rb
new file mode 100644
index 00000000000..fd9645df7a3
--- /dev/null
+++ b/spec/support/shared_examples/service_desk_issue_templates_examples.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'issue description templates from current project only' do
+ it 'loads issue description templates from the project only' do
+ within('#service-desk-template-select') do
+ expect(page).to have_content('project-issue-bar')
+ expect(page).to have_content('project-issue-foo')
+ expect(page).not_to have_content('group-issue-bar')
+ expect(page).not_to have_content('group-issue-foo')
+ end
+ end
+end
diff --git a/spec/support/shared_examples/services/boards/update_boards_shared_examples.rb b/spec/support/shared_examples/services/boards/update_boards_shared_examples.rb
new file mode 100644
index 00000000000..cd773a2a04a
--- /dev/null
+++ b/spec/support/shared_examples/services/boards/update_boards_shared_examples.rb
@@ -0,0 +1,83 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'board update service' do
+ subject(:service) { described_class.new(board.resource_parent, user, all_params) }
+
+ it 'updates the board with valid params' do
+ result = described_class.new(group, user, name: 'Engineering').execute(board)
+
+ expect(result).to eq(true)
+ expect(board.reload.name).to eq('Engineering')
+ end
+
+ it 'does not update the board with invalid params' do
+ orig_name = board.name
+
+ result = described_class.new(group, user, name: nil).execute(board)
+
+ expect(result).to eq(false)
+ expect(board.reload.name).to eq(orig_name)
+ end
+
+ context 'with scoped_issue_board available' do
+ before do
+ stub_licensed_features(scoped_issue_board: true)
+ end
+
+ context 'user is member of the board parent' do
+ before do
+ board.resource_parent.add_reporter(user)
+ end
+
+ it 'updates the configuration params when scoped issue board is enabled' do
+ service.execute(board)
+
+ labels = updated_scoped_params.delete(:labels)
+ expect(board.reload).to have_attributes(updated_scoped_params)
+ expect(board.labels).to match_array(labels)
+ end
+ end
+
+ context 'when labels param is used' do
+ let(:params) { { labels: [label.name, parent_label.name, 'new label'].join(',') } }
+
+ subject(:service) { described_class.new(board.resource_parent, user, params) }
+
+ context 'when user can create new labels' do
+ before do
+ board.resource_parent.add_reporter(user)
+ end
+
+ it 'adds labels to the board' do
+ service.execute(board)
+
+ expect(board.reload.labels.map(&:name)).to match_array([label.name, parent_label.name, 'new label'])
+ end
+ end
+
+ context 'when user can not create new labels' do
+ before do
+ board.resource_parent.add_guest(user)
+ end
+
+ it 'adds only existing labels to the board' do
+ service.execute(board)
+
+ expect(board.reload.labels.map(&:name)).to match_array([label.name, parent_label.name])
+ end
+ end
+ end
+ end
+
+ context 'without scoped_issue_board available' do
+ before do
+ stub_licensed_features(scoped_issue_board: false)
+ end
+
+ it 'filters unpermitted params when scoped issue board is not enabled' do
+ service.execute(board)
+
+ expect(board.reload).to have_attributes(updated_without_scoped_params)
+ end
+ end
+end
diff --git a/spec/support/shared_examples/services/packages/maven/metadata_shared_examples.rb b/spec/support/shared_examples/services/packages/maven/metadata_shared_examples.rb
new file mode 100644
index 00000000000..4de672bb732
--- /dev/null
+++ b/spec/support/shared_examples/services/packages/maven/metadata_shared_examples.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'handling metadata content pointing to a file for the create xml service' do
+ context 'with metadata content pointing to a file' do
+ let(:service) { described_class.new(metadata_content: file, package: package) }
+ let(:file) do
+ Tempfile.new('metadata').tap do |file|
+ if file_contents
+ file.write(file_contents)
+ file.flush
+ file.rewind
+ end
+ end
+ end
+
+ after do
+ file.close
+ file.unlink
+ end
+
+ context 'with valid content' do
+ let(:file_contents) { metadata_xml }
+
+ it 'returns no changes' do
+ expect(subject).to be_success
+ expect(subject.payload).to eq(changes_exist: false, empty_versions: false)
+ end
+ end
+
+ context 'with invalid content' do
+ let(:file_contents) { '<meta></metadata>' }
+
+ it_behaves_like 'returning an error service response', message: 'metadata_content is invalid'
+ end
+
+ context 'with no content' do
+ let(:file_contents) { nil }
+
+ it_behaves_like 'returning an error service response', message: 'metadata_content is invalid'
+ end
+ end
+end
+
+RSpec.shared_examples 'handling invalid parameters for create xml service' do
+ context 'with no package' do
+ let(:metadata_xml) { '' }
+ let(:package) { nil }
+
+ it_behaves_like 'returning an error service response', message: 'package not set'
+ end
+
+ context 'with no metadata content' do
+ let(:metadata_xml) { nil }
+
+ it_behaves_like 'returning an error service response', message: 'metadata_content not set'
+ end
+end
diff --git a/spec/support/snowplow.rb b/spec/support/snowplow.rb
index 0d6102f1705..e58be667b37 100644
--- a/spec/support/snowplow.rb
+++ b/spec/support/snowplow.rb
@@ -1,24 +1,13 @@
# frozen_string_literal: true
+require_relative 'stub_snowplow'
+
RSpec.configure do |config|
config.include SnowplowHelpers, :snowplow
+ config.include StubSnowplow, :snowplow
config.before(:each, :snowplow) do
- # Using a high buffer size to not cause early flushes
- buffer_size = 100
- # 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
+ stub_snowplow
end
config.after(:each, :snowplow) do
diff --git a/spec/support/stub_snowplow.rb b/spec/support/stub_snowplow.rb
new file mode 100644
index 00000000000..a21ce2399d7
--- /dev/null
+++ b/spec/support/stub_snowplow.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module StubSnowplow
+ def stub_snowplow
+ # Using a high buffer size to not cause early flushes
+ buffer_size = 100
+ # WebMock is set up to allow requests to `localhost`
+ host = 'localhost'
+
+ # rubocop:disable RSpec/AnyInstanceOf
+ 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))
+ # rubocop:enable RSpec/AnyInstanceOf
+
+ 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
+end