diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-03-16 18:18:33 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-03-16 18:18:33 +0000 |
commit | f64a639bcfa1fc2bc89ca7db268f594306edfd7c (patch) | |
tree | a2c3c2ebcc3b45e596949db485d6ed18ffaacfa1 /spec/support | |
parent | bfbc3e0d6583ea1a91f627528bedc3d65ba4b10f (diff) | |
download | gitlab-ce-f64a639bcfa1fc2bc89ca7db268f594306edfd7c.tar.gz |
Add latest changes from gitlab-org/gitlab@13-10-stable-eev13.10.0-rc40
Diffstat (limited to 'spec/support')
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 |