diff options
Diffstat (limited to 'spec/support')
99 files changed, 3111 insertions, 832 deletions
diff --git a/spec/support/ability_check.rb b/spec/support/ability_check.rb new file mode 100644 index 00000000000..213944506bb --- /dev/null +++ b/spec/support/ability_check.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true + +require 'gitlab/utils/strong_memoize' + +module Support + module AbilityCheck + def self.inject(mod) + mod.prepend AbilityExtension + end + + module AbilityExtension + def before_check(policy, ability, user, subject, opts) + return super if Checker.ok?(policy, ability) + + ActiveSupport::Deprecation.warn(<<~WARNING) + Ability #{ability.inspect} in #{policy.class} not found. + user=#{user.inspect}, subject=#{subject}, opts=#{opts.inspect}" + + To exclude this check add this entry to #{Checker::TODO_YAML}: + #{policy.class}: + - #{ability} + WARNING + end + end + + module Checker + include Gitlab::Utils::StrongMemoize + extend self + + TODO_YAML = File.join(__dir__, 'ability_check_todo.yml') + + def ok?(policy, ability) + ignored?(policy, ability) || ability_found?(policy, ability) + end + + private + + def ignored?(policy, ability) + todo_list[policy.class.name]&.include?(ability.to_s) + end + + # Use Policy#has_ability? instead after it has been accepted and released. + # See https://gitlab.com/gitlab-org/ruby/gems/declarative-policy/-/issues/25 + def ability_found?(policy, ability) + # NilPolicy has no abilities. Ignore it. + return true if policy.is_a?(DeclarativePolicy::NilPolicy) + + # Search in current policy first + return true if policy.class.ability_map.map.key?(ability) + + # Search recursively in all delegations otherwise. + # This is potentially slow. + # Stolen from: + # https://gitlab.com/gitlab-org/ruby/gems/declarative-policy/-/blob/d691e/lib/declarative_policy/base.rb#L360-369 + policy.class.delegations.any? do |_, block| + new_subject = policy.instance_eval(&block) + new_policy = policy.policy_for(new_subject) + + ability_found?(new_policy, ability) + end + end + + def todo_list + hash = YAML.load_file(TODO_YAML) + return {} unless hash.is_a?(Hash) + + hash.transform_values(&:to_set) + end + + strong_memoize_attr :todo_list + end + end +end diff --git a/spec/support/ability_check_todo.yml b/spec/support/ability_check_todo.yml new file mode 100644 index 00000000000..eafd595b137 --- /dev/null +++ b/spec/support/ability_check_todo.yml @@ -0,0 +1,73 @@ +# This list tracks unknown abilities per policy. +# +# This file is used by `spec/support/ability_check.rb`. +# +# Each TODO entry means that an ability wasn't found in +# the particular policy class or its delegations. +# +# This could be one of the reasons: +# * The ability is misspelled. +# - Suggested action: Fix typo. +# * The ability has been removed from a policy but is still in use. +# - Remove production code in question. +# * The ability is defined in EE policy but is used in FOSS code. +# - Guard the check or move it to EE folder. +# - See https://docs.gitlab.com/ee/development/ee_features.html +# * The ability is defined in another policy but delegation is missing. +# - Add delegation policy or guard the check with a type check. +# - See https://docs.gitlab.com/ee/development/policies.html#delegation +# * The ability check is polymorphic (for example, Issuable) and some policies +# do not implement this ability. +# - Exclude TODO permanently below. +# - Guard the check with a type check. +# * The ability check is defined on GraphQL field which does not support +# authorization on resolved field values yet. +# See https://gitlab.com/gitlab-org/gitlab/-/issues/300922 +--- +# <Policy class>: +# - <ability name> +# - <ability name> +# ... + +# Temporary excludes: + +Ci::BridgePolicy: +- read_job_artifacts +CommitStatusPolicy: +- read_job_artifacts +EpicPolicy: +- create_timelog +- read_emoji +- set_issue_crm_contacts +GlobalPolicy: +- read_achievement +- read_on_demand_dast_scan +- update_max_pages_size +GroupPolicy: +- admin_merge_request +- change_push_rules +- manage_owners +IssuePolicy: +- create_test_case +MergeRequestPolicy: +- set_confidentiality +- set_issue_crm_contacts +Namespaces::UserNamespacePolicy: +- read_crm_contact +PersonalSnippetPolicy: +- read_internal_note +- read_project +ProjectMemberPolicy: +- override_project_member +ProjectPolicy: +- admin_feature_flags_issue_links +- admin_vulnerability +- create_requirement +- create_test_case +- read_group_saml_identity +UserPolicy: +- admin_observability +- admin_terraform_state +- read_observability + +# Permanent excludes (please provide a reason): diff --git a/spec/support/helpers/ci/template_helpers.rb b/spec/support/helpers/ci/template_helpers.rb index cd3ab4bd82d..1818dec5fc7 100644 --- a/spec/support/helpers/ci/template_helpers.rb +++ b/spec/support/helpers/ci/template_helpers.rb @@ -6,6 +6,10 @@ module Ci 'registry.gitlab.com' end + def auto_build_image_repository + "gitlab-org/cluster-integration/auto-build-image" + end + def public_image_exist?(registry, repository, image) public_image_manifest(registry, repository, image).present? end diff --git a/spec/support/helpers/content_editor_helpers.rb b/spec/support/helpers/content_editor_helpers.rb new file mode 100644 index 00000000000..c12fd1fbbd7 --- /dev/null +++ b/spec/support/helpers/content_editor_helpers.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +module ContentEditorHelpers + def switch_to_content_editor + click_button _('Viewing markdown') + click_button _('Rich text') + end + + def type_in_content_editor(keys) + find(content_editor_testid).send_keys keys + end + + def open_insert_media_dropdown + page.find('svg[data-testid="media-icon"]').click + end + + def set_source_editor_content(content) + find('.js-gfm-input').set content + end + + def expect_formatting_menu_to_be_visible + expect(page).to have_css('[data-testid="formatting-bubble-menu"]') + end + + def expect_formatting_menu_to_be_hidden + expect(page).not_to have_css('[data-testid="formatting-bubble-menu"]') + end + + def expect_media_bubble_menu_to_be_visible + expect(page).to have_css('[data-testid="media-bubble-menu"]') + end + + def upload_asset(fixture_name) + attach_file('content_editor_image', Rails.root.join('spec', 'fixtures', fixture_name), make_visible: true) + end + + def wait_until_hidden_field_is_updated(value) + expect(page).to have_field(with: value, type: 'hidden') + end + + def display_media_bubble_menu(media_element_selector, fixture_file) + upload_asset fixture_file + + wait_for_requests + + expect(page).to have_css(media_element_selector) + + page.find(media_element_selector).click + end + + def click_edit_diagram_button + page.find('[data-testid="edit-diagram"]').click + end + + def expect_drawio_editor_is_opened + expect(page).to have_css('#drawio-frame', visible: :hidden) + end +end diff --git a/spec/support/helpers/fake_u2f_device.rb b/spec/support/helpers/fake_u2f_device.rb deleted file mode 100644 index 2ed1222ebd3..00000000000 --- a/spec/support/helpers/fake_u2f_device.rb +++ /dev/null @@ -1,47 +0,0 @@ -# frozen_string_literal: true - -class FakeU2fDevice - attr_reader :name - - def initialize(page, name, device = nil) - @page = page - @name = name - @u2f_device = device - end - - def respond_to_u2f_registration - app_id = @page.evaluate_script('gon.u2f.app_id') - challenges = @page.evaluate_script('gon.u2f.challenges') - - json_response = u2f_device(app_id).register_response(challenges[0]) - - @page.execute_script(" - u2f.register = function(appId, registerRequests, signRequests, callback) { - callback(#{json_response}); - }; - ") - end - - def respond_to_u2f_authentication - app_id = @page.evaluate_script('gon.u2f.app_id') - challenge = @page.evaluate_script('gon.u2f.challenge') - json_response = u2f_device(app_id).sign_response(challenge) - - @page.execute_script(" - u2f.sign = function(appId, challenges, signRequests, callback) { - callback(#{json_response}); - }; - window.gl.u2fAuthenticate.start(); - ") - end - - def fake_u2f_authentication - @page.execute_script("window.gl.u2fAuthenticate.renderAuthenticated('abc');") - end - - private - - def u2f_device(app_id) - @u2f_device ||= U2F::FakeU2F.new(app_id) - end -end diff --git a/spec/support/helpers/features/two_factor_helpers.rb b/spec/support/helpers/features/two_factor_helpers.rb index 08a7665201f..d5f069a40ea 100644 --- a/spec/support/helpers/features/two_factor_helpers.rb +++ b/spec/support/helpers/features/two_factor_helpers.rb @@ -14,23 +14,25 @@ module Spec module Helpers module Features module TwoFactorHelpers + def copy_recovery_codes + click_on _('Copy codes') + click_on _('Proceed') + end + + def enable_two_factor_authentication + click_on _('Enable two-factor authentication') + expect(page).to have_content(_('Set up new device')) + wait_for_requests + end + def manage_two_factor_authentication click_on 'Manage two-factor authentication' expect(page).to have_content("Set up new device") wait_for_requests end - def register_u2f_device(u2f_device = nil, name: 'My device') - u2f_device ||= FakeU2fDevice.new(page, name) - u2f_device.respond_to_u2f_registration - click_on 'Set up new device' - expect(page).to have_content('Your device was successfully set up') - fill_in "Pick a name", with: name - click_on 'Register device' - u2f_device - end - # Registers webauthn device via UI + # Remove after `webauthn_without_totp` feature flag is deleted. def register_webauthn_device(webauthn_device = nil, name: 'My device') webauthn_device ||= FakeWebauthnDevice.new(page, name) webauthn_device.respond_to_webauthn_registration @@ -41,6 +43,25 @@ module Spec webauthn_device end + def webauthn_device_registration(webauthn_device: nil, name: 'My device', password: 'fake') + webauthn_device ||= FakeWebauthnDevice.new(page, name) + webauthn_device.respond_to_webauthn_registration + click_on _('Set up new device') + webauthn_fill_form_and_submit(name: name, password: password) + webauthn_device + end + + def webauthn_fill_form_and_submit(name: 'My device', password: 'fake') + expect(page).to have_content( + _('Your device was successfully set up! Give it a name and register it with the GitLab server.') + ) + within '[data-testid="create-webauthn"]' do + fill_in _('Device name'), with: name + fill_in _('Current password'), with: password + click_on _('Register device') + end + end + # Adds webauthn device directly via database def add_webauthn_device(app_id, user, fake_device = nil, name: 'My device') fake_device ||= WebAuthn::FakeClient.new(app_id) diff --git a/spec/support/helpers/filtered_search_helpers.rb b/spec/support/helpers/filtered_search_helpers.rb index 677cea7b804..b07f5dcf2e1 100644 --- a/spec/support/helpers/filtered_search_helpers.rb +++ b/spec/support/helpers/filtered_search_helpers.rb @@ -190,9 +190,9 @@ module FilteredSearchHelpers ## # For use with gl-filtered-search - def select_tokens(*args, submit: false) + def select_tokens(*args, submit: false, input_text: 'Search') within '[data-testid="filtered-search-input"]' do - find_field('Search').click + find_field(input_text).click args.each do |token| # Move mouse away to prevent invoking tooltips on usernames, which blocks the search input @@ -230,6 +230,13 @@ module FilteredSearchHelpers find('.gl-filtered-search-token-segment', text: value).click end + def toggle_sort_direction + page.within('.vue-filtered-search-bar-container .sort-dropdown-container') do + page.find("button[title^='Sort direction']").click + wait_for_requests + end + end + def expect_visible_suggestions_list expect(page).to have_css('.gl-filtered-search-suggestion-list') end diff --git a/spec/support/helpers/fixture_helpers.rb b/spec/support/helpers/fixture_helpers.rb index 7b3b8ae5f7a..eafdecb2e3d 100644 --- a/spec/support/helpers/fixture_helpers.rb +++ b/spec/support/helpers/fixture_helpers.rb @@ -8,6 +8,6 @@ module FixtureHelpers end def expand_fixture_path(filename, dir: '') - File.expand_path(Rails.root.join(dir, 'spec', 'fixtures', filename)) + File.expand_path(rails_root_join(dir, 'spec', 'fixtures', filename)) end end diff --git a/spec/support/helpers/gitaly_setup.rb b/spec/support/helpers/gitaly_setup.rb index 398a2a20f2f..bf3c67a1818 100644 --- a/spec/support/helpers/gitaly_setup.rb +++ b/spec/support/helpers/gitaly_setup.rb @@ -198,8 +198,23 @@ module GitalySetup end LOGGER.debug "Checking gitaly-ruby bundle...\n" + + bundle_install unless bundle_check + + abort 'bundle check failed' unless bundle_check + end + + def bundle_check + bundle_cmd('check') + end + + def bundle_install + bundle_cmd('install') + end + + def bundle_cmd(cmd) out = ENV['CI'] ? $stdout : '/dev/null' - abort 'bundle check failed' unless system(env, 'bundle', 'check', out: out, chdir: gemfile_dir) + system(env, 'bundle', cmd, out: out, chdir: gemfile_dir) end def connect_proc(toml) diff --git a/spec/support/helpers/graphql_helpers.rb b/spec/support/helpers/graphql_helpers.rb index 2176a477371..191e5192a61 100644 --- a/spec/support/helpers/graphql_helpers.rb +++ b/spec/support/helpers/graphql_helpers.rb @@ -310,7 +310,7 @@ module GraphqlHelpers "{ #{q} }" end - def graphql_mutation(name, input, fields = nil, &block) + def graphql_mutation(name, input, fields = nil, excluded = [], &block) raise ArgumentError, 'Please pass either `fields` parameter or a block to `#graphql_mutation`, but not both.' if fields.present? && block name = name.graphql_name if name.respond_to?(:graphql_name) @@ -319,7 +319,7 @@ module GraphqlHelpers mutation_field = GitlabSchema.mutation.fields[mutation_name] fields = yield if block - fields ||= all_graphql_fields_for(mutation_field.type.to_type_signature) + fields ||= all_graphql_fields_for(mutation_field.type.to_type_signature, excluded: excluded) query = <<~MUTATION mutation(#{input_variable_name}: #{mutation_field.arguments['input'].type.to_type_signature}) { diff --git a/spec/support/helpers/login_helpers.rb b/spec/support/helpers/login_helpers.rb index 5fde80e6dc9..e93d04a0b80 100644 --- a/spec/support/helpers/login_helpers.rb +++ b/spec/support/helpers/login_helpers.rb @@ -139,11 +139,6 @@ module LoginHelpers click_link_or_button "oauth-login-#{provider}" end - def fake_successful_u2f_authentication - allow(U2fRegistration).to receive(:authenticate).and_return(true) - FakeU2fDevice.new(page, nil).fake_u2f_authentication - end - def fake_successful_webauthn_authentication allow_any_instance_of(Webauthn::AuthenticateService).to receive(:execute).and_return(true) FakeWebauthnDevice.new(page, nil).fake_webauthn_authentication @@ -218,6 +213,15 @@ module LoginHelpers config end + def prepare_provider_route(provider_name) + routes = Rails.application.routes + routes.disable_clear_and_finalize = true + routes.formatter.clear + routes.draw do + post "/users/auth/#{provider_name}" => "omniauth_callbacks##{provider_name}" + end + end + def stub_omniauth_provider(provider, context: Rails.application) env = env_from_context(context) diff --git a/spec/support/helpers/navbar_structure_helper.rb b/spec/support/helpers/navbar_structure_helper.rb index 48c6e590e1b..8248ea0bb84 100644 --- a/spec/support/helpers/navbar_structure_helper.rb +++ b/spec/support/helpers/navbar_structure_helper.rb @@ -106,6 +106,14 @@ module NavbarStructureHelper ) end + def insert_infrastructure_aws_nav + insert_after_sub_nav_item( + _('Google Cloud'), + within: _('Infrastructure'), + new_sub_nav_item_name: _('AWS') + ) + end + def project_analytics_sub_nav_item [ _('Value stream'), diff --git a/spec/support/helpers/query_recorder.rb b/spec/support/helpers/query_recorder.rb index 5be9ba9ae1e..e8fa73a1b95 100644 --- a/spec/support/helpers/query_recorder.rb +++ b/spec/support/helpers/query_recorder.rb @@ -102,6 +102,10 @@ module ActiveRecord @occurrences ||= @log.group_by(&:to_s).transform_values(&:count) end + def occurrences_starting_with(str) + occurrences.select { |query, _count| query.starts_with?(str) } + end + def ignorable?(values) return true if skip_schema_queries && values[:name]&.include?("SCHEMA") return true if values[:name]&.match(/License Load/) diff --git a/spec/support/helpers/repo_helpers.rb b/spec/support/helpers/repo_helpers.rb index 9f37cf61cc9..45467fb7099 100644 --- a/spec/support/helpers/repo_helpers.rb +++ b/spec/support/helpers/repo_helpers.rb @@ -41,6 +41,7 @@ eos line_code: '2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_14', line_code_path: 'files/ruby/popen.rb', del_line_code: '2f6fcd96b88b36ce98c38da085c795a27d92a3dd_13_13', + referenced_by: [], message: <<eos Change some files Signed-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> @@ -56,6 +57,7 @@ eos author_full_name: "Sytse Sijbrandij", author_email: "sytse@gitlab.com", files_changed_count: 1, + referenced_by: [], message: <<eos Add directory structure for tree_helper spec @@ -74,6 +76,7 @@ eos sha: "913c66a37b4a45b9769037c55c2d238bd0942d2e", author_full_name: "Dmitriy Zaporozhets", author_email: "dmitriy.zaporozhets@gmail.com", + referenced_by: [], message: <<eos Files, encoding and much more Signed-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> @@ -89,6 +92,7 @@ eos author_email: "dmitriy.zaporozhets@gmail.com", old_blob_id: '33f3729a45c02fc67d00adb1b8bca394b0e761d9', new_blob_id: '2f63565e7aac07bcdadb654e253078b727143ec4', + referenced_by: [], message: <<eos Modified image Signed-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> diff --git a/spec/support/helpers/stub_configuration.rb b/spec/support/helpers/stub_configuration.rb index 4ca8f26be9e..2a7b36a4c00 100644 --- a/spec/support/helpers/stub_configuration.rb +++ b/spec/support/helpers/stub_configuration.rb @@ -179,7 +179,7 @@ module StubConfiguration def to_settings(hash) hash.transform_values do |value| if value.is_a? Hash - Settingslogic.new(value.deep_stringify_keys) + Settingslogic.new(value.to_h.deep_stringify_keys) else value end diff --git a/spec/support/helpers/stub_object_storage.rb b/spec/support/helpers/stub_object_storage.rb index 6b633856228..d120e1805e3 100644 --- a/spec/support/helpers/stub_object_storage.rb +++ b/spec/support/helpers/stub_object_storage.rb @@ -15,7 +15,7 @@ module StubObjectStorage direct_upload: false, cdn: {} ) - old_config = Settingslogic.new(config.deep_stringify_keys) + old_config = Settingslogic.new(config.to_h.deep_stringify_keys) new_config = config.to_h.deep_symbolize_keys.merge({ enabled: enabled, proxy_download: proxy_download, @@ -30,7 +30,7 @@ module StubObjectStorage allow(config).to receive(:proxy_download) { proxy_download } allow(config).to receive(:direct_upload) { direct_upload } - uploader_config = Settingslogic.new(new_config.deep_stringify_keys) + uploader_config = Settingslogic.new(new_config.to_h.deep_stringify_keys) allow(uploader).to receive(:object_store_options).and_return(uploader_config) allow(uploader.options).to receive(:object_store).and_return(uploader_config) diff --git a/spec/support/helpers/test_env.rb b/spec/support/helpers/test_env.rb index 3403064bf0b..727b8a6b880 100644 --- a/spec/support/helpers/test_env.rb +++ b/spec/support/helpers/test_env.rb @@ -2,6 +2,7 @@ require 'parallel' require_relative 'gitaly_setup' +require_relative '../../../lib/gitlab/setup_helper' module TestEnv extend self diff --git a/spec/support/helpers/usage_data_helpers.rb b/spec/support/helpers/usage_data_helpers.rb index 2bec945fbc8..9abfc39e31f 100644 --- a/spec/support/helpers/usage_data_helpers.rb +++ b/spec/support/helpers/usage_data_helpers.rb @@ -54,8 +54,6 @@ module UsageDataHelpers projects_asana_active projects_jenkins_active projects_jira_active - projects_jira_server_active - projects_jira_cloud_active projects_jira_dvcs_cloud_active projects_jira_dvcs_server_active projects_slack_active diff --git a/spec/support/matchers/background_migrations_matchers.rb b/spec/support/matchers/background_migrations_matchers.rb index 9f39f576b95..97993b158c8 100644 --- a/spec/support/matchers/background_migrations_matchers.rb +++ b/spec/support/matchers/background_migrations_matchers.rb @@ -100,3 +100,17 @@ RSpec::Matchers.define :be_finalize_background_migration_of do |migration| end end end + +RSpec::Matchers.define :ensure_batched_background_migration_is_finished_for do |migration_arguments| + define_method :matches? do |klass| + expect_next_instance_of(klass) do |instance| + expect(instance).to receive(:ensure_batched_background_migration_is_finished).with(migration_arguments) + end + end + + define_method :does_not_match? do |klass| + expect_next_instance_of(klass) do |instance| + expect(instance).not_to receive(:ensure_batched_background_migration_is_finished).with(migration_arguments) + end + end +end diff --git a/spec/support/matchers/exceed_redis_call_limit.rb b/spec/support/matchers/exceed_redis_call_limit.rb new file mode 100644 index 00000000000..2b1e1ebad23 --- /dev/null +++ b/spec/support/matchers/exceed_redis_call_limit.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +module ExceedRedisCallLimitHelpers + def build_recorder(block) + return block if block.is_a?(RedisCommands::Recorder) + + RedisCommands::Recorder.new(&block) + end + + def verify_count(expected, block) + @actual = build_recorder(block).count + + @actual > expected + end + + def verify_commands_count(command, expected, block) + @actual = build_recorder(block).by_command(command).count + + @actual > expected + end +end + +RSpec::Matchers.define :exceed_redis_calls_limit do |expected| + supports_block_expectations + + include ExceedRedisCallLimitHelpers + + match do |block| + verify_count(expected, block) + end + + failure_message do + "Expected at least #{expected} calls, but got #{actual}" + end + + failure_message_when_negated do + "Expected a maximum of #{expected} calls, but got #{actual}" + end +end + +RSpec::Matchers.define :exceed_redis_command_calls_limit do |command, expected| + supports_block_expectations + + include ExceedRedisCallLimitHelpers + + match do |block| + verify_commands_count(command, expected, block) + end + + failure_message do + "Expected at least #{expected} calls to '#{command}', but got #{actual}" + end + + failure_message_when_negated do + "Expected a maximum of #{expected} calls to '#{command}', but got #{actual}" + end +end diff --git a/spec/support/matchers/request_urgency_matcher.rb b/spec/support/matchers/request_urgency_matcher.rb new file mode 100644 index 00000000000..d3c5093719e --- /dev/null +++ b/spec/support/matchers/request_urgency_matcher.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require 'spec_helper' + +module RequestUrgencyMatcher + RSpec::Matchers.define :have_request_urgency do |request_urgency| + match do |_actual| + if controller_instance = request.env["action_controller.instance"] + controller_instance.urgency.name == request_urgency + elsif endpoint = request.env['api.endpoint'] + urgency = endpoint.options[:for].try(:urgency_for_app, endpoint) + urgency.name == request_urgency + else + raise 'neither a controller nor a request spec' + end + end + + failure_message do |_actual| + if controller_instance = request.env["action_controller.instance"] + "request urgency #{controller_instance.urgency.name} is set, \ + but expected to be #{request_urgency}".squish + elsif endpoint = request.env['api.endpoint'] + urgency = endpoint.options[:for].try(:urgency_for_app, endpoint) + "request urgency #{urgency.name} is set, \ + but expected to be #{request_urgency}".squish + end + end + end +end diff --git a/spec/support/permissions_check.rb b/spec/support/permissions_check.rb new file mode 100644 index 00000000000..efe0ecb530b --- /dev/null +++ b/spec/support/permissions_check.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Support + module PermissionsCheck + def self.inject(mod) + mod.prepend PermissionsExtension if Gitlab::Utils.to_boolean(ENV['GITLAB_DEBUG_POLICIES']) + end + + module PermissionsExtension + def before_check(policy, ability, _user, _subject, _opts) + puts( + "POLICY CHECK DEBUG -> " \ + "policy: #{policy.class.name}, ability: #{ability}, called_from: #{caller_locations(2, 5)}" + ) + end + end + end +end diff --git a/spec/support/protected_tags/access_control_ce_shared_examples.rb b/spec/support/protected_tags/access_control_ce_shared_examples.rb index 8666c19481c..6aa9647bcec 100644 --- a/spec/support/protected_tags/access_control_ce_shared_examples.rb +++ b/spec/support/protected_tags/access_control_ce_shared_examples.rb @@ -6,18 +6,8 @@ RSpec.shared_examples "protected tags > access control > CE" do visit project_protected_tags_path(project) set_protected_tag_name('master') - - within('.js-new-protected-tag') do - allowed_to_create_button = find(".js-allowed-to-create") - - unless allowed_to_create_button.text == access_type_name - allowed_to_create_button.click - find('.create_access_levels-container .dropdown-menu li', match: :first) - within('.create_access_levels-container .dropdown-menu') { click_on access_type_name } - end - end - - click_on "Protect" + set_allowed_to('create', access_type_name) + click_on_protect expect(ProtectedTag.count).to eq(1) expect(ProtectedTag.last.create_access_levels.map(&:access_level)).to eq([access_type_id]) @@ -27,19 +17,12 @@ RSpec.shared_examples "protected tags > access control > CE" do visit project_protected_tags_path(project) set_protected_tag_name('master') - - click_on "Protect" + set_allowed_to('create', 'No one') + click_on_protect expect(ProtectedTag.count).to eq(1) - within(".protected-tags-list") do - find(".js-allowed-to-create").click - - within('.js-allowed-to-create-container') do - expect(first("li")).to have_content("Roles") - click_on access_type_name - end - end + set_allowed_to('create', access_type_name, form: '.protected-tags-list') wait_for_requests diff --git a/spec/support/rspec.rb b/spec/support/rspec.rb index ff0b5bebe33..6e377b8cb0d 100644 --- a/spec/support/rspec.rb +++ b/spec/support/rspec.rb @@ -19,6 +19,13 @@ RSpec.configure do |config| # Re-run failures locally with `--only-failures` config.example_status_persistence_file_path = ENV.fetch('RSPEC_LAST_RUN_RESULTS_FILE', './spec/examples.txt') + # Makes diffs show entire non-truncated values. + config.before(:each, :unlimited_max_formatted_output_length) do + config.expect_with :rspec do |c| + c.max_formatted_output_length = nil + end + end + unless ENV['CI'] # Allow running `:focus` examples locally, # falling back to all tests when there is no `:focus` example. diff --git a/spec/support/rspec_order_todo.yml b/spec/support/rspec_order_todo.yml index 7aa7d8e8abd..74b80c4864c 100644 --- a/spec/support/rspec_order_todo.yml +++ b/spec/support/rspec_order_todo.yml @@ -226,7 +226,6 @@ - './ee/spec/features/analytics/code_analytics_spec.rb' - './ee/spec/features/analytics/group_analytics_spec.rb' - './ee/spec/features/billings/billing_plans_spec.rb' -- './ee/spec/features/billings/extend_reactivate_trial_spec.rb' - './ee/spec/features/billings/qrtly_reconciliation_alert_spec.rb' - './ee/spec/features/boards/board_filters_spec.rb' - './ee/spec/features/boards/boards_licensed_features_spec.rb' @@ -957,7 +956,6 @@ - './ee/spec/helpers/ee/geo_helper_spec.rb' - './ee/spec/helpers/ee/gitlab_routing_helper_spec.rb' - './ee/spec/helpers/ee/graph_helper_spec.rb' -- './ee/spec/helpers/ee/groups/analytics/cycle_analytics_helper_spec.rb' - './ee/spec/helpers/ee/groups/group_members_helper_spec.rb' - './ee/spec/helpers/ee/groups_helper_spec.rb' - './ee/spec/helpers/ee/groups/settings_helper_spec.rb' @@ -1577,7 +1575,6 @@ - './ee/spec/lib/omni_auth/strategies/group_saml_spec.rb' - './ee/spec/lib/omni_auth/strategies/kerberos_spec.rb' - './ee/spec/lib/peek/views/elasticsearch_spec.rb' -- './ee/spec/lib/sidebars/groups/menus/administration_menu_spec.rb' - './ee/spec/lib/sidebars/groups/menus/analytics_menu_spec.rb' - './ee/spec/lib/sidebars/groups/menus/epics_menu_spec.rb' - './ee/spec/lib/sidebars/groups/menus/security_compliance_menu_spec.rb' @@ -3201,7 +3198,6 @@ - './ee/spec/workers/concerns/elastic/indexing_control_spec.rb' - './ee/spec/workers/concerns/elastic/migration_obsolete_spec.rb' - './ee/spec/workers/concerns/elastic/migration_options_spec.rb' -- './ee/spec/workers/concerns/geo_queue_spec.rb' - './ee/spec/workers/concerns/update_orchestration_policy_configuration_spec.rb' - './ee/spec/workers/create_github_webhook_worker_spec.rb' - './ee/spec/workers/deployments/auto_rollback_worker_spec.rb' @@ -5084,7 +5080,6 @@ - './spec/helpers/admin/deploy_key_helper_spec.rb' - './spec/helpers/admin/identities_helper_spec.rb' - './spec/helpers/admin/user_actions_helper_spec.rb' -- './spec/helpers/analytics/cycle_analytics_helper_spec.rb' - './spec/helpers/appearances_helper_spec.rb' - './spec/helpers/application_helper_spec.rb' - './spec/helpers/application_settings_helper_spec.rb' @@ -5142,7 +5137,6 @@ - './spec/helpers/groups/settings_helper_spec.rb' - './spec/helpers/hooks_helper_spec.rb' - './spec/helpers/icons_helper_spec.rb' -- './spec/helpers/ide_helper_spec.rb' - './spec/helpers/import_helper_spec.rb' - './spec/helpers/instance_configuration_helper_spec.rb' - './spec/helpers/integrations_helper_spec.rb' @@ -7956,7 +7950,6 @@ - './spec/models/concerns/token_authenticatable_strategies/encryption_helper_spec.rb' - './spec/models/concerns/transactions_spec.rb' - './spec/models/concerns/triggerable_hooks_spec.rb' -- './spec/models/concerns/uniquify_spec.rb' - './spec/models/concerns/usage_statistics_spec.rb' - './spec/models/concerns/vulnerability_finding_helpers_spec.rb' - './spec/models/concerns/vulnerability_finding_signature_helpers_spec.rb' @@ -8956,7 +8949,6 @@ - './spec/requests/groups/settings/access_tokens_controller_spec.rb' - './spec/requests/groups/settings/applications_controller_spec.rb' - './spec/requests/health_controller_spec.rb' -- './spec/requests/ide_controller_spec.rb' - './spec/requests/import/gitlab_groups_controller_spec.rb' - './spec/requests/import/gitlab_projects_controller_spec.rb' - './spec/requests/import/url_controller_spec.rb' @@ -10381,18 +10373,14 @@ - './spec/workers/clusters/integrations/check_prometheus_health_worker_spec.rb' - './spec/workers/concerns/application_worker_spec.rb' - './spec/workers/concerns/cluster_agent_queue_spec.rb' -- './spec/workers/concerns/cluster_queue_spec.rb' - './spec/workers/concerns/cronjob_queue_spec.rb' - './spec/workers/concerns/gitlab/github_import/object_importer_spec.rb' -- './spec/workers/concerns/gitlab/github_import/queue_spec.rb' - './spec/workers/concerns/gitlab/github_import/rescheduling_methods_spec.rb' - './spec/workers/concerns/gitlab/github_import/stage_methods_spec.rb' - './spec/workers/concerns/gitlab/notify_upon_death_spec.rb' - './spec/workers/concerns/limited_capacity/job_tracker_spec.rb' - './spec/workers/concerns/limited_capacity/worker_spec.rb' - './spec/workers/concerns/packages/cleanup_artifact_worker_spec.rb' -- './spec/workers/concerns/pipeline_background_queue_spec.rb' -- './spec/workers/concerns/pipeline_queue_spec.rb' - './spec/workers/concerns/project_import_options_spec.rb' - './spec/workers/concerns/reenqueuer_spec.rb' - './spec/workers/concerns/repository_check_queue_spec.rb' diff --git a/spec/support/services/import_csv_service_shared_examples.rb b/spec/support/services/import_csv_service_shared_examples.rb new file mode 100644 index 00000000000..1555497ae48 --- /dev/null +++ b/spec/support/services/import_csv_service_shared_examples.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.shared_examples_for 'importer with email notification' do + it 'notifies user of import result' do + expect(Notify).to receive_message_chain(email_method, :deliver_later) + + subject + end +end + +RSpec.shared_examples 'correctly handles invalid files' do + shared_examples_for 'invalid file' do + it 'returns invalid file error' do + expect(subject[:success]).to eq(0) + expect(subject[:parse_error]).to eq(true) + end + end + + context 'when given file with unsupported extension' do + let(:file) { fixture_file_upload('spec/fixtures/banana_sample.gif') } + + it_behaves_like 'invalid file' + end + + context 'when given empty file' do + let(:file) { fixture_file_upload('spec/fixtures/csv_empty.csv') } + + it_behaves_like 'invalid file' + end + + context 'when given file without headers' do + let(:file) { fixture_file_upload('spec/fixtures/csv_no_headers.csv') } + + it_behaves_like 'invalid file' + end +end diff --git a/spec/support/services/issuable_import_csv_service_shared_examples.rb b/spec/support/services/issuable_import_csv_service_shared_examples.rb index 0dea6cfb729..71740ba8ab2 100644 --- a/spec/support/services/issuable_import_csv_service_shared_examples.rb +++ b/spec/support/services/issuable_import_csv_service_shared_examples.rb @@ -18,45 +18,14 @@ RSpec.shared_examples 'issuable import csv service' do |issuable_type| end end - shared_examples_for 'importer with email notification' do - it 'notifies user of import result' do - expect(Notify).to receive_message_chain(email_method, :deliver_later) - - subject - end - end - - shared_examples_for 'invalid file' do - it 'returns invalid file error' do - expect(subject[:success]).to eq(0) - expect(subject[:parse_error]).to eq(true) - end - - it_behaves_like 'importer with email notification' - it_behaves_like 'an issuable importer' - end - describe '#execute' do before do project.add_developer(user) end - context 'invalid file extension' do - let(:file) { fixture_file_upload('spec/fixtures/banana_sample.gif') } - - it_behaves_like 'invalid file' - end - - context 'empty file' do - let(:file) { fixture_file_upload('spec/fixtures/csv_empty.csv') } - - it_behaves_like 'invalid file' - end - - context 'file without headers' do - let(:file) { fixture_file_upload('spec/fixtures/csv_no_headers.csv') } - - it_behaves_like 'invalid file' + it_behaves_like 'correctly handles invalid files' do + it_behaves_like 'importer with email notification' + it_behaves_like 'an issuable importer' end context 'with a file generated by Gitlab CSV export' do diff --git a/spec/support/shared_contexts/features/integrations/integrations_shared_context.rb b/spec/support/shared_contexts/features/integrations/integrations_shared_context.rb index 2c92ef64815..9612b657093 100644 --- a/spec/support/shared_contexts/features/integrations/integrations_shared_context.rb +++ b/spec/support/shared_contexts/features/integrations/integrations_shared_context.rb @@ -74,6 +74,12 @@ Integration.available_integration_names.each do |integration| hash.merge!(k => File.read('spec/fixtures/ssl_key.pem')) elsif integration == 'apple_app_store' && k == :app_store_key_id hash.merge!(k => 'ABC1') + elsif integration == 'apple_app_store' && k == :app_store_private_key_file_name + hash.merge!(k => 'ssl_key.pem') + elsif integration == 'google_play' && k == :service_account_key + hash.merge!(k => File.read('spec/fixtures/service_account.json')) + elsif integration == 'google_play' && k == :service_account_key_file_name + hash.merge!(k => 'service_account.json') else hash.merge!(k => "someword") end diff --git a/spec/support/shared_contexts/navbar_structure_context.rb b/spec/support/shared_contexts/navbar_structure_context.rb index b74819d2ac7..866a97b0e3c 100644 --- a/spec/support/shared_contexts/navbar_structure_context.rb +++ b/spec/support/shared_contexts/navbar_structure_context.rb @@ -5,10 +5,10 @@ RSpec.shared_context 'project navbar structure' do let(:security_and_compliance_nav_item) do { - nav_item: _('Security & Compliance'), + nav_item: _('Security and Compliance'), nav_sub_items: [ (_('Audit events') if Gitlab.ee?), - _('Configuration') + _('Security configuration') ] } end @@ -34,10 +34,10 @@ RSpec.shared_context 'project navbar structure' do _('Commits'), _('Branches'), _('Tags'), - _('Contributors'), + _('Contributor statistics'), _('Graph'), - _('Compare'), - (_('Locked Files') if Gitlab.ee?) + _('Compare revisions'), + (_('Locked files') if Gitlab.ee?) ] }, { @@ -85,8 +85,7 @@ RSpec.shared_context 'project navbar structure' do _('Metrics'), _('Error Tracking'), _('Alerts'), - _('Incidents'), - _('Airflow') + _('Incidents') ] }, { @@ -141,6 +140,7 @@ RSpec.shared_context 'group navbar structure' do _('CI/CD'), _('Applications'), _('Packages and registries'), + s_('UsageQuota|Usage Quotas'), _('Domain Verification') ] } @@ -155,18 +155,9 @@ RSpec.shared_context 'group navbar structure' do } end - let(:administration_nav_item) do - { - nav_item: _('Administration'), - nav_sub_items: [ - s_('UsageQuota|Usage Quotas') - ] - } - end - let(:security_and_compliance_nav_item) do { - nav_item: _('Security & Compliance'), + nav_item: _('Security and Compliance'), nav_sub_items: [ _('Audit events') ] @@ -268,3 +259,30 @@ RSpec.shared_context 'dashboard navbar structure' do ] end end + +RSpec.shared_context '"Explore" navbar structure' do + let(:structure) do + [ + { + nav_item: "Explore", + nav_sub_items: [] + }, + { + nav_item: _("Projects"), + nav_sub_items: [] + }, + { + nav_item: _("Groups"), + nav_sub_items: [] + }, + { + nav_item: _("Topics"), + nav_sub_items: [] + }, + { + nav_item: _("Snippets"), + nav_sub_items: [] + } + ] + end +end diff --git a/spec/support/shared_contexts/policies/group_policy_shared_context.rb b/spec/support/shared_contexts/policies/group_policy_shared_context.rb index 4c081c8464e..a9b12d47393 100644 --- a/spec/support/shared_contexts/policies/group_policy_shared_context.rb +++ b/spec/support/shared_contexts/policies/group_policy_shared_context.rb @@ -42,9 +42,7 @@ RSpec.shared_context 'GroupPolicy context' do let(:developer_permissions) do %i[ - create_metrics_dashboard_annotation - delete_metrics_dashboard_annotation - update_metrics_dashboard_annotation + admin_metrics_dashboard_annotation create_custom_emoji create_package read_cluster @@ -59,6 +57,7 @@ RSpec.shared_context 'GroupPolicy context' do create_cluster update_cluster admin_cluster add_cluster destroy_upload admin_achievement + award_achievement ] end diff --git a/spec/support/shared_contexts/policies/project_policy_shared_context.rb b/spec/support/shared_contexts/policies/project_policy_shared_context.rb index afc7fc8766f..5014a810f35 100644 --- a/spec/support/shared_contexts/policies/project_policy_shared_context.rb +++ b/spec/support/shared_contexts/policies/project_policy_shared_context.rb @@ -38,8 +38,8 @@ RSpec.shared_context 'ProjectPolicy context' do read_commit_status read_confidential_issues read_container_image read_harbor_registry read_deployment read_environment read_merge_request read_metrics_dashboard_annotation read_pipeline read_prometheus - read_sentry_issue update_issue create_merge_request_in read_external_emails - read_internal_note + read_sentry_issue update_issue create_merge_request_in + read_external_emails read_internal_note export_work_items ] end @@ -52,12 +52,11 @@ RSpec.shared_context 'ProjectPolicy context' do admin_merge_request admin_tag create_build create_commit_status create_container_image create_deployment create_environment create_merge_request_from - create_metrics_dashboard_annotation create_pipeline create_release - create_wiki delete_metrics_dashboard_annotation - destroy_container_image push_code read_pod_logs read_terraform_state - resolve_note update_build update_commit_status update_container_image - update_deployment update_environment update_merge_request - update_metrics_dashboard_annotation update_pipeline update_release destroy_release + admin_metrics_dashboard_annotation create_pipeline create_release + create_wiki destroy_container_image push_code read_pod_logs + read_terraform_state resolve_note update_build update_commit_status + update_container_image update_deployment update_environment + update_merge_request update_pipeline update_release destroy_release read_resource_group update_resource_group update_escalation_status ] end diff --git a/spec/support/shared_contexts/rack_attack_shared_context.rb b/spec/support/shared_contexts/rack_attack_shared_context.rb index 12625ead72b..4e8c6e9dec6 100644 --- a/spec/support/shared_contexts/rack_attack_shared_context.rb +++ b/spec/support/shared_contexts/rack_attack_shared_context.rb @@ -5,8 +5,7 @@ RSpec.shared_context 'rack attack cache store' do # Instead of test environment's :null_store so the throttles can increment Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new - # Make time-dependent tests deterministic - freeze_time { example.run } + example.run Rack::Attack.cache.store = Rails.cache end diff --git a/spec/support/shared_contexts/requests/api/debian_repository_shared_context.rb b/spec/support/shared_contexts/requests/api/debian_repository_shared_context.rb index 57967fb9414..ad64e4d5be5 100644 --- a/spec/support/shared_contexts/requests/api/debian_repository_shared_context.rb +++ b/spec/support/shared_contexts/requests/api/debian_repository_shared_context.rb @@ -58,6 +58,9 @@ RSpec.shared_context 'Debian repository shared context' do |container_type, can_ let(:distribution) { { private: private_distribution, public: public_distribution }[visibility_level] } let(:architecture) { { private: private_architecture, public: public_architecture }[visibility_level] } let(:component) { { private: private_component, public: public_component }[visibility_level] } + let(:component_file) { { private: private_component_file, public: public_component_file }[visibility_level] } + let(:component_file_sources) { { private: private_component_file_sources, public: public_component_file_sources }[visibility_level] } + let(:component_file_di) { { private: private_component_file_di, public: public_component_file_di }[visibility_level] } let(:component_file_older_sha256) { { private: private_component_file_older_sha256, public: public_component_file_older_sha256 }[visibility_level] } let(:component_file_sources_older_sha256) { { private: private_component_file_sources_older_sha256, public: public_component_file_sources_older_sha256 }[visibility_level] } let(:component_file_di_older_sha256) { { private: private_component_file_di_older_sha256, public: public_component_file_di_older_sha256 }[visibility_level] } 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 index dc5195e4b01..1fa1a5c69f4 100644 --- a/spec/support/shared_contexts/security_and_compliance_permissions_shared_context.rb +++ b/spec/support/shared_contexts/security_and_compliance_permissions_shared_context.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -RSpec.shared_context '"Security & Compliance" permissions' do +RSpec.shared_context '"Security and Compliance" permissions' do let(:project_instance) { an_instance_of(Project) } let(:user_instance) { an_instance_of(User) } let(:before_request_defined) { false } @@ -18,7 +18,7 @@ RSpec.shared_context '"Security & Compliance" permissions' do 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 + context 'when the "Security and Compliance" feature is disabled' do subject { response } before do diff --git a/spec/support/shared_examples/analytics/cycle_analytics/flow_metrics_examples.rb b/spec/support/shared_examples/analytics/cycle_analytics/flow_metrics_examples.rb new file mode 100644 index 00000000000..b324a5886a9 --- /dev/null +++ b/spec/support/shared_examples/analytics/cycle_analytics/flow_metrics_examples.rb @@ -0,0 +1,464 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'value stream analytics flow metrics issueCount examples' do + let_it_be(:milestone) { create(:milestone, group: group) } + let_it_be(:label) { create(:group_label, group: group) } + + let_it_be(:author) { create(:user) } + let_it_be(:assignee) { create(:user) } + + let_it_be(:issue1) { create(:issue, project: project1, author: author, created_at: 12.days.ago) } + let_it_be(:issue2) { create(:issue, project: project2, author: author, created_at: 13.days.ago) } + + let_it_be(:issue3) do + create(:labeled_issue, + project: project1, + labels: [label], + author: author, + milestone: milestone, + assignees: [assignee], + created_at: 14.days.ago) + end + + let_it_be(:issue4) do + create(:labeled_issue, + project: project2, + labels: [label], + assignees: [assignee], + created_at: 15.days.ago) + end + + let_it_be(:issue_outside_of_the_range) { create(:issue, project: project2, author: author, created_at: 50.days.ago) } + + let(:query) do + <<~QUERY + query($path: ID!, $assigneeUsernames: [String!], $authorUsername: String, $milestoneTitle: String, $labelNames: [String!], $from: Time!, $to: Time!) { + #{context}(fullPath: $path) { + flowMetrics { + issueCount(assigneeUsernames: $assigneeUsernames, authorUsername: $authorUsername, milestoneTitle: $milestoneTitle, labelNames: $labelNames, from: $from, to: $to) { + value + unit + identifier + title + } + } + } + } + QUERY + end + + let(:variables) do + { + path: full_path, + from: 20.days.ago.iso8601, + to: 10.days.ago.iso8601 + } + end + + subject(:result) do + post_graphql(query, current_user: current_user, variables: variables) + + graphql_data.dig(context.to_s, 'flowMetrics', 'issueCount') + end + + it 'returns the correct count' do + expect(result).to eq({ + 'identifier' => 'issues', + 'unit' => nil, + 'value' => 4, + 'title' => n_('New Issue', 'New Issues', 4) + }) + end + + context 'with partial filters' do + let(:variables) do + { + path: full_path, + assigneeUsernames: [assignee.username], + labelNames: [label.title], + from: 20.days.ago.iso8601, + to: 10.days.ago.iso8601 + } + end + + it 'returns filtered count' do + expect(result).to eq({ + 'identifier' => 'issues', + 'unit' => nil, + 'value' => 2, + 'title' => n_('New Issue', 'New Issues', 2) + }) + end + end + + context 'with all filters' do + let(:variables) do + { + path: full_path, + assigneeUsernames: [assignee.username], + labelNames: [label.title], + authorUsername: author.username, + milestoneTitle: milestone.title, + from: 20.days.ago.iso8601, + to: 10.days.ago.iso8601 + } + end + + it 'returns filtered count' do + expect(result).to eq({ + 'identifier' => 'issues', + 'unit' => nil, + 'value' => 1, + 'title' => n_('New Issue', 'New Issues', 1) + }) + end + end + + context 'when the user is not authorized' do + let(:current_user) { create(:user) } + + it 'returns nil' do + expect(result).to eq(nil) + end + end +end + +RSpec.shared_examples 'value stream analytics flow metrics deploymentCount examples' do + let_it_be(:deployment1) do + create(:deployment, :success, environment: production_environment1, finished_at: 5.days.ago) + end + + let_it_be(:deployment2) do + create(:deployment, :success, environment: production_environment2, finished_at: 10.days.ago) + end + + let_it_be(:deployment3) do + create(:deployment, :success, environment: production_environment2, finished_at: 15.days.ago) + end + + let(:variables) do + { + path: full_path, + from: 12.days.ago.iso8601, + to: 3.days.ago.iso8601 + } + end + + let(:query) do + <<~QUERY + query($path: ID!, $from: Time!, $to: Time!) { + #{context}(fullPath: $path) { + flowMetrics { + deploymentCount(from: $from, to: $to) { + value + unit + identifier + title + } + } + } + } + QUERY + end + + subject(:result) do + post_graphql(query, current_user: current_user, variables: variables) + + graphql_data.dig(context.to_s, 'flowMetrics', 'deploymentCount') + end + + it 'returns the correct count' do + expect(result).to eq({ + 'identifier' => 'deploys', + 'unit' => nil, + 'value' => 2, + 'title' => n_('Deploy', 'Deploys', 2) + }) + end + + context 'when the user is not authorized' do + let(:current_user) { create(:user) } + + it 'returns nil' do + expect(result).to eq(nil) + end + end + + context 'when outside of the date range' do + let(:variables) do + { + path: full_path, + from: 20.days.ago.iso8601, + to: 18.days.ago.iso8601 + } + end + + it 'returns 0 count' do + expect(result).to eq({ + 'identifier' => 'deploys', + 'unit' => nil, + 'value' => 0, + 'title' => n_('Deploy', 'Deploys', 0) + }) + end + end +end + +RSpec.shared_examples 'value stream analytics flow metrics leadTime examples' do + let_it_be(:milestone) { create(:milestone, group: group) } + let_it_be(:label) { create(:group_label, group: group) } + + let_it_be(:author) { create(:user) } + let_it_be(:assignee) { create(:user) } + + let_it_be(:issue1) do + create(:issue, project: project1, author: author, created_at: 17.days.ago, closed_at: 12.days.ago) + end + + let_it_be(:issue2) do + create(:issue, project: project2, author: author, created_at: 16.days.ago, closed_at: 13.days.ago) + end + + let_it_be(:issue3) do + create(:labeled_issue, + project: project1, + labels: [label], + author: author, + milestone: milestone, + assignees: [assignee], + created_at: 14.days.ago, + closed_at: 11.days.ago) + end + + let_it_be(:issue4) do + create(:labeled_issue, + project: project2, + labels: [label], + assignees: [assignee], + created_at: 20.days.ago, + closed_at: 15.days.ago) + end + + before do + Analytics::CycleAnalytics::DataLoaderService.new(group: group, model: Issue).execute + end + + let(:query) do + <<~QUERY + query($path: ID!, $assigneeUsernames: [String!], $authorUsername: String, $milestoneTitle: String, $labelNames: [String!], $from: Time!, $to: Time!) { + #{context}(fullPath: $path) { + flowMetrics { + leadTime(assigneeUsernames: $assigneeUsernames, authorUsername: $authorUsername, milestoneTitle: $milestoneTitle, labelNames: $labelNames, from: $from, to: $to) { + value + unit + identifier + title + links { + label + url + } + } + } + } + } + QUERY + end + + let(:variables) do + { + path: full_path, + from: 21.days.ago.iso8601, + to: 10.days.ago.iso8601 + } + end + + subject(:result) do + post_graphql(query, current_user: current_user, variables: variables) + + graphql_data.dig(context.to_s, 'flowMetrics', 'leadTime') + end + + it 'returns the correct value' do + expect(result).to match(a_hash_including({ + 'identifier' => 'lead_time', + 'unit' => n_('day', 'days', 4), + 'value' => 4, + 'title' => _('Lead Time'), + 'links' => [ + { 'label' => s_('ValueStreamAnalytics|Dashboard'), 'url' => match(/issues_analytics/) }, + { 'label' => s_('ValueStreamAnalytics|Go to docs'), 'url' => match(/definitions/) } + ] + })) + end + + context 'when the user is not authorized' do + let(:current_user) { create(:user) } + + it 'returns nil' do + expect(result).to eq(nil) + end + end + + context 'when outside of the date range' do + let(:variables) do + { + path: full_path, + from: 30.days.ago.iso8601, + to: 25.days.ago.iso8601 + } + end + + it 'returns 0 count' do + expect(result).to match(a_hash_including({ 'value' => nil })) + end + end + + context 'with all filters' do + let(:variables) do + { + path: full_path, + assigneeUsernames: [assignee.username], + labelNames: [label.title], + authorUsername: author.username, + milestoneTitle: milestone.title, + from: 20.days.ago.iso8601, + to: 10.days.ago.iso8601 + } + end + + it 'returns filtered count' do + expect(result).to match(a_hash_including({ 'value' => 3 })) + end + end +end + +RSpec.shared_examples 'value stream analytics flow metrics cycleTime examples' do + let_it_be(:milestone) { create(:milestone, group: group) } + let_it_be(:label) { create(:group_label, group: group) } + + let_it_be(:author) { create(:user) } + let_it_be(:assignee) { create(:user) } + + let_it_be(:issue1) do + create(:issue, project: project1, author: author, closed_at: 12.days.ago).tap do |issue| + issue.metrics.update!(first_mentioned_in_commit_at: 17.days.ago) + end + end + + let_it_be(:issue2) do + create(:issue, project: project2, author: author, closed_at: 13.days.ago).tap do |issue| + issue.metrics.update!(first_mentioned_in_commit_at: 16.days.ago) + end + end + + let_it_be(:issue3) do + create(:labeled_issue, + project: project1, + labels: [label], + author: author, + milestone: milestone, + assignees: [assignee], + closed_at: 11.days.ago).tap do |issue| + issue.metrics.update!(first_mentioned_in_commit_at: 14.days.ago) + end + end + + let_it_be(:issue4) do + create(:labeled_issue, + project: project2, + labels: [label], + assignees: [assignee], + closed_at: 15.days.ago).tap do |issue| + issue.metrics.update!(first_mentioned_in_commit_at: 20.days.ago) + end + end + + before do + Analytics::CycleAnalytics::DataLoaderService.new(group: group, model: Issue).execute + end + + let(:query) do + <<~QUERY + query($path: ID!, $assigneeUsernames: [String!], $authorUsername: String, $milestoneTitle: String, $labelNames: [String!], $from: Time!, $to: Time!) { + #{context}(fullPath: $path) { + flowMetrics { + cycleTime(assigneeUsernames: $assigneeUsernames, authorUsername: $authorUsername, milestoneTitle: $milestoneTitle, labelNames: $labelNames, from: $from, to: $to) { + value + unit + identifier + title + links { + label + url + } + } + } + } + } + QUERY + end + + let(:variables) do + { + path: full_path, + from: 21.days.ago.iso8601, + to: 10.days.ago.iso8601 + } + end + + subject(:result) do + post_graphql(query, current_user: current_user, variables: variables) + + graphql_data.dig(context.to_s, 'flowMetrics', 'cycleTime') + end + + it 'returns the correct value' do + expect(result).to eq({ + 'identifier' => 'cycle_time', + 'unit' => n_('day', 'days', 4), + 'value' => 4, + 'title' => _('Cycle Time'), + 'links' => [] + }) + end + + context 'when the user is not authorized' do + let(:current_user) { create(:user) } + + it 'returns nil' do + expect(result).to eq(nil) + end + end + + context 'when outside of the date range' do + let(:variables) do + { + path: full_path, + from: 30.days.ago.iso8601, + to: 25.days.ago.iso8601 + } + end + + it 'returns 0 count' do + expect(result).to match(a_hash_including({ 'value' => nil })) + end + end + + context 'with all filters' do + let(:variables) do + { + path: full_path, + assigneeUsernames: [assignee.username], + labelNames: [label.title], + authorUsername: author.username, + milestoneTitle: milestone.title, + from: 20.days.ago.iso8601, + to: 10.days.ago.iso8601 + } + end + + it 'returns filtered count' do + expect(result).to match(a_hash_including({ 'value' => 3 })) + end + end +end diff --git a/spec/support/shared_examples/analytics/cycle_analytics/request_params_examples.rb b/spec/support/shared_examples/analytics/cycle_analytics/request_params_examples.rb new file mode 100644 index 00000000000..f1ffddf6507 --- /dev/null +++ b/spec/support/shared_examples/analytics/cycle_analytics/request_params_examples.rb @@ -0,0 +1,127 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'unlicensed cycle analytics request params' do + let(:params) do + { + created_after: '2019-01-01', + created_before: '2019-03-01', + project_ids: [2, 3], + namespace: namespace, + current_user: user + } + end + + subject { described_class.new(params) } + + before do + root_group.add_owner(user) + end + + describe 'validations' do + it 'is valid' do + expect(subject).to be_valid + end + + context 'when `created_before` is missing' do + before do + params[:created_before] = nil + end + + it 'is valid', time_travel_to: '2019-03-01' do + expect(subject).to be_valid + end + end + + context 'when `created_before` is earlier than `created_after`' do + before do + params[:created_before] = '2015-01-01' + end + + it 'is invalid' do + expect(subject).not_to be_valid + expect(subject.errors.messages[:created_before]).not_to be_empty + end + end + + context 'when the date range exceeds 180 days' do + before do + params[:created_before] = '2019-07-15' + end + + it 'is invalid' do + expect(subject).not_to be_valid + message = s_('CycleAnalytics|The given date range is larger than 180 days') + expect(subject.errors.messages[:created_after]).to include(message) + end + end + end + + it 'casts `created_after` to `Time`' do + expect(subject.created_after).to be_a_kind_of(Time) + end + + it 'casts `created_before` to `Time`' do + expect(subject.created_before).to be_a_kind_of(Time) + end + + describe 'optional `value_stream`' do + context 'when `value_stream` is not empty' do + let(:value_stream) { instance_double('Analytics::CycleAnalytics::ValueStream') } + + before do + params[:value_stream] = value_stream + end + + it { expect(subject.value_stream).to eq(value_stream) } + end + + context 'when `value_stream` is nil' do + before do + params[:value_stream] = nil + end + + it { expect(subject.value_stream).to eq(nil) } + end + end + + describe 'sorting params' do + before do + params.merge!(sort: 'duration', direction: 'asc') + end + + it 'converts sorting params to symbol when passing it to data collector' do + data_collector_params = subject.to_data_collector_params + + expect(data_collector_params[:sort]).to eq(:duration) + expect(data_collector_params[:direction]).to eq(:asc) + end + + it 'adds sorting params to data attributes' do + data_attributes = subject.to_data_attributes + + expect(data_attributes[:sort]).to eq('duration') + expect(data_attributes[:direction]).to eq('asc') + end + end + + describe 'aggregation params' do + context 'when not licensed' do + it 'returns nil' do + data_collector_params = subject.to_data_attributes + expect(data_collector_params[:aggregation]).to eq(nil) + end + end + end + + describe 'use_aggregated_data_collector param' do + subject(:value) { described_class.new(params).to_data_collector_params[:use_aggregated_data_collector] } + + it { is_expected.to eq(false) } + end + + describe 'enable_tasks_by_type_chart data attribute' do + subject(:value) { described_class.new(params).to_data_attributes[:enable_tasks_by_type_chart] } + + it { is_expected.to eq('false') } + 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 7df4b7635d3..ddd3bbd636a 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 @@ -80,7 +80,7 @@ RSpec.shared_examples 'multiple issue boards' do click_button 'Select a label' - page.choose(planning.title) + find('label', text: planning.title).click click_button 'Add to board' diff --git a/spec/support/shared_examples/controllers/repositories/git_http_controller_shared_examples.rb b/spec/support/shared_examples/controllers/repositories/git_http_controller_shared_examples.rb index cc28a79b4ca..e75188f8249 100644 --- a/spec/support/shared_examples/controllers/repositories/git_http_controller_shared_examples.rb +++ b/spec/support/shared_examples/controllers/repositories/git_http_controller_shared_examples.rb @@ -60,19 +60,6 @@ RSpec.shared_examples Repositories::GitHttpController do expect(response).to have_gitlab_http_status(:ok) end - it 'updates the user activity' do - activity_project = container.is_a?(PersonalSnippet) ? nil : project - - activity_service = instance_double(Users::ActivityService) - - args = { author: user, project: activity_project, namespace: activity_project&.namespace } - expect(Users::ActivityService).to receive(:new).with(args).and_return(activity_service) - - expect(activity_service).to receive(:execute) - - get :info_refs, params: params - end - include_context 'parsed logs' do it 'adds user info to the logs' do get :info_refs, params: params @@ -87,14 +74,20 @@ RSpec.shared_examples Repositories::GitHttpController do end describe 'POST #git_upload_pack' do - before do + it 'returns 200' do allow(controller).to receive(:verify_workhorse_api!).and_return(true) - end - it 'returns 200' do post :git_upload_pack, params: params expect(response).to have_gitlab_http_status(:ok) end + + context 'when JWT token is not provided' do + it 'returns 403' do + post :git_upload_pack, params: params + + expect(response).to have_gitlab_http_status(:forbidden) + end + end end end diff --git a/spec/support/shared_examples/features/2fa_shared_examples.rb b/spec/support/shared_examples/features/2fa_shared_examples.rb index 44f30c32472..b6339607d6b 100644 --- a/spec/support/shared_examples/features/2fa_shared_examples.rb +++ b/spec/support/shared_examples/features/2fa_shared_examples.rb @@ -6,8 +6,6 @@ RSpec.shared_examples 'hardware device for 2fa' do |device_type| def register_device(device_type, **kwargs) case device_type.downcase - when "u2f" - register_u2f_device(**kwargs) when "webauthn" register_webauthn_device(**kwargs) else diff --git a/spec/support/shared_examples/features/abuse_report_shared_examples.rb b/spec/support/shared_examples/features/abuse_report_shared_examples.rb new file mode 100644 index 00000000000..7a520fb0cd2 --- /dev/null +++ b/spec/support/shared_examples/features/abuse_report_shared_examples.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'reports the user with an abuse category' do + it 'creates abuse report' do + click_button 'Report abuse to administrator' + choose "They're posting spam." + click_button 'Next' + + fill_in 'abuse_report_message', with: 'This user sends spam' + click_button 'Send report' + + expect(page).to have_content 'Thank you for your report' + end +end diff --git a/spec/support/shared_examples/features/content_editor_shared_examples.rb b/spec/support/shared_examples/features/content_editor_shared_examples.rb index 6cd9c4ce1c4..7582e67efbd 100644 --- a/spec/support/shared_examples/features/content_editor_shared_examples.rb +++ b/spec/support/shared_examples/features/content_editor_shared_examples.rb @@ -1,44 +1,9 @@ # frozen_string_literal: true RSpec.shared_examples 'edits content using the content editor' do - let(:content_editor_testid) { '[data-testid="content-editor"] [contenteditable].ProseMirror' } - - def switch_to_content_editor - click_button _('View rich text') - click_button _('Rich text') - end - - def type_in_content_editor(keys) - find(content_editor_testid).send_keys keys - end - - def open_insert_media_dropdown - page.find('svg[data-testid="media-icon"]').click - end - - def set_source_editor_content(content) - find('.js-gfm-input').set content - end - - def expect_formatting_menu_to_be_visible - expect(page).to have_css('[data-testid="formatting-bubble-menu"]') - end - - def expect_formatting_menu_to_be_hidden - expect(page).not_to have_css('[data-testid="formatting-bubble-menu"]') - end - - def expect_media_bubble_menu_to_be_visible - expect(page).to have_css('[data-testid="media-bubble-menu"]') - end + include ContentEditorHelpers - def upload_asset(fixture_name) - attach_file('content_editor_image', Rails.root.join('spec', 'fixtures', fixture_name), make_visible: true) - end - - def wait_until_hidden_field_is_updated(value) - expect(page).to have_field('wiki[content]', with: value, type: 'hidden') - end + let(:content_editor_testid) { '[data-testid="content-editor"] [contenteditable].ProseMirror' } it 'saves page content in local storage if the user navigates away' do switch_to_content_editor @@ -52,16 +17,6 @@ RSpec.shared_examples 'edits content using the content editor' do refresh expect(page).to have_text('Typing text in the content editor') - - refresh # also retained after second refresh - - expect(page).to have_text('Typing text in the content editor') - - click_link 'Cancel' # draft is deleted on cancel - - page.go_back - - expect(page).not_to have_text('Typing text in the content editor') end describe 'formatting bubble menu' do @@ -92,25 +47,18 @@ RSpec.shared_examples 'edits content using the content editor' do open_insert_media_dropdown end - def test_displays_media_bubble_menu(media_element_selector, fixture_file) - upload_asset fixture_file - - wait_for_requests - - expect(page).to have_css(media_element_selector) - - page.find(media_element_selector).click + it 'displays correct media bubble menu for images', :js do + display_media_bubble_menu '[data-testid="content_editor_editablebox"] img[src]', 'dk.png' expect_formatting_menu_to_be_hidden expect_media_bubble_menu_to_be_visible end - it 'displays correct media bubble menu for images', :js do - test_displays_media_bubble_menu '[data-testid="content_editor_editablebox"] img[src]', 'dk.png' - end - it 'displays correct media bubble menu for video', :js do - test_displays_media_bubble_menu '[data-testid="content_editor_editablebox"] video', 'video_sample.mp4' + display_media_bubble_menu '[data-testid="content_editor_editablebox"] video', 'video_sample.mp4' + + expect_formatting_menu_to_be_hidden + expect_media_bubble_menu_to_be_visible end end @@ -225,7 +173,7 @@ RSpec.shared_examples 'edits content using the content editor' do before do if defined?(project) create(:issue, project: project, title: 'My Cool Linked Issue') - create(:merge_request, source_project: project, title: 'My Cool Merge Request') + create(:merge_request, source_project: project, source_branch: 'branch-1', title: 'My Cool Merge Request') create(:label, project: project, title: 'My Cool Label') create(:milestone, project: project, title: 'My Cool Milestone') @@ -234,7 +182,7 @@ RSpec.shared_examples 'edits content using the content editor' do project = create(:project, group: group) create(:issue, project: project, title: 'My Cool Linked Issue') - create(:merge_request, source_project: project, title: 'My Cool Merge Request') + create(:merge_request, source_project: project, source_branch: 'branch-1', title: 'My Cool Merge Request') create(:group_label, group: group, title: 'My Cool Label') create(:milestone, group: group, title: 'My Cool Milestone') @@ -251,7 +199,9 @@ RSpec.shared_examples 'edits content using the content editor' do expect(find(suggestions_dropdown)).to have_text('abc123') expect(find(suggestions_dropdown)).to have_text('all') - expect(find(suggestions_dropdown)).to have_text('Group Members (2)') + expect(find(suggestions_dropdown)).to have_text('Group Members') + + type_in_content_editor 'bc' send_keys [:arrow_down, :enter] @@ -332,3 +282,24 @@ RSpec.shared_examples 'edits content using the content editor' do end end end + +RSpec.shared_examples 'inserts diagrams.net diagram using the content editor' do + include ContentEditorHelpers + + before do + switch_to_content_editor + + open_insert_media_dropdown + end + + it 'displays correct media bubble menu with edit diagram button' do + display_media_bubble_menu '[data-testid="content_editor_editablebox"] img[src]', 'diagram.drawio.svg' + + expect_formatting_menu_to_be_hidden + expect_media_bubble_menu_to_be_visible + + click_edit_diagram_button + + expect_drawio_editor_is_opened + end +end diff --git a/spec/support/shared_examples/features/incident_details_routing_shared_examples.rb b/spec/support/shared_examples/features/incident_details_routing_shared_examples.rb index dab125caa60..b8e42843e6f 100644 --- a/spec/support/shared_examples/features/incident_details_routing_shared_examples.rb +++ b/spec/support/shared_examples/features/incident_details_routing_shared_examples.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -RSpec.shared_examples 'for each incident details route' do |example, tab_text:| +RSpec.shared_examples 'for each incident details route' do |example, tab_text:, tab:| before do sign_in(user) visit incident_path @@ -25,4 +25,16 @@ RSpec.shared_examples 'for each incident details route' do |example, tab_text:| it_behaves_like example end + + context "for /-/issues/incident/:id/#{tab} route" do + let(:incident_path) { incident_project_issues_path(project, incident, tab) } + + it_behaves_like example + end + + context "for /-/issues/:id/#{tab} route" do + let(:incident_path) { incident_issue_project_issue_path(project, incident, tab) } + + it_behaves_like example + end end diff --git a/spec/support/shared_examples/features/integrations/user_activates_mattermost_slash_command_integration_shared_examples.rb b/spec/support/shared_examples/features/integrations/user_activates_mattermost_slash_command_integration_shared_examples.rb index 4c312b42c0a..148ff2cfb54 100644 --- a/spec/support/shared_examples/features/integrations/user_activates_mattermost_slash_command_integration_shared_examples.rb +++ b/spec/support/shared_examples/features/integrations/user_activates_mattermost_slash_command_integration_shared_examples.rb @@ -8,7 +8,7 @@ RSpec.shared_examples 'user activates the Mattermost Slash Command integration' it 'shows a token placeholder' do token_placeholder = find_field('service_token')['placeholder'] - expect(token_placeholder).to eq('XXxxXXxxXXxxXXxxXXxxXXxx') + expect(token_placeholder).to eq('') end it 'redirects to the integrations page after saving but not activating' do 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 b6f7094e422..13adcfe9191 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 @@ -17,8 +17,6 @@ RSpec.shared_examples 'issuable invite members' do page.within '.dropdown-menu-user' do expect(page).to have_link('Invite Members') - expect(page).to have_selector('[data-track-action="click_invite_members"]') - expect(page).to have_selector('[data-track-label="edit_assignee"]') end click_link 'Invite Members' diff --git a/spec/support/shared_examples/features/manage_applications_shared_examples.rb b/spec/support/shared_examples/features/manage_applications_shared_examples.rb index b59f3f1e27b..63ba5832771 100644 --- a/spec/support/shared_examples/features/manage_applications_shared_examples.rb +++ b/spec/support/shared_examples/features/manage_applications_shared_examples.rb @@ -5,87 +5,40 @@ RSpec.shared_examples 'manage applications' do let_it_be(:application_name_changed) { "#{application_name} changed" } let_it_be(:application_redirect_uri) { 'https://foo.bar' } - context 'when hash_oauth_secrets flag set' do - before do - stub_feature_flags(hash_oauth_secrets: true) - end - - it 'allows user to manage applications', :js do - visit new_application_path + it 'allows user to manage applications', :js do + visit new_application_path - expect(page).to have_content 'Add new application' + expect(page).to have_content 'Add new application' - fill_in :doorkeeper_application_name, with: application_name - fill_in :doorkeeper_application_redirect_uri, with: application_redirect_uri - check :doorkeeper_application_scopes_read_user - click_on 'Save application' + fill_in :doorkeeper_application_name, with: application_name + fill_in :doorkeeper_application_redirect_uri, with: application_redirect_uri + check :doorkeeper_application_scopes_read_user + click_on 'Save application' - validate_application(application_name, 'Yes') - expect(page).to have_content _('This is the only time the secret is accessible. Copy the secret and store it securely') - expect(page).to have_link('Continue', href: index_path) + validate_application(application_name, 'Yes') + expect(page).to have_content _('This is the only time the secret is accessible. Copy the secret and store it securely') + expect(page).to have_link('Continue', href: index_path) - expect(page).to have_css("button[title=\"Copy secret\"]", text: 'Copy') + expect(page).to have_css("button[title=\"Copy secret\"]", text: 'Copy') - click_on 'Edit' + click_on 'Edit' - application_name_changed = "#{application_name} changed" + application_name_changed = "#{application_name} changed" - fill_in :doorkeeper_application_name, with: application_name_changed - uncheck :doorkeeper_application_confidential - click_on 'Save application' - - validate_application(application_name_changed, 'No') - expect(page).not_to have_link('Continue') - expect(page).to have_content _('The secret is only available when you first create the application') - - visit_applications_path - - page.within '.oauth-applications' do - click_on 'Destroy' - end - expect(page.find('.oauth-applications')).not_to have_content 'test_changed' - end - end - - context 'when hash_oauth_secrets flag not set' do - before do - stub_feature_flags(hash_oauth_secrets: false) - end - - it 'allows user to manage applications', :js do - visit new_application_path - - expect(page).to have_content 'Add new application' - - fill_in :doorkeeper_application_name, with: application_name - fill_in :doorkeeper_application_redirect_uri, with: application_redirect_uri - check :doorkeeper_application_scopes_read_user - click_on 'Save application' - - validate_application(application_name, 'Yes') - expect(page).to have_link('Continue', href: index_path) - - application = Doorkeeper::Application.find_by(name: application_name) - expect(page).to have_css("button[title=\"Copy secret\"][data-clipboard-text=\"#{application.secret}\"]", text: 'Copy') - - click_on 'Edit' - - application_name_changed = "#{application_name} changed" - - fill_in :doorkeeper_application_name, with: application_name_changed - uncheck :doorkeeper_application_confidential - click_on 'Save application' + fill_in :doorkeeper_application_name, with: application_name_changed + uncheck :doorkeeper_application_confidential + click_on 'Save application' - validate_application(application_name_changed, 'No') - expect(page).not_to have_link('Continue') + validate_application(application_name_changed, 'No') + expect(page).not_to have_link('Continue') + expect(page).to have_content _('The secret is only available when you create the application or renew the secret.') - visit_applications_path + visit_applications_path - page.within '.oauth-applications' do - click_on 'Destroy' - end - expect(page.find('.oauth-applications')).not_to have_content 'test_changed' + page.within '.oauth-applications' do + click_on 'Destroy' end + expect(page.find('.oauth-applications')).not_to have_content 'test_changed' end context 'when scopes are blank' do diff --git a/spec/support/shared_examples/features/protected_tags_with_deploy_keys_examples.rb b/spec/support/shared_examples/features/protected_tags_with_deploy_keys_examples.rb new file mode 100644 index 00000000000..cc0984b6226 --- /dev/null +++ b/spec/support/shared_examples/features/protected_tags_with_deploy_keys_examples.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'Deploy keys with protected tags' do + let(:dropdown_sections_minus_deploy_keys) { all_dropdown_sections - ['Deploy Keys'] } + + context 'when deploy keys are enabled to this project' do + let!(:deploy_key_1) { create(:deploy_key, title: 'title 1', projects: [project]) } + let!(:deploy_key_2) { create(:deploy_key, title: 'title 2', projects: [project]) } + + context 'when only one deploy key can push' do + before do + deploy_key_1.deploy_keys_projects.first.update!(can_push: true) + end + + it "shows all dropdown sections in the 'Allowed to create' main dropdown, with only one deploy key" do + visit project_protected_tags_path(project) + + find(".js-allowed-to-create").click + wait_for_requests + + within('[data-testid="allowed-to-create-dropdown"]') do + dropdown_headers = page.all('.dropdown-header').map(&:text) + + expect(dropdown_headers).to contain_exactly(*all_dropdown_sections) + expect(page).to have_content('title 1') + expect(page).not_to have_content('title 2') + end + end + + it "shows all sections in the 'Allowed to create' update dropdown" do + create(:protected_tag, :no_one_can_create, project: project, name: 'v1.0.0') + + visit project_protected_tags_path(project) + + within(".js-protected-tag-edit-form") do + find(".js-allowed-to-create").click + wait_for_requests + + dropdown_headers = page.all('.dropdown-header').map(&:text) + + expect(dropdown_headers).to contain_exactly(*all_dropdown_sections) + end + end + end + + context 'when no deploy key can push' do + it "just shows all sections but not deploy keys in the 'Allowed to create' dropdown" do + visit project_protected_tags_path(project) + + find(".js-allowed-to-create").click + wait_for_requests + + within('[data-testid="allowed-to-create-dropdown"]') do + dropdown_headers = page.all('.dropdown-header').map(&:text) + + expect(dropdown_headers).to contain_exactly(*dropdown_sections_minus_deploy_keys) + end + end + end + end +end diff --git a/spec/support/shared_examples/features/secure_oauth_authorizations_shared_examples.rb b/spec/support/shared_examples/features/secure_oauth_authorizations_shared_examples.rb index 028e075c87a..231406289b4 100644 --- a/spec/support/shared_examples/features/secure_oauth_authorizations_shared_examples.rb +++ b/spec/support/shared_examples/features/secure_oauth_authorizations_shared_examples.rb @@ -10,7 +10,7 @@ RSpec.shared_examples 'Secure OAuth Authorizations' do end context 'when user is unconfirmed' do - let(:user) { create(:user, confirmed_at: nil) } + let(:user) { create(:user, :unconfirmed) } it 'displays an error' do expect(page).to have_text I18n.t('doorkeeper.errors.messages.unconfirmed_email') diff --git a/spec/support/shared_examples/features/trial_email_validation_shared_example.rb b/spec/support/shared_examples/features/trial_email_validation_shared_example.rb index 8304a91af86..81c9ac1164b 100644 --- a/spec/support/shared_examples/features/trial_email_validation_shared_example.rb +++ b/spec/support/shared_examples/features/trial_email_validation_shared_example.rb @@ -1,59 +1,38 @@ # frozen_string_literal: true RSpec.shared_examples 'user email validation' do - let(:email_hint_message) { 'We recommend a work email address.' } - let(:email_error_message) { 'Please provide a valid email address.' } + let(:email_hint_message) { _('We recommend a work email address.') } + let(:email_error_message) { _('Please provide a valid email address.') } let(:email_warning_message) do - 'This email address does not look right, are you sure you typed it correctly?' + _('This email address does not look right, are you sure you typed it correctly?') end - context 'with trial_email_validation flag enabled' do - it 'shows an error message until a correct email is entered' do - visit path - expect(page).to have_content(email_hint_message) - expect(page).not_to have_content(email_error_message) - expect(page).not_to have_content(email_warning_message) + it 'shows an error message until a correct email is entered' do + visit path + expect(page).to have_content(email_hint_message) + expect(page).not_to have_content(email_error_message) + expect(page).not_to have_content(email_warning_message) - fill_in 'new_user_email', with: 'foo@' - fill_in 'new_user_first_name', with: '' + fill_in 'new_user_email', with: 'foo@' + fill_in 'new_user_first_name', with: '' - expect(page).not_to have_content(email_hint_message) - expect(page).to have_content(email_error_message) - expect(page).not_to have_content(email_warning_message) + expect(page).not_to have_content(email_hint_message) + expect(page).to have_content(email_error_message) + expect(page).not_to have_content(email_warning_message) - fill_in 'new_user_email', with: 'foo@bar' - fill_in 'new_user_first_name', with: '' + fill_in 'new_user_email', with: 'foo@bar' + fill_in 'new_user_first_name', with: '' - expect(page).not_to have_content(email_hint_message) - expect(page).not_to have_content(email_error_message) - expect(page).to have_content(email_warning_message) + expect(page).not_to have_content(email_hint_message) + expect(page).not_to have_content(email_error_message) + expect(page).to have_content(email_warning_message) - fill_in 'new_user_email', with: 'foo@gitlab.com' - fill_in 'new_user_first_name', with: '' + fill_in 'new_user_email', with: 'foo@gitlab.com' + fill_in 'new_user_first_name', with: '' - expect(page).not_to have_content(email_hint_message) - expect(page).not_to have_content(email_error_message) - expect(page).not_to have_content(email_warning_message) - end - end - - context 'when trial_email_validation flag disabled' do - before do - stub_feature_flags trial_email_validation: false - end - - it 'does not show an error message' do - visit path - expect(page).to have_content(email_hint_message) - expect(page).not_to have_content(email_error_message) - expect(page).not_to have_content(email_warning_message) - - fill_in 'new_user_email', with: 'foo@' - - expect(page).to have_content(email_hint_message) - expect(page).not_to have_content(email_error_message) - expect(page).not_to have_content(email_warning_message) - end + expect(page).not_to have_content(email_hint_message) + expect(page).not_to have_content(email_error_message) + expect(page).not_to have_content(email_warning_message) end end diff --git a/spec/support/shared_examples/features/variable_list_pagination_shared_examples.rb b/spec/support/shared_examples/features/variable_list_pagination_shared_examples.rb new file mode 100644 index 00000000000..0b0c9edcb42 --- /dev/null +++ b/spec/support/shared_examples/features/variable_list_pagination_shared_examples.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'variable list pagination' do |variable_type| + first_page_count = 20 + + before do + first_page_count.times do |i| + case variable_type + when :ci_variable + create(variable_type, key: "test_key_#{i}", value: 'test_value', masked: true, project: project) + when :ci_group_variable + create(variable_type, key: "test_key_#{i}", value: 'test_value', masked: true, group: group) + else + create(variable_type, key: "test_key_#{i}", value: 'test_value', masked: true) + end + end + + visit page_path + wait_for_requests + end + + it 'can navigate between pages' do + page.within('[data-testid="ci-variable-table"]') do + expect(page.all('.js-ci-variable-row').length).to be(first_page_count) + end + + click_button 'Next' + wait_for_requests + + page.within('[data-testid="ci-variable-table"]') do + expect(page.all('.js-ci-variable-row').length).to be(1) + end + + click_button 'Previous' + wait_for_requests + + page.within('[data-testid="ci-variable-table"]') do + expect(page.all('.js-ci-variable-row').length).to be(first_page_count) + end + end + + it 'sorts variables alphabetically in ASC and DESC order' 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) + expect(find('.js-ci-variable-row:nth-child(20) td[data-label="Key"]').text).to eq('test_key_8') + end + + click_button 'Next' + wait_for_requests + + page.within('[data-testid="ci-variable-table"]') do + expect(find('.js-ci-variable-row:nth-child(1) td[data-label="Key"]').text).to eq('test_key_9') + end + + page.within('[data-testid="ci-variable-table"]') do + find('.b-table-sort-icon-left').click + end + + wait_for_requests + + page.within('[data-testid="ci-variable-table"]') do + expect(find('.js-ci-variable-row:nth-child(1) td[data-label="Key"]').text).to eq('test_key_9') + expect(find('.js-ci-variable-row:nth-child(20) td[data-label="Key"]').text).to eq('test_key_0') + end + end +end diff --git a/spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb index 0334187e4b1..c1e4185e058 100644 --- a/spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb +++ b/spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb @@ -150,6 +150,7 @@ RSpec.shared_examples 'User updates wiki page' do end it_behaves_like 'edits content using the content editor' + it_behaves_like 'inserts diagrams.net diagram using the content editor' it_behaves_like 'autocompletes items' end diff --git a/spec/support/shared_examples/features/wiki/user_views_wiki_sidebar_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_views_wiki_sidebar_shared_examples.rb index 639eb3f2b99..b3378c76658 100644 --- a/spec/support/shared_examples/features/wiki/user_views_wiki_sidebar_shared_examples.rb +++ b/spec/support/shared_examples/features/wiki/user_views_wiki_sidebar_shared_examples.rb @@ -84,6 +84,30 @@ RSpec.shared_examples 'User views wiki sidebar' do expect(page).not_to have_link('View All Pages') end + it 'shows all collapse buttons in the sidebar' do + visit wiki_path(wiki) + + within('.right-sidebar') do + expect(page.all("[data-testid='chevron-down-icon']").size).to eq(3) + end + end + + it 'collapses/expands children when click collapse/expand button in the sidebar', :js do + visit wiki_path(wiki) + + within('.right-sidebar') do + first("[data-testid='chevron-down-icon']").click + (11..15).each { |i| expect(page).not_to have_content("my page #{i}") } + expect(page.all("[data-testid='chevron-down-icon']").size).to eq(1) + expect(page.all("[data-testid='chevron-right-icon']").size).to eq(1) + + first("[data-testid='chevron-right-icon']").click + (11..15).each { |i| expect(page).to have_content("my page #{i}") } + expect(page.all("[data-testid='chevron-down-icon']").size).to eq(3) + expect(page.all("[data-testid='chevron-right-icon']").size).to eq(0) + end + end + context 'when there are more than 15 existing pages' do before do create(:wiki_page, wiki: wiki, title: 'my page 16') diff --git a/spec/support/shared_examples/features/work_items_shared_examples.rb b/spec/support/shared_examples/features/work_items_shared_examples.rb index 4f3d957ad71..0b8bfc4d2a2 100644 --- a/spec/support/shared_examples/features/work_items_shared_examples.rb +++ b/spec/support/shared_examples/features/work_items_shared_examples.rb @@ -1,5 +1,20 @@ # frozen_string_literal: true +RSpec.shared_examples 'work items title' do + let(:title_selector) { '[data-testid="work-item-title"]' } + + it 'successfully shows and changes the title of the work item' do + expect(work_item.reload.title).to eq work_item.title + + find(title_selector).set("Work item title") + find(title_selector).native.send_keys(:return) + + wait_for_requests + + expect(work_item.reload.title).to eq 'Work item title' + end +end + RSpec.shared_examples 'work items status' do let(:state_selector) { '[data-testid="work-item-state-select"]' } @@ -19,7 +34,7 @@ RSpec.shared_examples 'work items comments' do let(:form_selector) { '[data-testid="work-item-add-comment"]' } it 'successfully creates and shows comments' do - click_button 'Add a comment' + click_button 'Add a reply' find(form_selector).fill_in(with: "Test comment") click_button "Comment" @@ -39,7 +54,6 @@ RSpec.shared_examples 'work items assignees' do # submit and simulate blur to save send_keys(:enter) find("body").click - wait_for_requests expect(work_item.assignees).to include(user) @@ -47,6 +61,8 @@ RSpec.shared_examples 'work items assignees' do end RSpec.shared_examples 'work items labels' do + let(:label_title_selector) { '[data-testid="labels-title"]' } + it 'successfully assigns a label' do label = create(:label, project: work_item.project, title: "testing-label") @@ -55,8 +71,7 @@ RSpec.shared_examples 'work items labels' do # submit and simulate blur to save send_keys(:enter) - find("body").click - + find(label_title_selector).click wait_for_requests expect(work_item.labels).to include(label) @@ -139,3 +154,27 @@ RSpec.shared_examples 'work items invite members' do end end end + +RSpec.shared_examples 'work items milestone' do + def set_milestone(milestone_dropdown, milestone_text) + milestone_dropdown.click + + find('[data-testid="work-item-milestone-dropdown"] .gl-form-input', visible: true).send_keys "\"#{milestone_text}\"" + wait_for_requests + + click_button(milestone_text) + wait_for_requests + end + + let(:milestone_dropdown_selector) { '[data-testid="work-item-milestone-dropdown"]' } + + it 'searches and sets or removes milestone for the work item' do + set_milestone(find(milestone_dropdown_selector), milestone.title) + + expect(page.find(milestone_dropdown_selector)).to have_text(milestone.title) + + set_milestone(find(milestone_dropdown_selector), 'No milestone') + + expect(page.find(milestone_dropdown_selector)).to have_text('Add to milestone') + end +end diff --git a/spec/support/shared_examples/graphql/mutations/members/bulk_update_shared_examples.rb b/spec/support/shared_examples/graphql/mutations/members/bulk_update_shared_examples.rb new file mode 100644 index 00000000000..e885b5d283e --- /dev/null +++ b/spec/support/shared_examples/graphql/mutations/members/bulk_update_shared_examples.rb @@ -0,0 +1,123 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'members bulk update mutation' do + let_it_be(:current_user) { create(:user) } + let_it_be(:user1) { create(:user) } + let_it_be(:user2) { create(:user) } + let_it_be(:member1) { create(member_type, source: source, user: user1) } + let_it_be(:member2) { create(member_type, source: source, user: user2) } + + let(:extra_params) { { expires_at: 10.days.from_now } } + let(:input_params) { input.merge(extra_params) } + let(:mutation) { graphql_mutation(mutation_name, input_params) } + let(:mutation_response) { graphql_mutation_response(mutation_name) } + + let(:input) do + { + source_id_key => source.to_global_id.to_s, + 'user_ids' => [user1.to_global_id.to_s, user2.to_global_id.to_s], + 'access_level' => 'GUEST' + } + end + + context 'when user is not logged-in' do + it_behaves_like 'a mutation that returns a top-level access error' + end + + context 'when user is not an owner' do + before do + source.add_developer(current_user) + end + + it_behaves_like 'a mutation that returns a top-level access error' + end + + context 'when user is an owner' do + before do + source.add_owner(current_user) + end + + shared_examples 'updates the user access role' do + specify do + post_graphql_mutation(mutation, current_user: current_user) + + new_access_levels = mutation_response[response_member_field].map do |member| + member['accessLevel']['integerValue'] + end + expect(response).to have_gitlab_http_status(:success) + expect(mutation_response['errors']).to be_empty + expect(new_access_levels).to all(be Gitlab::Access::GUEST) + end + end + + it_behaves_like 'updates the user access role' + + context 'when inherited members are passed' do + let(:input) do + { + source_id_key => source.to_global_id.to_s, + 'user_ids' => [user1.to_global_id.to_s, user2.to_global_id.to_s, parent_group_member.user.to_global_id.to_s], + 'access_level' => 'GUEST' + } + end + + it 'does not update the members' do + post_graphql_mutation(mutation, current_user: current_user) + + error = Mutations::Members::BulkUpdateBase::INVALID_MEMBERS_ERROR + expect(json_response['errors'].first['message']).to include(error) + end + end + + context 'when members count is more than the allowed limit' do + let(:max_members_update_limit) { 1 } + + before do + stub_const('Mutations::Members::BulkUpdateBase::MAX_MEMBERS_UPDATE_LIMIT', max_members_update_limit) + end + + it 'does not update the members' do + post_graphql_mutation(mutation, current_user: current_user) + + error = Mutations::Members::BulkUpdateBase::MAX_MEMBERS_UPDATE_ERROR + expect(json_response['errors'].first['message']).to include(error) + end + end + + context 'when the update service raises access denied error' do + before do + allow_next_instance_of(Members::UpdateService) do |instance| + allow(instance).to receive(:execute).and_raise(Gitlab::Access::AccessDeniedError) + end + end + + it 'does not update the members' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(mutation_response[response_member_field]).to be_nil + expect(mutation_response['errors']) + .to contain_exactly("Unable to update members, please check user permissions.") + end + end + + context 'when the update service returns an error message' do + before do + allow_next_instance_of(Members::UpdateService) do |instance| + error_result = { + message: 'Expires at cannot be a date in the past', + status: :error, + members: [member1] + } + allow(instance).to receive(:execute).and_return(error_result) + end + end + + it 'will pass through the error' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(mutation_response[response_member_field].first['id']).to eq(member1.to_global_id.to_s) + expect(mutation_response['errors']).to contain_exactly('Expires at cannot be a date in the past') + end + end + end +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 0d2e9f6ec8c..09239ced73a 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 @@ -46,7 +46,7 @@ RSpec.shared_context 'exposing regular notes on a noteable in GraphQL' do discussions { edges { node { - #{all_graphql_fields_for('Discussion', max_depth: 4)} + #{all_graphql_fields_for('Discussion', max_depth: 4, excluded: ['productAnalyticsState'])} } } } diff --git a/spec/support/shared_examples/graphql/notes_quick_actions_for_work_items_shared_examples.rb b/spec/support/shared_examples/graphql/notes_quick_actions_for_work_items_shared_examples.rb new file mode 100644 index 00000000000..5f4e7e5d4e7 --- /dev/null +++ b/spec/support/shared_examples/graphql/notes_quick_actions_for_work_items_shared_examples.rb @@ -0,0 +1,154 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'work item supports assignee widget updates via quick actions' do + let_it_be(:developer) { create(:user).tap { |user| project.add_developer(user) } } + + context 'when assigning a user' do + let(:body) { "/assign @#{developer.username}" } + + it 'updates the work item assignee' do + expect do + post_graphql_mutation(mutation, current_user: current_user) + noteable.reload + end.to change { noteable.assignee_ids }.from([]).to([developer.id]) + + expect(response).to have_gitlab_http_status(:success) + end + end + + context 'when unassigning a user' do + let(:body) { "/unassign @#{developer.username}" } + + before do + noteable.update!(assignee_ids: [developer.id]) + end + + it 'updates the work item assignee' do + expect do + post_graphql_mutation(mutation, current_user: current_user) + noteable.reload + end.to change { noteable.assignee_ids }.from([developer.id]).to([]) + + expect(response).to have_gitlab_http_status(:success) + end + end +end + +RSpec.shared_examples 'work item does not support assignee widget updates via quick actions' do + let(:developer) { create(:user).tap { |user| project.add_developer(user) } } + let(:body) { "Updating assignee.\n/assign @#{developer.username}" } + + before do + WorkItems::Type.default_by_type(:task).widget_definitions + .find_by_widget_type(:assignees).update!(disabled: true) + end + + it 'ignores the quick action' do + expect do + post_graphql_mutation(mutation, current_user: current_user) + noteable.reload + end.not_to change { noteable.assignee_ids } + end +end + +RSpec.shared_examples 'work item supports labels widget updates via quick actions' do + shared_examples 'work item labels are updated' do + it do + expect do + post_graphql_mutation(mutation, current_user: current_user) + noteable.reload + end.to change { noteable.labels.count }.to(expected_labels.count) + + expect(noteable.labels).to match_array(expected_labels) + end + end + + let_it_be(:existing_label) { create(:label, project: project) } + let_it_be(:label1) { create(:label, project: project) } + let_it_be(:label2) { create(:label, project: project) } + + let(:add_label_ids) { [] } + let(:remove_label_ids) { [] } + + before_all do + noteable.update!(labels: [existing_label]) + end + + context 'when only removing labels' do + let(:remove_label_ids) { [existing_label.to_gid.to_s] } + let(:expected_labels) { [] } + let(:body) { "/remove_label ~\"#{existing_label.name}\"" } + + it_behaves_like 'work item labels are updated' + end + + context 'when only adding labels' do + let(:add_label_ids) { [label1.to_gid.to_s, label2.to_gid.to_s] } + let(:expected_labels) { [label1, label2, existing_label] } + let(:body) { "/labels ~\"#{label1.name}\" ~\"#{label2.name}\"" } + + it_behaves_like 'work item labels are updated' + end + + context 'when adding and removing labels' do + let(:remove_label_ids) { [existing_label.to_gid.to_s] } + let(:add_label_ids) { [label1.to_gid.to_s, label2.to_gid.to_s] } + let(:expected_labels) { [label1, label2] } + let(:body) { "/label ~\"#{label1.name}\" ~\"#{label2.name}\"\n/remove_label ~\"#{existing_label.name}\"" } + + it_behaves_like 'work item labels are updated' + end +end + +RSpec.shared_examples 'work item does not support labels widget updates via quick actions' do + let(:label1) { create(:label, project: project) } + let(:body) { "Updating labels.\n/labels ~\"#{label1.name}\"" } + + before do + WorkItems::Type.default_by_type(:task).widget_definitions + .find_by_widget_type(:labels).update!(disabled: true) + end + + it 'ignores the quick action' do + expect do + post_graphql_mutation(mutation, current_user: current_user) + noteable.reload + end.not_to change { noteable.labels.count } + + expect(noteable.labels).to be_empty + end +end + +RSpec.shared_examples 'work item supports start and due date widget updates via quick actions' do + let(:due_date) { Date.today } + let(:body) { "/remove_due_date" } + + before do + noteable.update!(due_date: due_date) + end + + it 'updates start and due date' do + expect do + post_graphql_mutation(mutation, current_user: current_user) + noteable.reload + end.to not_change(noteable, :start_date).and( + change { noteable.due_date }.from(due_date).to(nil) + ) + end +end + +RSpec.shared_examples 'work item does not support start and due date widget updates via quick actions' do + let(:body) { "Updating due date.\n/due today" } + + before do + WorkItems::Type.default_by_type(:task).widget_definitions + .find_by_widget_type(:start_and_due_date).update!(disabled: true) + end + + it 'ignores the quick action' do + expect do + post_graphql_mutation(mutation, current_user: current_user) + noteable.reload + end.not_to change { noteable.due_date } + end +end diff --git a/spec/support/shared_examples/graphql/types/merge_request_interactions_type_shared_examples.rb b/spec/support/shared_examples/graphql/types/merge_request_interactions_type_shared_examples.rb index bb33a7559dc..ed193d06161 100644 --- a/spec/support/shared_examples/graphql/types/merge_request_interactions_type_shared_examples.rb +++ b/spec/support/shared_examples/graphql/types/merge_request_interactions_type_shared_examples.rb @@ -42,6 +42,7 @@ RSpec.shared_examples "a user type with merge request interaction type" do profileEnableGitpodPath savedReplies savedReply + user_achievements ] expect(described_class).to have_graphql_fields(*expected_fields) diff --git a/spec/support/shared_examples/helpers/callouts_for_web_hooks.rb b/spec/support/shared_examples/helpers/callouts_for_web_hooks.rb new file mode 100644 index 00000000000..b3d3000aa06 --- /dev/null +++ b/spec/support/shared_examples/helpers/callouts_for_web_hooks.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'CalloutsHelper#web_hook_disabled_dismissed shared examples' do + context 'when the web-hook failure callout has never been dismissed' do + it 'is false' do + expect(helper).not_to be_web_hook_disabled_dismissed(container) + end + end + + context 'when the web-hook failure callout has been dismissed', :freeze_time, :clean_gitlab_redis_shared_state do + before do + create(factory, + feature_name: Users::CalloutsHelper::WEB_HOOK_DISABLED, + user: user, + dismissed_at: 1.week.ago, + container_key => container) + end + + it 'is true' do + expect(helper).to be_web_hook_disabled_dismissed(container) + end + + it 'is true when passed as a presenter' do + skip "Does not apply to #{container.class}" unless container.is_a?(Presentable) + + expect(helper).to be_web_hook_disabled_dismissed(container.present) + end + + context 'when there was an older failure' do + before do + Gitlab::Redis::SharedState.with { |r| r.set(key, 1.month.ago.iso8601) } + end + + it 'is true' do + expect(helper).to be_web_hook_disabled_dismissed(container) + end + end + + context 'when there has been a more recent failure' do + before do + Gitlab::Redis::SharedState.with { |r| r.set(key, 1.day.ago.iso8601) } + end + + it 'is false' do + expect(helper).not_to be_web_hook_disabled_dismissed(container) + end + end + end +end diff --git a/spec/support/shared_examples/integrations/integration_settings_form.rb b/spec/support/shared_examples/integrations/integration_settings_form.rb index aeb4e0feb12..a21eddb182c 100644 --- a/spec/support/shared_examples/integrations/integration_settings_form.rb +++ b/spec/support/shared_examples/integrations/integration_settings_form.rb @@ -7,7 +7,6 @@ RSpec.shared_examples 'integration settings form' do it 'displays all the integrations', feature_category: :integrations do aggregate_failures do integrations.each do |integration| - stub_feature_flags(integration_slack_app_notifications: false) navigate_to_integration(integration) page.within('form.integration-settings-form') do diff --git a/spec/support/shared_examples/lib/api/terraform_state_enabled_shared_examples.rb b/spec/support/shared_examples/lib/api/terraform_state_enabled_shared_examples.rb new file mode 100644 index 00000000000..b88eade7db2 --- /dev/null +++ b/spec/support/shared_examples/lib/api/terraform_state_enabled_shared_examples.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'it depends on value of the `terraform_state.enabled` config' do |params = {}| + let(:expected_success_status) { params[:success_status] || :ok } + + context 'when terraform_state.enabled=false' do + before do + stub_config(terraform_state: { enabled: false }) + end + + it 'returns `forbidden` response' do + request + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + + context 'when terraform_state.enabled=true' do + before do + stub_config(terraform_state: { enabled: true }) + end + + it 'returns a successful response' do + request + + expect(response).to have_gitlab_http_status(expected_success_status) + end + end +end diff --git a/spec/support/shared_examples/lib/gitlab/database/async_constraints_validation_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/database/async_constraints_validation_shared_examples.rb new file mode 100644 index 00000000000..b9d71183851 --- /dev/null +++ b/spec/support/shared_examples/lib/gitlab/database/async_constraints_validation_shared_examples.rb @@ -0,0 +1,131 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'async constraints validation' do + include ExclusiveLeaseHelpers + + let!(:lease) { stub_exclusive_lease(lease_key, :uuid, timeout: lease_timeout) } + let(:lease_key) { "gitlab/database/asyncddl/actions/#{Gitlab::Database::PRIMARY_DATABASE_NAME}" } + let(:lease_timeout) { described_class::TIMEOUT_PER_ACTION } + + let(:constraints_model) { Gitlab::Database::AsyncConstraints::PostgresAsyncConstraintValidation } + let(:table_name) { '_test_async_constraints' } + let(:constraint_name) { 'constraint_parent_id' } + + let(:validation) do + create(:postgres_async_constraint_validation, + table_name: table_name, + name: constraint_name, + constraint_type: constraint_type) + end + + let(:connection) { validation.connection } + + subject { described_class.new(validation) } + + it 'validates the constraint while controlling statement timeout' do + allow(connection).to receive(:execute).and_call_original + expect(connection).to receive(:execute) + .with("SET statement_timeout TO '43200s'").ordered.and_call_original + expect(connection).to receive(:execute) + .with(/ALTER TABLE "#{table_name}" VALIDATE CONSTRAINT "#{constraint_name}";/).ordered.and_call_original + expect(connection).to receive(:execute) + .with("RESET statement_timeout").ordered.and_call_original + + subject.perform + end + + it 'removes the constraint validation record from table' do + expect(validation).to receive(:destroy!).and_call_original + + expect { subject.perform }.to change { constraints_model.count }.by(-1) + end + + it 'skips logic if not able to acquire exclusive lease' do + expect(lease).to receive(:try_obtain).ordered.and_return(false) + expect(connection).not_to receive(:execute).with(/ALTER TABLE/) + expect(validation).not_to receive(:destroy!) + + expect { subject.perform }.not_to change { constraints_model.count } + end + + it 'logs messages around execution' do + allow(Gitlab::AppLogger).to receive(:info).and_call_original + + subject.perform + + expect(Gitlab::AppLogger) + .to have_received(:info) + .with(a_hash_including(message: 'Starting to validate constraint')) + + expect(Gitlab::AppLogger) + .to have_received(:info) + .with(a_hash_including(message: 'Finished validating constraint')) + end + + context 'when the constraint does not exist' do + before do + connection.create_table(table_name, force: true) + end + + it 'skips validation and removes the record' do + expect(connection).not_to receive(:execute).with(/ALTER TABLE/) + + expect { subject.perform }.to change { constraints_model.count }.by(-1) + end + + it 'logs an appropriate message' do + expected_message = /Skipping #{constraint_name} validation since it does not exist/ + + allow(Gitlab::AppLogger).to receive(:info).and_call_original + + subject.perform + + expect(Gitlab::AppLogger) + .to have_received(:info) + .with(a_hash_including(message: expected_message)) + end + end + + context 'with error handling' do + before do + allow(connection).to receive(:execute).and_call_original + + allow(connection).to receive(:execute) + .with(/ALTER TABLE "#{table_name}" VALIDATE CONSTRAINT "#{constraint_name}";/) + .and_raise(ActiveRecord::StatementInvalid) + end + + context 'on production' do + before do + allow(Gitlab::ErrorTracking).to receive(:should_raise_for_dev?).and_return(false) + end + + it 'increases execution attempts' do + expect { subject.perform }.to change { validation.attempts }.by(1) + + expect(validation.last_error).to be_present + expect(validation).not_to be_destroyed + end + + it 'logs an error message including the constraint_name' do + expect(Gitlab::AppLogger) + .to receive(:error) + .with(a_hash_including(:message, :constraint_name)) + .and_call_original + + subject.perform + end + end + + context 'on development' do + it 'also raises errors' do + expect { subject.perform } + .to raise_error(ActiveRecord::StatementInvalid) + .and change { validation.attempts }.by(1) + + expect(validation.last_error).to be_present + expect(validation).not_to be_destroyed + end + end + end +end diff --git a/spec/support/shared_examples/lib/gitlab/database/index_validators_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/database/index_validators_shared_examples.rb new file mode 100644 index 00000000000..6f0cede7130 --- /dev/null +++ b/spec/support/shared_examples/lib/gitlab/database/index_validators_shared_examples.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.shared_examples "index validators" do |validator, expected_result| + let(:structure_file_path) { Rails.root.join('spec/fixtures/structure.sql') } + let(:database_indexes) do + [ + ['wrong_index', 'CREATE UNIQUE INDEX wrong_index ON public.table_name (column_name)'], + ['extra_index', 'CREATE INDEX extra_index ON public.table_name (column_name)'], + ['index', 'CREATE UNIQUE INDEX "index" ON public.achievements USING btree (namespace_id, lower(name))'] + ] + end + + let(:inconsistency_type) { validator.name.demodulize.underscore } + + let(:database_name) { 'main' } + + let(:database_model) { Gitlab::Database.database_base_models[database_name] } + + let(:connection) { database_model.connection } + + let(:schema) { connection.current_schema } + + let(:database) { Gitlab::Database::SchemaValidation::Database.new(connection) } + let(:structure_file) { Gitlab::Database::SchemaValidation::StructureSql.new(structure_file_path, schema) } + + subject(:result) { validator.new(structure_file, database).execute } + + before do + allow(connection).to receive(:select_rows).and_return(database_indexes) + end + + it 'returns index inconsistencies' do + expect(result.map(&:object_name)).to match_array(expected_result) + expect(result.map(&:type)).to all(eql inconsistency_type) + end +end diff --git a/spec/support/shared_examples/lib/gitlab/database/schema_objects_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/database/schema_objects_shared_examples.rb new file mode 100644 index 00000000000..d5ecab0cb6b --- /dev/null +++ b/spec/support/shared_examples/lib/gitlab/database/schema_objects_shared_examples.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.shared_examples "schema objects assertions for" do |stmt_name| + let(:stmt) { PgQuery.parse(statement).tree.stmts.first.stmt } + let(:schema_object) { described_class.new(stmt.public_send(stmt_name)) } + + describe '#name' do + it 'returns schema object name' do + expect(schema_object.name).to eq(name) + end + end + + describe '#statement' do + it 'returns schema object statement' do + expect(schema_object.statement).to eq(statement) + end + end +end diff --git a/spec/support/shared_examples/lib/gitlab/database/trigger_validators_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/database/trigger_validators_shared_examples.rb new file mode 100644 index 00000000000..13a112275c2 --- /dev/null +++ b/spec/support/shared_examples/lib/gitlab/database/trigger_validators_shared_examples.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.shared_examples 'trigger validators' do |validator, expected_result| + subject(:result) { validator.new(structure_file, database).execute } + + let(:structure_file_path) { Rails.root.join('spec/fixtures/structure.sql') } + let(:structure_file) { Gitlab::Database::SchemaValidation::StructureSql.new(structure_file_path, schema) } + let(:inconsistency_type) { validator.name.demodulize.underscore } + let(:database_name) { 'main' } + let(:schema) { 'public' } + let(:database_model) { Gitlab::Database.database_base_models[database_name] } + let(:connection) { database_model.connection } + let(:database) { Gitlab::Database::SchemaValidation::Database.new(connection) } + + let(:database_triggers) do + [ + ['trigger', 'CREATE TRIGGER trigger AFTER INSERT ON public.t1 FOR EACH ROW EXECUTE FUNCTION t1()'], + ['wrong_trigger', 'CREATE TRIGGER wrong_trigger BEFORE UPDATE ON public.t2 FOR EACH ROW EXECUTE FUNCTION t2()'], + ['extra_trigger', 'CREATE TRIGGER extra_trigger BEFORE INSERT ON public.t4 FOR EACH ROW EXECUTE FUNCTION t4()'] + ] + end + + before do + allow(connection).to receive(:select_rows).and_return(database_triggers) + end + + it 'returns trigger inconsistencies' do + expect(result.map(&:object_name)).to match_array(expected_result) + expect(result.map(&:type)).to all(eql inconsistency_type) + end +end diff --git a/spec/support/shared_examples/lib/gitlab/search_language_filter_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/search_language_filter_shared_examples.rb index a3e4379f4d3..18545698c27 100644 --- a/spec/support/shared_examples/lib/gitlab/search_language_filter_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/search_language_filter_shared_examples.rb @@ -26,29 +26,4 @@ RSpec.shared_examples 'search results filtered by language' do expect(blob_results.size).to eq(5) expect(paths).to match_array(expected_paths) end - - context 'when the search_blobs_language_aggregation feature flag is disabled' do - before do - stub_feature_flags(search_blobs_language_aggregation: false) - end - - it 'does not filter by language', :sidekiq_inline, :aggregate_failures do - expected_paths = %w[ - CHANGELOG - CONTRIBUTING.md - bar/branch-test.txt - custom-highlighting/test.gitlab-custom - files/ruby/popen.rb - files/ruby/regex.rb - files/ruby/version_info.rb - files/whitespace - encoding/test.txt - files/markdown/ruby-style-guide.md - ] - - paths = blob_results.map { |blob| blob.binary_path } - expect(blob_results.size).to eq(10) - expect(paths).to match_array(expected_paths) - end - end end diff --git a/spec/support/shared_examples/lib/gitlab/usage_data_counters/issuable_activity_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/usage_data_counters/issuable_activity_shared_examples.rb index d4802a19202..9873bab1caf 100644 --- a/spec/support/shared_examples/lib/gitlab/usage_data_counters/issuable_activity_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/usage_data_counters/issuable_activity_shared_examples.rb @@ -27,18 +27,6 @@ RSpec.shared_examples 'a daily tracked issuable snowplow and service ping events expect_snowplow_event(**{ category: category, action: event_action, user: user1 }.merge(event_params)) end - - context 'with route_hll_to_snowplow_phase2 disabled' do - before do - stub_feature_flags(route_hll_to_snowplow_phase2: false) - end - - it 'does not emit snowplow event' do - track_action({ author: user1 }.merge(track_params)) - - expect_no_snowplow_event - end - end end RSpec.shared_examples 'daily tracked issuable snowplow and service ping events with project' do diff --git a/spec/support/shared_examples/lib/gitlab/utils/username_and_email_generator_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/utils/username_and_email_generator_shared_examples.rb new file mode 100644 index 00000000000..a42d1450e4d --- /dev/null +++ b/spec/support/shared_examples/lib/gitlab/utils/username_and_email_generator_shared_examples.rb @@ -0,0 +1,104 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'username and email pair is generated by Gitlab::Utils::UsernameAndEmailGenerator' do + let(:randomhex) { 'randomhex' } + + it 'check email domain' do + expect(subject.email).to end_with("@#{email_domain}") + end + + it 'contains SecureRandom part' do + allow(SecureRandom).to receive(:hex).at_least(:once).and_return(randomhex) + + expect(subject.username).to include("_#{randomhex}") + expect(subject.email).to include("_#{randomhex}@") + end + + it 'email name is the same as username' do + expect(subject.email).to include("#{subject.username}@") + end + + context 'when conflicts' do + let(:reserved_username) { "#{username_prefix}_#{randomhex}" } + let(:reserved_email) { "#{reserved_username}@#{email_domain}" } + + shared_examples 'uniquifies username and email' do + it 'uniquifies username and email' do + expect(subject.username).to eq("#{reserved_username}1") + expect(subject.email).to include("#{subject.username}@") + end + end + + context 'when username is reserved' do + context 'when username is reserved by user' do + before do + create(:user, username: reserved_username) + allow(SecureRandom).to receive(:hex).at_least(:once).and_return(randomhex) + end + + include_examples 'uniquifies username and email' + end + + context 'when it conflicts with top-level group namespace' do + before do + create(:group, path: reserved_username) + allow(SecureRandom).to receive(:hex).at_least(:once).and_return(randomhex) + end + + include_examples 'uniquifies username and email' + end + + context 'when it conflicts with top-level group namespace that includes upcased characters' do + before do + create(:group, path: reserved_username.upcase) + allow(SecureRandom).to receive(:hex).at_least(:once).and_return(randomhex) + end + + include_examples 'uniquifies username and email' + end + end + + context 'when email is reserved' do + context 'when it conflicts with confirmed primary email' do + before do + create(:user, email: reserved_email) + allow(SecureRandom).to receive(:hex).at_least(:once).and_return(randomhex) + end + + include_examples 'uniquifies username and email' + end + + context 'when it conflicts with unconfirmed primary email' do + before do + create(:user, :unconfirmed, email: reserved_email) + allow(SecureRandom).to receive(:hex).at_least(:once).and_return(randomhex) + end + + include_examples 'uniquifies username and email' + end + + context 'when it conflicts with confirmed secondary email' do + before do + create(:email, :confirmed, email: reserved_email) + allow(SecureRandom).to receive(:hex).at_least(:once).and_return(randomhex) + end + + include_examples 'uniquifies username and email' + end + end + + context 'when email and username is reserved' do + before do + create(:user, email: reserved_email) + create(:user, username: "#{reserved_username}1") + allow(SecureRandom).to receive(:hex).at_least(:once).and_return(randomhex) + end + + it 'uniquifies username and email' do + expect(subject.username).to eq("#{reserved_username}2") + + expect(subject.email).to include("#{subject.username}@") + end + end + end +end diff --git a/spec/support/shared_examples/lib/menus_shared_examples.rb b/spec/support/shared_examples/lib/menus_shared_examples.rb index 2c2cb362b07..ed3165079fb 100644 --- a/spec/support/shared_examples/lib/menus_shared_examples.rb +++ b/spec/support/shared_examples/lib/menus_shared_examples.rb @@ -37,3 +37,27 @@ RSpec.shared_examples_for 'pill_count formatted results' do expect(pill_count).to eq('112.6k') end end + +RSpec.shared_examples_for 'serializable as super_sidebar_menu_args' do + let(:extra_attrs) { raise NotImplementedError } + + it 'returns hash with provided attributes' do + expect(menu.serialize_as_menu_item_args).to eq({ + title: menu.title, + link: menu.link, + active_routes: menu.active_routes, + container_html_options: menu.container_html_options, + **extra_attrs + }) + end + + it 'returns hash with an item_id' do + expect(menu.serialize_as_menu_item_args[:item_id]).not_to be_nil + end +end + +RSpec.shared_examples_for 'not serializable as super_sidebar_menu_args' do + it 'returns nil' do + expect(menu.serialize_as_menu_item_args).to be_nil + end +end diff --git a/spec/support/shared_examples/lib/sidebars/user_profile/user_profile_menus_shared_examples.rb b/spec/support/shared_examples/lib/sidebars/user_profile/user_profile_menus_shared_examples.rb new file mode 100644 index 00000000000..92c51a03b31 --- /dev/null +++ b/spec/support/shared_examples/lib/sidebars/user_profile/user_profile_menus_shared_examples.rb @@ -0,0 +1,89 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'User profile menu' do |title:, active_route:| + let_it_be(:current_user) { build(:user) } + let_it_be(:user) { build(:user) } + + let(:context) { Sidebars::Context.new(current_user: current_user, container: user) } + + subject { described_class.new(context) } + + it 'does not contain any sub menu' do + expect(subject.has_items?).to be false + end + + it 'renders the correct link' do + expect(subject.link).to match link + end + + it 'renders the correct title' do + expect(subject.title).to eq title + end + + it 'defines correct active route' do + expect(subject.active_routes[:path]).to be active_route + end + + it 'renders if user is logged in' do + expect(subject.render?).to be true + end + + [:blocked, :banned].each do |trait| + context "when viewed user is #{trait}" do + let_it_be(:viewed_user) { build(:user, trait) } + let(:context) { Sidebars::Context.new(current_user: user, container: viewed_user) } + + context 'when user is not logged in' do + it 'is not allowed to view the menu item' do + expect(described_class.new(Sidebars::Context.new(current_user: nil, + container: viewed_user)).render?).to be false + end + end + + context 'when current user has permission' do + before do + allow(Ability).to receive(:allowed?).with(user, :read_user_profile, viewed_user).and_return(true) + end + + it 'is allowed to view the menu item' do + expect(described_class.new(context).render?).to be true + end + end + + context 'when current user does not have permission' do + it 'is not allowed to view the menu item' do + expect(described_class.new(context).render?).to be false + end + end + end + end +end + +RSpec.shared_examples 'Followers/followees counts' do |symbol| + let_it_be(:current_user) { build(:user) } + let_it_be(:user) { build(:user) } + + let(:context) { Sidebars::Context.new(current_user: current_user, container: user) } + + subject { described_class.new(context) } + + context 'when there are items' do + before do + allow(user).to receive(symbol).and_return([1, 2]) + end + + it 'renders the pill' do + expect(subject.has_pill?).to be(true) + end + + it 'returns the count' do + expect(subject.pill_count).to be(2) + end + end + + context 'when there are no items' do + it 'does not render the pill' do + expect(subject.has_pill?).to be(false) + end + end +end diff --git a/spec/support/shared_examples/lib/sidebars/user_settings/menus/user_settings_menus_shared_examples.rb b/spec/support/shared_examples/lib/sidebars/user_settings/menus/user_settings_menus_shared_examples.rb new file mode 100644 index 00000000000..b91386d1935 --- /dev/null +++ b/spec/support/shared_examples/lib/sidebars/user_settings/menus/user_settings_menus_shared_examples.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'User settings menu' do |link:, title:, icon:, active_routes:| + let_it_be(:user) { create(:user) } + + let(:context) { Sidebars::Context.new(current_user: user, container: nil) } + + subject { described_class.new(context) } + + it 'does not contain any sub menu' do + expect(subject.has_items?).to be false + end + + it 'renders the correct link' do + expect(subject.link).to match link + end + + it 'renders the correct title' do + expect(subject.title).to eq title + end + + it 'renders the correct icon' do + expect(subject.sprite_icon).to be icon + end + + it 'defines correct active route' do + expect(subject.active_routes).to eq active_routes + end +end + +RSpec.shared_examples 'User settings menu #render? method' do + describe '#render?' do + subject { described_class.new(context) } + + context 'when user is logged in' do + let_it_be(:user) { build(:user) } + let(:context) { Sidebars::Context.new(current_user: user, container: nil) } + + it 'renders' do + expect(subject.render?).to be true + end + end + + context 'when user is not logged in' do + let(:context) { Sidebars::Context.new(current_user: nil, container: nil) } + + it 'does not render' do + expect(subject.render?).to be false + end + end + end +end diff --git a/spec/support/shared_examples/mailers/export_csv_shared_examples.rb b/spec/support/shared_examples/mailers/export_csv_shared_examples.rb new file mode 100644 index 00000000000..731d7c810f9 --- /dev/null +++ b/spec/support/shared_examples/mailers/export_csv_shared_examples.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'export csv email' do |collection_type| + include_context 'gitlab email notification' + + it 'attachment has csv mime type' do + expect(attachment.mime_type).to eq 'text/csv' + end + + it 'generates a useful filename' do + expect(attachment.filename).to include(Date.today.year.to_s) + expect(attachment.filename).to include(collection_type) + expect(attachment.filename).to include('myproject') + expect(attachment.filename).to end_with('.csv') + end + + it 'mentions number of objects and project name' do + expect(subject).to have_content '3' + expect(subject).to have_content empty_project.name + end + + it "doesn't need to mention truncation by default" do + expect(subject).not_to have_content 'truncated' + end + + context 'when truncated' do + let(:export_status) { { truncated: true, rows_expected: 12, rows_written: 10 } } + + it 'mentions that the csv has been truncated' do + expect(subject).to have_content 'truncated' + end + + it 'mentions the number of objects written and expected' do + expect(subject).to have_content "10 of 12 #{collection_type.humanize.downcase}" + end + end +end diff --git a/spec/support/shared_examples/models/active_record_enum_shared_examples.rb b/spec/support/shared_examples/models/active_record_enum_shared_examples.rb index 3d765b6ca93..10f3263d4fc 100644 --- a/spec/support/shared_examples/models/active_record_enum_shared_examples.rb +++ b/spec/support/shared_examples/models/active_record_enum_shared_examples.rb @@ -10,3 +10,13 @@ RSpec.shared_examples 'having unique enum values' do end end end + +RSpec.shared_examples 'having enum with nil value' do + it 'has enum with nil value' do + subject.public_send("#{attr_value}!") + + expect(subject.public_send("#{attr}_for_database")).to be_nil + expect(subject.public_send("#{attr}?")).to eq(true) + expect(subject.class.public_send(attr_value)).to eq([subject]) + end +end diff --git a/spec/support/shared_examples/models/ci/token_format_shared_examples.rb b/spec/support/shared_examples/models/ci/token_format_shared_examples.rb new file mode 100644 index 00000000000..0272982e2d0 --- /dev/null +++ b/spec/support/shared_examples/models/ci/token_format_shared_examples.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +RSpec.shared_examples_for 'ensures runners_token is prefixed' do |factory| + subject(:record) { FactoryBot.build(factory) } + + describe '#runners_token', feature_category: :system_access do + let(:runners_prefix) { RunnersTokenPrefixable::RUNNERS_TOKEN_PREFIX } + + it 'generates runners_token which starts with runner prefix' do + expect(record.runners_token).to match(a_string_starting_with(runners_prefix)) + end + + context 'when record has an invalid token' do + subject(:record) { FactoryBot.build(factory, runners_token: invalid_runners_token) } + + let(:invalid_runners_token) { "not_start_with_runners_prefix" } + + it 'generates runners_token which starts with runner prefix' do + expect(record.runners_token).to match(a_string_starting_with(runners_prefix)) + end + + it 'changes the attribute values for runners_token and runners_token_encrypted' do + expect { record.runners_token } + .to change { record[:runners_token] }.from(invalid_runners_token).to(nil) + .and change { record[:runners_token_encrypted] }.from(nil) + end + end + end +end diff --git a/spec/support/shared_examples/models/concerns/auto_disabling_hooks_shared_examples.rb b/spec/support/shared_examples/models/concerns/auto_disabling_hooks_shared_examples.rb index 122774a9028..a26c20ccc61 100644 --- a/spec/support/shared_examples/models/concerns/auto_disabling_hooks_shared_examples.rb +++ b/spec/support/shared_examples/models/concerns/auto_disabling_hooks_shared_examples.rb @@ -61,6 +61,20 @@ disabled_until: disabled_until) # Nothing is missing expect(find_hooks.executable.to_a + find_hooks.disabled.to_a).to match_array(find_hooks.to_a) end + + context 'when the flag is disabled' do + before do + stub_feature_flags(auto_disabling_web_hooks: false) + end + + it 'causes all hooks to be considered executable' do + expect(find_hooks.executable.count).to eq(16) + end + + it 'causes no hooks to be considered disabled' do + expect(find_hooks.disabled).to be_empty + end + end end describe '#executable?', :freeze_time do @@ -108,6 +122,16 @@ disabled_until: disabled_until) it 'has the correct state' do expect(web_hook.executable?).to eq(executable) end + + context 'when the flag is disabled' do + before do + stub_feature_flags(auto_disabling_web_hooks: false) + end + + it 'is always executable' do + expect(web_hook).to be_executable + end + end end end @@ -151,7 +175,7 @@ disabled_until: disabled_until) context 'when we have exhausted the grace period' do before do - hook.update!(recent_failures: WebHook::FAILURE_THRESHOLD) + hook.update!(recent_failures: WebHooks::AutoDisabling::FAILURE_THRESHOLD) end context 'when the hook is permanently disabled' do @@ -172,6 +196,16 @@ disabled_until: disabled_until) def run_expectation expect { hook.backoff! }.to change { hook.backoff_count }.by(1) end + + context 'when the flag is disabled' do + before do + stub_feature_flags(auto_disabling_web_hooks: false) + end + + it 'does not increment backoff count' do + expect { hook.failed! }.not_to change { hook.backoff_count } + end + end end end end @@ -181,6 +215,16 @@ disabled_until: disabled_until) def run_expectation expect { hook.failed! }.to change { hook.recent_failures }.by(1) end + + context 'when the flag is disabled' do + before do + stub_feature_flags(auto_disabling_web_hooks: false) + end + + it 'does not increment recent failure count' do + expect { hook.failed! }.not_to change { hook.recent_failures } + end + end end end @@ -189,6 +233,16 @@ disabled_until: disabled_until) expect { hook.disable! }.to change { hook.executable? }.from(true).to(false) end + context 'when the flag is disabled' do + before do + stub_feature_flags(auto_disabling_web_hooks: false) + end + + it 'does not disable the hook' do + expect { hook.disable! }.not_to change { hook.executable? } + end + end + it 'does nothing if the hook is already disabled' do allow(hook).to receive(:permanently_disabled?).and_return(true) @@ -210,7 +264,7 @@ disabled_until: disabled_until) end it 'allows FAILURE_THRESHOLD initial failures before we back-off' do - WebHook::FAILURE_THRESHOLD.times do + WebHooks::AutoDisabling::FAILURE_THRESHOLD.times do hook.backoff! expect(hook).not_to be_temporarily_disabled end @@ -221,13 +275,23 @@ disabled_until: disabled_until) context 'when hook has been told to back off' do before do - hook.update!(recent_failures: WebHook::FAILURE_THRESHOLD) + hook.update!(recent_failures: WebHooks::AutoDisabling::FAILURE_THRESHOLD) hook.backoff! end it 'is true' do expect(hook).to be_temporarily_disabled end + + context 'when the flag is disabled' do + before do + stub_feature_flags(auto_disabling_web_hooks: false) + end + + it 'is false' do + expect(hook).not_to be_temporarily_disabled + end + end end end @@ -244,6 +308,16 @@ disabled_until: disabled_until) it 'is true' do expect(hook).to be_permanently_disabled end + + context 'when the flag is disabled' do + before do + stub_feature_flags(auto_disabling_web_hooks: false) + end + + it 'is false' do + expect(hook).not_to be_permanently_disabled + end + end end end @@ -258,15 +332,31 @@ disabled_until: disabled_until) end it { is_expected.to eq :disabled } + + context 'when the flag is disabled' do + before do + stub_feature_flags(auto_disabling_web_hooks: false) + end + + it { is_expected.to eq(:executable) } + end end context 'when hook has been backed off' do before do - hook.update!(recent_failures: WebHook::FAILURE_THRESHOLD + 1) + hook.update!(recent_failures: WebHooks::AutoDisabling::FAILURE_THRESHOLD + 1) hook.disabled_until = 1.hour.from_now end it { is_expected.to eq :temporarily_disabled } + + context 'when the flag is disabled' do + before do + stub_feature_flags(auto_disabling_web_hooks: false) + end + + it { is_expected.to eq(:executable) } + end end end end diff --git a/spec/support/shared_examples/models/concerns/cascading_namespace_setting_shared_examples.rb b/spec/support/shared_examples/models/concerns/cascading_namespace_setting_shared_examples.rb index a4db4e25db3..9dec1a5056c 100644 --- a/spec/support/shared_examples/models/concerns/cascading_namespace_setting_shared_examples.rb +++ b/spec/support/shared_examples/models/concerns/cascading_namespace_setting_shared_examples.rb @@ -171,15 +171,59 @@ RSpec.shared_examples 'a cascading namespace setting boolean attribute' do end describe "##{settings_attribute_name}=" do - before do - subgroup_settings.update!(settings_attribute_name => nil) - group_settings.update!(settings_attribute_name => true) + using RSpec::Parameterized::TableSyntax + + where(:parent_value, :current_subgroup_value, :new_subgroup_value, :expected_subgroup_value_after_update) do + true | nil | true | nil + true | nil | "true" | nil + true | false | true | true + true | false | "true" | true + true | true | false | false + true | true | "false" | false + false | nil | false | nil + false | nil | true | true + false | true | false | false + false | false | true | true end - it 'does not save the value locally when it matches the cascaded value' do - subgroup_settings.update!(settings_attribute_name => true) + with_them do + before do + subgroup_settings.update!(settings_attribute_name => current_subgroup_value) + group_settings.update!(settings_attribute_name => parent_value) + end + + it 'validates starting values from before block', :aggregate_failures do + expect(group_settings.reload.read_attribute(settings_attribute_name)).to eq(parent_value) + expect(subgroup_settings.reload.read_attribute(settings_attribute_name)).to eq(current_subgroup_value) + end + + it 'does not save the value locally when it matches cascaded value', :aggregate_failures do + subgroup_settings.send("#{settings_attribute_name}=", new_subgroup_value) + + # Verify dirty value + expect(subgroup_settings.read_attribute(settings_attribute_name)).to eq(expected_subgroup_value_after_update) + + subgroup_settings.save! - expect(subgroup_settings.read_attribute(settings_attribute_name)).to eq(nil) + # Verify persisted value + expect(subgroup_settings.reload.read_attribute(settings_attribute_name)) + .to eq(expected_subgroup_value_after_update) + end + + context 'when mass assigned' do + before do + subgroup_settings.attributes = + { settings_attribute_name => new_subgroup_value, "lock_#{settings_attribute_name}" => false } + end + + it 'does not save the value locally when it matches cascaded value', :aggregate_failures do + subgroup_settings.save! + + # Verify persisted value + expect(subgroup_settings.reload.read_attribute(settings_attribute_name)) + .to eq(expected_subgroup_value_after_update) + end + end end end diff --git a/spec/support/shared_examples/models/concerns/counter_attribute_shared_examples.rb b/spec/support/shared_examples/models/concerns/counter_attribute_shared_examples.rb index 5755b9a56b1..139deaaece9 100644 --- a/spec/support/shared_examples/models/concerns/counter_attribute_shared_examples.rb +++ b/spec/support/shared_examples/models/concerns/counter_attribute_shared_examples.rb @@ -17,6 +17,11 @@ RSpec.shared_examples_for CounterAttribute do |counter_attributes| let(:amount) { 10 } let(:increment) { Gitlab::Counters::Increment.new(amount: amount, ref: 3) } let(:counter_key) { model.counter(attribute).key } + let(:returns_current) do + model.class.counter_attributes + .find { |a| a[:attribute] == attribute } + .fetch(:returns_current, false) + end subject { model.increment_counter(attribute, increment) } @@ -61,6 +66,33 @@ RSpec.shared_examples_for CounterAttribute do |counter_attributes| end end + describe '#increment_amount' do + it 'increases the egress in cache' do + model.increment_amount(attribute, 3) + + expect(model.counter(attribute).get).to eq(3) + end + end + + describe '#current_counter' do + let(:data_transfer_node) do + args = { project: project } + args[attribute] = 2 + create(:project_data_transfer, **args) + end + + it 'increases the amount in cache' do + if returns_current + incremented_by = 4 + db_state = model.read_attribute(attribute) + + model.send("increment_#{attribute}".to_sym, incremented_by) + + expect(model.send(attribute)).to eq(db_state + incremented_by) + end + end + end + context 'when increment amount is 0' do let(:amount) { 0 } @@ -155,14 +187,24 @@ RSpec.shared_examples_for CounterAttribute do |counter_attributes| end describe '#update_counters_with_lease' do - let(:increments) { { build_artifacts_size: 1, packages_size: 2 } } + let_it_be(:first_attribute) { counter_attributes.first } + let_it_be(:second_attribute) { counter_attributes.second } + + let_it_be(:increments) do + increments_hash = {} + + increments_hash[first_attribute] = 1 + increments_hash[second_attribute] = 2 + + increments_hash + end subject { model.update_counters_with_lease(increments) } it 'updates counters of the record' do expect { subject } - .to change { model.reload.build_artifacts_size }.by(1) - .and change { model.reload.packages_size }.by(2) + .to change { model.reload.send(first_attribute) }.by(1) + .and change { model.reload.send(second_attribute) }.by(2) end it_behaves_like 'obtaining lease to update database' do diff --git a/spec/support/shared_examples/models/concerns/integrations/base_slack_notification_shared_examples.rb b/spec/support/shared_examples/models/concerns/integrations/base_slack_notification_shared_examples.rb index 2e528f7996c..2dad35dc46e 100644 --- a/spec/support/shared_examples/models/concerns/integrations/base_slack_notification_shared_examples.rb +++ b/spec/support/shared_examples/models/concerns/integrations/base_slack_notification_shared_examples.rb @@ -35,7 +35,6 @@ RSpec.shared_examples Integrations::BaseSlackNotification do |factory:| end it_behaves_like 'Snowplow event tracking with RedisHLL context' do - let(:feature_flag_name) { :route_hll_to_snowplow_phase2 } let(:category) { described_class.to_s } let(:action) { 'perform_integrations_action' } let(:namespace) { project.namespace } diff --git a/spec/support/shared_examples/models/concerns/unstoppable_hooks_shared_examples.rb b/spec/support/shared_examples/models/concerns/unstoppable_hooks_shared_examples.rb index 848840ee297..24d114bbe23 100644 --- a/spec/support/shared_examples/models/concerns/unstoppable_hooks_shared_examples.rb +++ b/spec/support/shared_examples/models/concerns/unstoppable_hooks_shared_examples.rb @@ -110,7 +110,7 @@ disabled_until: disabled_until) context 'when we have exhausted the grace period' do before do - hook.update!(recent_failures: WebHook::FAILURE_THRESHOLD) + hook.update!(recent_failures: WebHooks::AutoDisabling::FAILURE_THRESHOLD) end it 'does not disable the hook' do @@ -131,7 +131,7 @@ disabled_until: disabled_until) expect(hook).not_to be_temporarily_disabled # Backing off - WebHook::FAILURE_THRESHOLD.times do + WebHooks::AutoDisabling::FAILURE_THRESHOLD.times do hook.backoff! expect(hook).not_to be_temporarily_disabled end @@ -167,7 +167,7 @@ disabled_until: disabled_until) context 'when hook has been backed off' do before do - hook.update!(recent_failures: WebHook::FAILURE_THRESHOLD + 1) + hook.update!(recent_failures: WebHooks::AutoDisabling::FAILURE_THRESHOLD + 1) hook.disabled_until = 1.hour.from_now end diff --git a/spec/support/shared_examples/models/concerns/web_hooks/has_web_hooks_shared_examples.rb b/spec/support/shared_examples/models/concerns/web_hooks/has_web_hooks_shared_examples.rb index cd6eb8c77fa..113dcc266fc 100644 --- a/spec/support/shared_examples/models/concerns/web_hooks/has_web_hooks_shared_examples.rb +++ b/spec/support/shared_examples/models/concerns/web_hooks/has_web_hooks_shared_examples.rb @@ -19,7 +19,7 @@ RSpec.shared_examples 'something that has web-hooks' do context 'when there is a failed hook' do before do hook = create_hook - hook.update!(recent_failures: WebHook::FAILURE_THRESHOLD + 1) + hook.update!(recent_failures: WebHooks::AutoDisabling::FAILURE_THRESHOLD + 1) end it { is_expected.to eq(true) } @@ -83,7 +83,7 @@ RSpec.shared_examples 'something that has web-hooks' do describe '#fetch_web_hook_failure', :clean_gitlab_redis_shared_state do context 'when a value has not been stored' do - it 'does not call #any_hook_failed?' do + it 'calls #any_hook_failed?' do expect(object.get_web_hook_failure).to be_nil expect(object).to receive(:any_hook_failed?).and_return(true) diff --git a/spec/support/shared_examples/models/cycle_analytics_stage_shared_examples.rb b/spec/support/shared_examples/models/cycle_analytics_stage_shared_examples.rb index 5eeefacdeb9..3f532629961 100644 --- a/spec/support/shared_examples/models/cycle_analytics_stage_shared_examples.rb +++ b/spec/support/shared_examples/models/cycle_analytics_stage_shared_examples.rb @@ -290,6 +290,7 @@ RSpec.shared_examples 'value stream analytics label based stage' do context 'when `ProjectLabel is given' do let_it_be(:label) { create(:label) } + let(:expected_error) { s_('CycleAnalyticsStage|is not available for the selected group') } it 'raises error when `ProjectLabel` is given for `start_event_label`' do params = { @@ -300,7 +301,9 @@ RSpec.shared_examples 'value stream analytics label based stage' do end_event_identifier: :issue_closed } - expect { described_class.new(params) }.to raise_error(ActiveRecord::AssociationTypeMismatch) + stage = described_class.new(params) + expect(stage).to be_invalid + expect(stage.errors.messages_for(:start_event_label_id)).to eq([expected_error]) end it 'raises error when `ProjectLabel` is given for `end_event_label`' do @@ -312,7 +315,9 @@ RSpec.shared_examples 'value stream analytics label based stage' do end_event_label: label } - expect { described_class.new(params) }.to raise_error(ActiveRecord::AssociationTypeMismatch) + stage = described_class.new(params) + expect(stage).to be_invalid + expect(stage.errors.messages_for(:end_event_label_id)).to eq([expected_error]) end end end diff --git a/spec/support/shared_examples/models/integrations/base_slash_commands_shared_examples.rb b/spec/support/shared_examples/models/integrations/base_slash_commands_shared_examples.rb index 7dfdd24177e..0cf109ce5c5 100644 --- a/spec/support/shared_examples/models/integrations/base_slash_commands_shared_examples.rb +++ b/spec/support/shared_examples/models/integrations/base_slash_commands_shared_examples.rb @@ -3,7 +3,6 @@ RSpec.shared_examples Integrations::BaseSlashCommands do describe "Associations" do it { is_expected.to respond_to :token } - it { is_expected.to have_many :chat_names } end describe 'default values' do @@ -85,7 +84,7 @@ RSpec.shared_examples Integrations::BaseSlashCommands do end context 'when the user is authenticated' do - let!(:chat_name) { create(:chat_name, integration: subject) } + let!(:chat_name) { create(:chat_name) } let(:params) { { token: 'token', team_id: chat_name.team_id, user_id: chat_name.chat_id } } subject do diff --git a/spec/support/shared_examples/models/packages/debian/component_file_shared_example.rb b/spec/support/shared_examples/models/packages/debian/component_file_shared_example.rb index 5be0f6349ea..78591482696 100644 --- a/spec/support/shared_examples/models/packages/debian/component_file_shared_example.rb +++ b/spec/support/shared_examples/models/packages/debian/component_file_shared_example.rb @@ -231,4 +231,20 @@ RSpec.shared_examples 'Debian Component File' do |container_type, can_freeze| it { is_expected.to eq("#{component1_1.name}/binary-#{architecture1_1.name}/Packages.xz") } end end + + describe '#empty?' do + subject { component_file_with_architecture.empty? } + + context 'with a non-empty component' do + it { is_expected.to be_falsey } + end + + context 'with an empty component' do + before do + component_file_with_architecture.update! size: 0 + end + + it { is_expected.to be_truthy } + end + end end diff --git a/spec/support/shared_examples/models/project_ci_cd_settings_shared_examples.rb b/spec/support/shared_examples/models/project_ci_cd_settings_shared_examples.rb index 3caf58da4d2..f1af1760e8d 100644 --- a/spec/support/shared_examples/models/project_ci_cd_settings_shared_examples.rb +++ b/spec/support/shared_examples/models/project_ci_cd_settings_shared_examples.rb @@ -19,7 +19,7 @@ RSpec.shared_examples 'ci_cd_settings delegation' do end end -RSpec.shared_examples 'a ci_cd_settings predicate method' do |prefix: ''| +RSpec.shared_examples 'a ci_cd_settings predicate method' do |prefix: '', default: false| using RSpec::Parameterized::TableSyntax context 'when ci_cd_settings is nil' do @@ -28,7 +28,7 @@ RSpec.shared_examples 'a ci_cd_settings predicate method' do |prefix: ''| end it 'returns false' do - expect(project.send("#{prefix}#{delegated_method}")).to be(false) + expect(project.send("#{prefix}#{delegated_method}")).to be(default) end end diff --git a/spec/support/shared_examples/observability/csp_shared_examples.rb b/spec/support/shared_examples/observability/csp_shared_examples.rb index 0cd211f69eb..9d6e7e75f4d 100644 --- a/spec/support/shared_examples/observability/csp_shared_examples.rb +++ b/spec/support/shared_examples/observability/csp_shared_examples.rb @@ -31,19 +31,19 @@ RSpec.shared_examples 'observability csp policy' do |controller_class = describe let(:observability_url) { Gitlab::Observability.observability_url } let(:signin_url) do Gitlab::Utils.append_path(Gitlab.config.gitlab.url, - '/users/sign_in') + '/users/sign_in') end let(:oauth_url) do Gitlab::Utils.append_path(Gitlab.config.gitlab.url, - '/oauth/authorize') + '/oauth/authorize') end before do setup_csp_for_controller(controller_class, csp, any_time: true) group.add_developer(user) login_as(user) - allow(Gitlab::Observability).to receive(:observability_enabled?).and_return(true) + stub_feature_flags(observability_group_tab: true) end subject do @@ -67,7 +67,7 @@ RSpec.shared_examples 'observability csp policy' do |controller_class = describe end before do - allow(Gitlab::Observability).to receive(:observability_enabled?).and_return(false) + stub_feature_flags(observability_group_tab: false) end it 'does not add observability urls to the csp header' do @@ -76,23 +76,6 @@ RSpec.shared_examples 'observability csp policy' do |controller_class = describe end end - context 'when checking if observability is enabled' do - let(:csp) do - ActionDispatch::ContentSecurityPolicy.new do |p| - p.frame_src 'https://something.test' - end - end - - it 'check access for a given user and group' do - allow(Gitlab::Observability).to receive(:observability_enabled?) - - get tested_path - - expect(Gitlab::Observability).to have_received(:observability_enabled?) - .with(user, group).at_least(:once) - end - end - context 'when frame-src exists in the CSP config' do let(:csp) do ActionDispatch::ContentSecurityPolicy.new do |p| diff --git a/spec/support/shared_examples/observability/embed_observabilities_examples.rb b/spec/support/shared_examples/observability/embed_observabilities_examples.rb new file mode 100644 index 00000000000..c8d4e9e0d7e --- /dev/null +++ b/spec/support/shared_examples/observability/embed_observabilities_examples.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'embeds observability' do + it 'renders iframe in description' do + page.within('.description') do + expect_observability_iframe(page.html) + end + end + + it 'renders iframe in comment' do + expect(page).not_to have_css('.note-text') + + page.within('.js-main-target-form') do + fill_in('note[note]', with: observable_url) + click_button('Comment') + end + + wait_for_requests + + page.within('.note-text') do + expect_observability_iframe(page.html) + end + end +end + +RSpec.shared_examples 'does not embed observability' do + it 'does not render iframe in description' do + page.within('.description') do + expect_observability_iframe(page.html, to_be_nil: true) + end + end + + it 'does not render iframe in comment' do + expect(page).not_to have_css('.note-text') + + page.within('.js-main-target-form') do + fill_in('note[note]', with: observable_url) + click_button('Comment') + end + + wait_for_requests + + page.within('.note-text') do + expect_observability_iframe(page.html, to_be_nil: true) + end + end +end + +def expect_observability_iframe(html, to_be_nil: false) + iframe = Nokogiri::HTML.parse(html).at_css('#observability-ui-iframe') + + expect(html).to include(observable_url) + + if to_be_nil + expect(iframe).to be_nil + else + expect(iframe).not_to be_nil + iframe_src = "#{expected_observable_url}&theme=light&username=#{user.username}&kiosk=inline-embed" + expect(iframe.attributes['src'].value).to eq(iframe_src) + end +end diff --git a/spec/support/shared_examples/policies/project_policy_shared_examples.rb b/spec/support/shared_examples/policies/project_policy_shared_examples.rb index 9ec1b8b3f5a..d1f5a01b10c 100644 --- a/spec/support/shared_examples/policies/project_policy_shared_examples.rb +++ b/spec/support/shared_examples/policies/project_policy_shared_examples.rb @@ -401,3 +401,24 @@ RSpec.shared_examples 'package access with repository disabled' do it { is_expected.to be_allowed(:read_package) } end + +RSpec.shared_examples 'equivalent project policy abilities' do + where(:project_visibility, :user_role_on_project) do + project_visibilities = [:public, :internal, :private] + user_role_on_project = [:anonymous, :non_member, :guest, :reporter, :developer, :maintainer, :owner, :admin] + project_visibilities.product(user_role_on_project) + end + + with_them do + it 'evaluates the same' do + project = public_send("#{project_visibility}_project") + current_user = public_send(user_role_on_project) + enable_admin_mode!(current_user) if user_role_on_project == :admin + policy = ProjectPolicy.new(current_user, project) + old_permissions = policy.allowed?(old_policy) + new_permissions = policy.allowed?(new_policy) + + expect(old_permissions).to eq new_permissions + end + end +end diff --git a/spec/support/shared_examples/requests/admin_mode_shared_examples.rb b/spec/support/shared_examples/requests/admin_mode_shared_examples.rb index 07fde7d3f35..ceb57fca786 100644 --- a/spec/support/shared_examples/requests/admin_mode_shared_examples.rb +++ b/spec/support/shared_examples/requests/admin_mode_shared_examples.rb @@ -1,94 +1,99 @@ # frozen_string_literal: true -RSpec.shared_examples 'GET request permissions for admin mode' do - it_behaves_like 'GET request permissions for admin mode when user' - it_behaves_like 'GET request permissions for admin mode when admin' +RSpec.shared_examples 'GET request permissions for admin mode' do |failed_status_code = :forbidden| + it_behaves_like 'GET request permissions for admin mode when user', failed_status_code + it_behaves_like 'GET request permissions for admin mode when admin', failed_status_code end -RSpec.shared_examples 'PUT request permissions for admin mode' do |params| - it_behaves_like 'PUT request permissions for admin mode when user', params - it_behaves_like 'PUT request permissions for admin mode when admin', params +RSpec.shared_examples 'PUT request permissions for admin mode' do |failed_status_code = :forbidden| + it_behaves_like 'PUT request permissions for admin mode when user', failed_status_code + it_behaves_like 'PUT request permissions for admin mode when admin', failed_status_code end -RSpec.shared_examples 'POST request permissions for admin mode' do |params| - it_behaves_like 'POST request permissions for admin mode when user', params - it_behaves_like 'POST request permissions for admin mode when admin', params +RSpec.shared_examples 'POST request permissions for admin mode' do |failed_status_code = :forbidden| + it_behaves_like 'POST request permissions for admin mode when user', failed_status_code + it_behaves_like 'POST request permissions for admin mode when admin', failed_status_code end -RSpec.shared_examples 'DELETE request permissions for admin mode' do - it_behaves_like 'DELETE request permissions for admin mode when user' - it_behaves_like 'DELETE request permissions for admin mode when admin' +RSpec.shared_examples 'DELETE request permissions for admin mode' do |success_status_code: :no_content, + failed_status_code: :forbidden| + + it_behaves_like 'DELETE request permissions for admin mode when user', failed_status_code + it_behaves_like 'DELETE request permissions for admin mode when admin', success_status_code: success_status_code, + failed_status_code: failed_status_code end -RSpec.shared_examples 'GET request permissions for admin mode when user' do +RSpec.shared_examples 'GET request permissions for admin mode when user' do |failed_status_code = :forbidden| subject { get api(path, current_user, admin_mode: admin_mode) } let_it_be(:current_user) { create(:user) } - it_behaves_like 'admin mode on', true, :forbidden - it_behaves_like 'admin mode on', false, :forbidden + it_behaves_like 'admin mode on', true, failed_status_code + it_behaves_like 'admin mode on', false, failed_status_code end -RSpec.shared_examples 'GET request permissions for admin mode when admin' do +RSpec.shared_examples 'GET request permissions for admin mode when admin' do |failed_status_code = :forbidden| subject { get api(path, current_user, admin_mode: admin_mode) } let_it_be(:current_user) { create(:admin) } it_behaves_like 'admin mode on', true, :ok - it_behaves_like 'admin mode on', false, :forbidden + it_behaves_like 'admin mode on', false, failed_status_code end -RSpec.shared_examples 'PUT request permissions for admin mode when user' do |params| +RSpec.shared_examples 'PUT request permissions for admin mode when user' do |failed_status_code = :forbidden| subject { put api(path, current_user, admin_mode: admin_mode), params: params } let_it_be(:current_user) { create(:user) } - it_behaves_like 'admin mode on', true, :forbidden - it_behaves_like 'admin mode on', false, :forbidden + it_behaves_like 'admin mode on', true, failed_status_code + it_behaves_like 'admin mode on', false, failed_status_code end -RSpec.shared_examples 'PUT request permissions for admin mode when admin' do |params| +RSpec.shared_examples 'PUT request permissions for admin mode when admin' do |failed_status_code = :forbidden| subject { put api(path, current_user, admin_mode: admin_mode), params: params } let_it_be(:current_user) { create(:admin) } it_behaves_like 'admin mode on', true, :ok - it_behaves_like 'admin mode on', false, :forbidden + it_behaves_like 'admin mode on', false, failed_status_code end -RSpec.shared_examples 'POST request permissions for admin mode when user' do |params| +RSpec.shared_examples 'POST request permissions for admin mode when user' do |failed_status_code = :forbidden| subject { post api(path, current_user, admin_mode: admin_mode), params: params } let_it_be(:current_user) { create(:user) } - it_behaves_like 'admin mode on', true, :forbidden - it_behaves_like 'admin mode on', false, :forbidden + it_behaves_like 'admin mode on', true, failed_status_code + it_behaves_like 'admin mode on', false, failed_status_code end -RSpec.shared_examples 'POST request permissions for admin mode when admin' do |params| +RSpec.shared_examples 'POST request permissions for admin mode when admin' do |failed_status_code = :forbidden| subject { post api(path, current_user, admin_mode: admin_mode), params: params } let_it_be(:current_user) { create(:admin) } it_behaves_like 'admin mode on', true, :created - it_behaves_like 'admin mode on', false, :forbidden + it_behaves_like 'admin mode on', false, failed_status_code end -RSpec.shared_examples 'DELETE request permissions for admin mode when user' do +RSpec.shared_examples 'DELETE request permissions for admin mode when user' do |failed_status_code = :forbidden| subject { delete api(path, current_user, admin_mode: admin_mode) } let_it_be(:current_user) { create(:user) } - it_behaves_like 'admin mode on', true, :forbidden - it_behaves_like 'admin mode on', false, :forbidden + it_behaves_like 'admin mode on', true, failed_status_code + it_behaves_like 'admin mode on', false, failed_status_code end -RSpec.shared_examples 'DELETE request permissions for admin mode when admin' do +RSpec.shared_examples 'DELETE request permissions for admin mode when admin' do |success_status_code: :no_content, + failed_status_code: :forbidden| + subject { delete api(path, current_user, admin_mode: admin_mode) } let_it_be(:current_user) { create(:admin) } - it_behaves_like 'admin mode on', true, :no_content - it_behaves_like 'admin mode on', false, :forbidden + it_behaves_like 'admin mode on', true, success_status_code + it_behaves_like 'admin mode on', false, failed_status_code end RSpec.shared_examples "admin mode on" do |admin_mode, status| diff --git a/spec/support/shared_examples/requests/api/debian_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/debian_packages_shared_examples.rb index 6d29076da0f..66554f18e80 100644 --- a/spec/support/shared_examples/requests/api/debian_packages_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/debian_packages_shared_examples.rb @@ -165,3 +165,29 @@ RSpec.shared_examples 'Debian packages write endpoint' do |desired_behavior, suc it_behaves_like 'rejects Debian access with unknown container id', :unauthorized, :basic end + +RSpec.shared_examples 'Debian packages index endpoint' do |success_body| + it_behaves_like 'Debian packages read endpoint', 'GET', :success, success_body + + context 'when no ComponentFile is found' do + let(:target_component_name) { component.name + FFaker::Lorem.word } + + it_behaves_like 'Debian packages read endpoint', 'GET', :no_content, /^$/ + end +end + +RSpec.shared_examples 'Debian packages index sha256 endpoint' do |success_body| + it_behaves_like 'Debian packages read endpoint', 'GET', :success, success_body + + context 'with empty checksum' do + let(:target_sha256) { 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855' } + + it_behaves_like 'Debian packages read endpoint', 'GET', :no_content, /^$/ + end + + context 'when ComponentFile is not found' do + let(:target_component_name) { component.name + FFaker::Lorem.word } + + it_behaves_like 'Debian packages read endpoint', 'GET', :not_found, /^{"message":"404 Not Found"}$/ + end +end diff --git a/spec/support/shared_examples/requests/api/discussions_shared_examples.rb b/spec/support/shared_examples/requests/api/discussions_shared_examples.rb index f577e2ad323..a90fa06d458 100644 --- a/spec/support/shared_examples/requests/api/discussions_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/discussions_shared_examples.rb @@ -123,18 +123,6 @@ RSpec.shared_examples 'discussions API' do |parent_type, noteable_type, id_name, expect_snowplow_event(category: 'Notes::CreateService', action: 'execute', label: 'note', value: anything) end - context 'with notes_create_service_tracking feature flag disabled' do - before do - stub_feature_flags(notes_create_service_tracking: false) - end - - it 'does not track Notes::CreateService events', :snowplow do - post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/discussions"), params: { body: 'hi!' } - - expect_no_snowplow_event(category: 'Notes::CreateService', action: 'execute') - end - end - context 'when an admin or owner makes the request' do it 'accepts the creation date to be set' do creation_time = 2.weeks.ago diff --git a/spec/support/shared_examples/requests/api/hooks_shared_examples.rb b/spec/support/shared_examples/requests/api/hooks_shared_examples.rb index f2002de4b55..797c5be802e 100644 --- a/spec/support/shared_examples/requests/api/hooks_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/hooks_shared_examples.rb @@ -135,7 +135,7 @@ RSpec.shared_examples 'web-hook API endpoints' do |prefix| context 'the hook is backed-off' do before do - WebHook::FAILURE_THRESHOLD.times { hook.backoff! } + WebHooks::AutoDisabling::FAILURE_THRESHOLD.times { hook.backoff! } hook.backoff! 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 b55639a6b82..ace76b5ef84 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 @@ -507,55 +507,118 @@ RSpec.shared_examples 'handling get dist tags requests' do |scope: :project| it_behaves_like 'returning response status', status end - 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 - :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 + shared_examples 'handling all conditions' do + where(:auth, :package_name_type, :visibility, :user_role, :expected_result, :expected_status) do + nil | :scoped_naming_convention | :public | nil | :accept | :ok + nil | :scoped_no_naming_convention | :public | nil | :accept | :ok + nil | :unscoped | :public | nil | :accept | :ok + nil | :non_existing | :public | nil | :reject | :not_found + nil | :scoped_naming_convention | :private | nil | :reject | :not_found + nil | :scoped_no_naming_convention | :private | nil | :reject | :not_found + nil | :unscoped | :private | nil | :reject | :not_found + nil | :non_existing | :private | nil | :reject | :not_found + nil | :scoped_naming_convention | :internal | nil | :reject | :not_found + nil | :scoped_no_naming_convention | :internal | nil | :reject | :not_found + nil | :unscoped | :internal | nil | :reject | :not_found + nil | :non_existing | :internal | nil | :reject | :not_found + + :oauth | :scoped_naming_convention | :public | :guest | :accept | :ok + :oauth | :scoped_naming_convention | :public | :reporter | :accept | :ok + :oauth | :scoped_no_naming_convention | :public | :guest | :accept | :ok + :oauth | :scoped_no_naming_convention | :public | :reporter | :accept | :ok + :oauth | :unscoped | :public | :guest | :accept | :ok + :oauth | :unscoped | :public | :reporter | :accept | :ok + :oauth | :non_existing | :public | :guest | :reject | :not_found + :oauth | :non_existing | :public | :reporter | :reject | :not_found + :oauth | :scoped_naming_convention | :private | :guest | :reject | :forbidden + :oauth | :scoped_naming_convention | :private | :reporter | :accept | :ok + :oauth | :scoped_no_naming_convention | :private | :guest | :reject | :forbidden + :oauth | :scoped_no_naming_convention | :private | :reporter | :accept | :ok + :oauth | :unscoped | :private | :guest | :reject | :forbidden + :oauth | :unscoped | :private | :reporter | :accept | :ok + :oauth | :non_existing | :private | :guest | :reject | :forbidden + :oauth | :non_existing | :private | :reporter | :reject | :not_found + :oauth | :scoped_naming_convention | :internal | :guest | :accept | :ok + :oauth | :scoped_naming_convention | :internal | :reporter | :accept | :ok + :oauth | :scoped_no_naming_convention | :internal | :guest | :accept | :ok + :oauth | :scoped_no_naming_convention | :internal | :reporter | :accept | :ok + :oauth | :unscoped | :internal | :guest | :accept | :ok + :oauth | :unscoped | :internal | :reporter | :accept | :ok + :oauth | :non_existing | :internal | :guest | :reject | :not_found + :oauth | :non_existing | :internal | :reporter | :reject | :not_found + + :personal_access_token | :scoped_naming_convention | :public | :guest | :accept | :ok + :personal_access_token | :scoped_naming_convention | :public | :reporter | :accept | :ok + :personal_access_token | :scoped_no_naming_convention | :public | :guest | :accept | :ok + :personal_access_token | :scoped_no_naming_convention | :public | :reporter | :accept | :ok + :personal_access_token | :unscoped | :public | :guest | :accept | :ok + :personal_access_token | :unscoped | :public | :reporter | :accept | :ok + :personal_access_token | :non_existing | :public | :guest | :reject | :not_found + :personal_access_token | :non_existing | :public | :reporter | :reject | :not_found + :personal_access_token | :scoped_naming_convention | :private | :guest | :reject | :forbidden + :personal_access_token | :scoped_naming_convention | :private | :reporter | :accept | :ok + :personal_access_token | :scoped_no_naming_convention | :private | :guest | :reject | :forbidden + :personal_access_token | :scoped_no_naming_convention | :private | :reporter | :accept | :ok + :personal_access_token | :unscoped | :private | :guest | :reject | :forbidden + :personal_access_token | :unscoped | :private | :reporter | :accept | :ok + :personal_access_token | :non_existing | :private | :guest | :reject | :forbidden + :personal_access_token | :non_existing | :private | :reporter | :reject | :not_found + :personal_access_token | :scoped_naming_convention | :internal | :guest | :accept | :ok + :personal_access_token | :scoped_naming_convention | :internal | :reporter | :accept | :ok + :personal_access_token | :scoped_no_naming_convention | :internal | :guest | :accept | :ok + :personal_access_token | :scoped_no_naming_convention | :internal | :reporter | :accept | :ok + :personal_access_token | :unscoped | :internal | :guest | :accept | :ok + :personal_access_token | :unscoped | :internal | :reporter | :accept | :ok + :personal_access_token | :non_existing | :internal | :guest | :reject | :not_found + :personal_access_token | :non_existing | :internal | :reporter | :reject | :not_found + + :job_token | :scoped_naming_convention | :public | :developer | :accept | :ok + :job_token | :scoped_no_naming_convention | :public | :developer | :accept | :ok + :job_token | :unscoped | :public | :developer | :accept | :ok + :job_token | :non_existing | :public | :developer | :reject | :not_found + :job_token | :scoped_naming_convention | :private | :developer | :accept | :ok + :job_token | :scoped_no_naming_convention | :private | :developer | :accept | :ok + :job_token | :unscoped | :private | :developer | :accept | :ok + :job_token | :non_existing | :private | :developer | :reject | :not_found + :job_token | :scoped_naming_convention | :internal | :developer | :accept | :ok + :job_token | :scoped_no_naming_convention | :internal | :developer | :accept | :ok + :job_token | :unscoped | :internal | :developer | :accept | :ok + :job_token | :non_existing | :internal | :developer | :reject | :not_found + + :deploy_token | :scoped_naming_convention | :public | nil | :accept | :ok + :deploy_token | :scoped_no_naming_convention | :public | nil | :accept | :ok + :deploy_token | :unscoped | :public | nil | :accept | :ok + :deploy_token | :non_existing | :public | nil | :reject | :not_found + :deploy_token | :scoped_naming_convention | :private | nil | :accept | :ok + :deploy_token | :scoped_no_naming_convention | :private | nil | :accept | :ok + :deploy_token | :unscoped | :private | nil | :accept | :ok + :deploy_token | :non_existing | :private | nil | :reject | :not_found + :deploy_token | :scoped_naming_convention | :internal | nil | :accept | :ok + :deploy_token | :scoped_no_naming_convention | :internal | nil | :accept | :ok + :deploy_token | :unscoped | :internal | nil | :accept | :ok + :deploy_token | :non_existing | :internal | nil | :reject | :not_found end with_them do - let(:anonymous) { user_role == :anonymous } + let(:headers) do + case auth + when :oauth + build_token_auth_header(token.plaintext_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 - subject { get(url, headers: anonymous ? {} : headers) } + subject { get(url, headers: headers) } before do - project.send("add_#{user_role}", user) unless anonymous + project.send("add_#{user_role}", user) if user_role project.update!(visibility: visibility.to_s) end @@ -571,20 +634,6 @@ RSpec.shared_examples 'handling get dist tags requests' do |scope: :project| end end - shared_examples 'handling all conditions' do - context 'with oauth token' do - let(:headers) { build_token_auth_header(token.plaintext_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' - end - end - context 'with a group namespace' do it_behaves_like 'handling all conditions' end @@ -599,7 +648,6 @@ RSpec.shared_examples 'handling get dist tags requests' do |scope: :project| end RSpec.shared_examples 'handling create dist tag requests' do |scope: :project| - using RSpec::Parameterized::TableSyntax include_context 'set package name from package name type' let_it_be(:tag_name) { 'test' } @@ -617,82 +665,10 @@ RSpec.shared_examples 'handling create dist tag requests' do |scope: :project| it_behaves_like 'returning response status', status end - 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 - :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 - let(:anonymous) { user_role == :anonymous } - - subject { put(url, env: env, headers: headers) } - - before do - project.send("add_#{user_role}", user) unless anonymous - project.update!(visibility: visibility.to_s) - end - - example_name = "#{params[:expected_result]} create package tag request" - status = params[:expected_status] - - if scope == :instance && params[:package_name_type] != :scoped_naming_convention - example_name = 'reject create package tag request' - status = :not_found - end - - it_behaves_like example_name, status: status - end - end - shared_examples 'handling all conditions' do - context 'with oauth token' do - let(:headers) { build_token_auth_header(token.plaintext_token) } + subject { put(url, env: env, headers: headers) } - 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 + it_behaves_like 'handling different package names, visibilities and user roles for tags create or delete', action: :create, scope: scope end context 'with a group namespace' do @@ -709,7 +685,6 @@ RSpec.shared_examples 'handling create dist tag requests' do |scope: :project| end RSpec.shared_examples 'handling delete dist tag requests' do |scope: :project| - using RSpec::Parameterized::TableSyntax include_context 'set package name from package name type' let_it_be(:package_tag) { create(:packages_tag, package: package) } @@ -725,82 +700,10 @@ RSpec.shared_examples 'handling delete dist tag requests' do |scope: :project| it_behaves_like 'returning response status', status end - 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 - :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 - let(:anonymous) { user_role == :anonymous } - - subject { delete(url, headers: headers) } - - before do - project.send("add_#{user_role}", user) unless anonymous - project.update!(visibility: visibility.to_s) - end - - example_name = "#{params[:expected_result]} delete package tag request" - status = params[:expected_status] - - if scope == :instance && params[:package_name_type] != :scoped_naming_convention - example_name = 'reject delete package tag request' - status = :not_found - end - - it_behaves_like example_name, status: status - end - end - shared_examples 'handling all conditions' do - context 'with oauth token' do - let(:headers) { build_token_auth_header(token.plaintext_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) } + subject { delete(url, headers: headers) } - it_behaves_like 'handling different package names, visibilities and user roles' - end + it_behaves_like 'handling different package names, visibilities and user roles for tags create or delete', action: :delete, scope: scope end context 'with a group namespace' do @@ -815,3 +718,141 @@ RSpec.shared_examples 'handling delete dist tag requests' do |scope: :project| end end end + +RSpec.shared_examples 'handling different package names, visibilities and user roles for tags create or delete' do |action:, scope: :project| + using RSpec::Parameterized::TableSyntax + + role = action == :create ? :developer : :maintainer + + where(:auth, :package_name_type, :visibility, :user_role, :expected_result, :expected_status) do + :oauth | :scoped_naming_convention | :public | nil | :reject | :forbidden + :oauth | :scoped_naming_convention | :public | :guest | :reject | :forbidden + :oauth | :scoped_naming_convention | :public | role | :accept | :ok + :oauth | :scoped_no_naming_convention | :public | nil | :reject | :forbidden + :oauth | :scoped_no_naming_convention | :public | :guest | :reject | :forbidden + :oauth | :scoped_no_naming_convention | :public | role | :accept | :ok + :oauth | :unscoped | :public | nil | :reject | :forbidden + :oauth | :unscoped | :public | :guest | :reject | :forbidden + :oauth | :unscoped | :public | role | :accept | :ok + :oauth | :non_existing | :public | nil | :reject | :forbidden + :oauth | :non_existing | :public | :guest | :reject | :forbidden + :oauth | :non_existing | :public | role | :reject | :not_found + :oauth | :scoped_naming_convention | :private | nil | :reject | :not_found + :oauth | :scoped_naming_convention | :private | :guest | :reject | :forbidden + :oauth | :scoped_naming_convention | :private | role | :accept | :ok + :oauth | :scoped_no_naming_convention | :private | nil | :reject | :not_found + :oauth | :scoped_no_naming_convention | :private | :guest | :reject | :forbidden + :oauth | :scoped_no_naming_convention | :private | role | :accept | :ok + :oauth | :unscoped | :private | nil | :reject | :not_found + :oauth | :unscoped | :private | :guest | :reject | :forbidden + :oauth | :unscoped | :private | role | :accept | :ok + :oauth | :non_existing | :private | nil | :reject | :not_found + :oauth | :non_existing | :private | :guest | :reject | :forbidden + :oauth | :non_existing | :private | role | :reject | :not_found + :oauth | :scoped_naming_convention | :internal | nil | :reject | :forbidden + :oauth | :scoped_naming_convention | :internal | :guest | :reject | :forbidden + :oauth | :scoped_naming_convention | :internal | role | :accept | :ok + :oauth | :scoped_no_naming_convention | :internal | nil | :reject | :forbidden + :oauth | :scoped_no_naming_convention | :internal | :guest | :reject | :forbidden + :oauth | :scoped_no_naming_convention | :internal | role | :accept | :ok + :oauth | :unscoped | :internal | nil | :reject | :forbidden + :oauth | :unscoped | :internal | :guest | :reject | :forbidden + :oauth | :unscoped | :internal | role | :accept | :ok + :oauth | :non_existing | :internal | nil | :reject | :forbidden + :oauth | :non_existing | :internal | :guest | :reject | :forbidden + :oauth | :non_existing | :internal | role | :reject | :not_found + + :personal_access_token | :scoped_naming_convention | :public | nil | :reject | :forbidden + :personal_access_token | :scoped_naming_convention | :public | :guest | :reject | :forbidden + :personal_access_token | :scoped_naming_convention | :public | role | :accept | :ok + :personal_access_token | :scoped_no_naming_convention | :public | nil | :reject | :forbidden + :personal_access_token | :scoped_no_naming_convention | :public | :guest | :reject | :forbidden + :personal_access_token | :scoped_no_naming_convention | :public | role | :accept | :ok + :personal_access_token | :unscoped | :public | nil | :reject | :forbidden + :personal_access_token | :unscoped | :public | :guest | :reject | :forbidden + :personal_access_token | :unscoped | :public | role | :accept | :ok + :personal_access_token | :non_existing | :public | nil | :reject | :forbidden + :personal_access_token | :non_existing | :public | :guest | :reject | :forbidden + :personal_access_token | :non_existing | :public | role | :reject | :not_found + :personal_access_token | :scoped_naming_convention | :private | nil | :reject | :not_found + :personal_access_token | :scoped_naming_convention | :private | :guest | :reject | :forbidden + :personal_access_token | :scoped_naming_convention | :private | role | :accept | :ok + :personal_access_token | :scoped_no_naming_convention | :private | nil | :reject | :not_found + :personal_access_token | :scoped_no_naming_convention | :private | :guest | :reject | :forbidden + :personal_access_token | :scoped_no_naming_convention | :private | role | :accept | :ok + :personal_access_token | :unscoped | :private | nil | :reject | :not_found + :personal_access_token | :unscoped | :private | :guest | :reject | :forbidden + :personal_access_token | :unscoped | :private | role | :accept | :ok + :personal_access_token | :non_existing | :private | nil | :reject | :not_found + :personal_access_token | :non_existing | :private | :guest | :reject | :forbidden + :personal_access_token | :non_existing | :private | role | :reject | :not_found + :personal_access_token | :scoped_naming_convention | :internal | nil | :reject | :forbidden + :personal_access_token | :scoped_naming_convention | :internal | :guest | :reject | :forbidden + :personal_access_token | :scoped_naming_convention | :internal | role | :accept | :ok + :personal_access_token | :scoped_no_naming_convention | :internal | nil | :reject | :forbidden + :personal_access_token | :scoped_no_naming_convention | :internal | :guest | :reject | :forbidden + :personal_access_token | :scoped_no_naming_convention | :internal | role | :accept | :ok + :personal_access_token | :unscoped | :internal | nil | :reject | :forbidden + :personal_access_token | :unscoped | :internal | :guest | :reject | :forbidden + :personal_access_token | :unscoped | :internal | role | :accept | :ok + :personal_access_token | :non_existing | :internal | nil | :reject | :forbidden + :personal_access_token | :non_existing | :internal | :guest | :reject | :forbidden + :personal_access_token | :non_existing | :internal | role | :reject | :not_found + + :job_token | :scoped_naming_convention | :public | role | :accept | :ok + :job_token | :scoped_no_naming_convention | :public | role | :accept | :ok + :job_token | :unscoped | :public | role | :accept | :ok + :job_token | :non_existing | :public | role | :reject | :not_found + :job_token | :scoped_naming_convention | :private | role | :accept | :ok + :job_token | :scoped_no_naming_convention | :private | role | :accept | :ok + :job_token | :unscoped | :private | role | :accept | :ok + :job_token | :non_existing | :private | role | :reject | :not_found + :job_token | :scoped_naming_convention | :internal | role | :accept | :ok + :job_token | :scoped_no_naming_convention | :internal | role | :accept | :ok + :job_token | :unscoped | :internal | role | :accept | :ok + :job_token | :non_existing | :internal | role | :reject | :not_found + + :deploy_token | :scoped_naming_convention | :public | nil | :accept | :ok + :deploy_token | :scoped_no_naming_convention | :public | nil | :accept | :ok + :deploy_token | :unscoped | :public | nil | :accept | :ok + :deploy_token | :non_existing | :public | nil | :reject | :not_found + :deploy_token | :scoped_naming_convention | :private | nil | :accept | :ok + :deploy_token | :scoped_no_naming_convention | :private | nil | :accept | :ok + :deploy_token | :unscoped | :private | nil | :accept | :ok + :deploy_token | :non_existing | :private | nil | :reject | :not_found + :deploy_token | :scoped_naming_convention | :internal | nil | :accept | :ok + :deploy_token | :scoped_no_naming_convention | :internal | nil | :accept | :ok + :deploy_token | :unscoped | :internal | nil | :accept | :ok + :deploy_token | :non_existing | :internal | nil | :reject | :not_found + end + + with_them do + let(:headers) do + case auth + when :oauth + build_token_auth_header(token.plaintext_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) + end + end + + before do + project.send("add_#{user_role}", user) if user_role + project.update!(visibility: visibility.to_s) + end + + example_name = "#{params[:expected_result]} #{action} package tag request" + status = params[:expected_status] + + if scope == :instance && params[:package_name_type] != :scoped_naming_convention + example_name = "reject #{action} package tag request" + status = :not_found + end + + it_behaves_like example_name, status: status + end +end diff --git a/spec/support/shared_examples/requests/api/nuget_endpoints_shared_examples.rb b/spec/support/shared_examples/requests/api/nuget_endpoints_shared_examples.rb index 17d8b9c7fab..7cafe8bb368 100644 --- a/spec/support/shared_examples/requests/api/nuget_endpoints_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/nuget_endpoints_shared_examples.rb @@ -36,6 +36,7 @@ RSpec.shared_examples 'handling nuget service requests' do |example_names_with_s with_them do let(:token) { user_token ? personal_access_token.token : 'wrong' } let(:headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) } + let(:snowplow_gitlab_standard_context) { snowplow_context(user_role: user_role) } subject { get api(url), headers: headers } @@ -72,6 +73,7 @@ RSpec.shared_examples 'handling nuget service requests' do |example_names_with_s with_them do let(:job) { user_token ? create(:ci_build, project: project, user: user, status: :running) : double(token: 'wrong') } let(:headers) { user_role == :anonymous ? {} : job_basic_auth_header(job) } + let(:snowplow_gitlab_standard_context) { snowplow_context(user_role: user_role) } subject { get api(url), headers: headers } @@ -140,6 +142,7 @@ RSpec.shared_examples 'handling nuget metadata requests with package name' do |e with_them do let(:token) { user_token ? personal_access_token.token : 'wrong' } let(:headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) } + let(:snowplow_gitlab_standard_context) { snowplow_context(user_role: user_role) } subject { get api(url), headers: headers } @@ -207,6 +210,7 @@ RSpec.shared_examples 'handling nuget metadata requests with package name and pa with_them do let(:token) { user_token ? personal_access_token.token : 'wrong' } let(:headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) } + let(:snowplow_gitlab_standard_context) { snowplow_context(user_role: user_role) } subject { get api(url), headers: headers } @@ -277,6 +281,7 @@ RSpec.shared_examples 'handling nuget search requests' do |example_names_with_st with_them do let(:token) { user_token ? personal_access_token.token : 'wrong' } let(:headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) } + let(:snowplow_gitlab_standard_context) { snowplow_context(user_role: user_role) } subject { get api(url), headers: headers } diff --git a/spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb index 6065b1163c4..9bd430c3b4f 100644 --- a/spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb @@ -254,6 +254,13 @@ RSpec.shared_examples 'pypi simple API endpoint' do with_them do let(:token) { user_token ? personal_access_token.token : 'wrong' } let(:headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) } + let(:snowplow_gitlab_standard_context) do + if user_role == :anonymous || (visibility_level == :public && !user_token) + snowplow_context + else + snowplow_context.merge(user: user) + end + end before do project.update_column(:visibility_level, Gitlab::VisibilityLevel.level_value(visibility_level.to_s)) @@ -269,7 +276,7 @@ RSpec.shared_examples 'pypi simple API endpoint' do let(:url) { "/projects/#{project.id}/packages/pypi/simple/my-package" } let(:headers) { basic_auth_header(user.username, personal_access_token.token) } - let(:snowplow_gitlab_standard_context) { { project: project, namespace: group, property: 'i_package_pypi_user' } } + let(:snowplow_gitlab_standard_context) { snowplow_context.merge({ project: project, user: user }) } it_behaves_like 'PyPI package versions', :developer, :success end diff --git a/spec/support/shared_examples/requests/api/status_shared_examples.rb b/spec/support/shared_examples/requests/api/status_shared_examples.rb index 40843ccbd15..fad5211fc59 100644 --- a/spec/support/shared_examples/requests/api/status_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/status_shared_examples.rb @@ -21,6 +21,23 @@ RSpec.shared_examples '400 response' do end end +RSpec.shared_examples '401 response' do + let(:message) { nil } + + before do + # Fires the request + request + end + + it 'returns 401' do + expect(response).to have_gitlab_http_status(:unauthorized) + + if message.present? + expect(json_response['message']).to eq(message) + end + end +end + RSpec.shared_examples '403 response' do before do # Fires the request diff --git a/spec/support/shared_examples/requests/applications_controller_shared_examples.rb b/spec/support/shared_examples/requests/applications_controller_shared_examples.rb index 642930dd982..4a7a7492398 100644 --- a/spec/support/shared_examples/requests/applications_controller_shared_examples.rb +++ b/spec/support/shared_examples/requests/applications_controller_shared_examples.rb @@ -7,40 +7,14 @@ RSpec.shared_examples 'applications controller - GET #show' do expect(response).to render_template :show end - - context 'when application is viewed after being created' do - before do - create_application - stub_feature_flags(hash_oauth_secrets: false) - end - - it 'sets `@created` instance variable to `true`' do - get show_path - - expect(assigns[:created]).to eq(true) - end - end - - context 'when application is reviewed' do - before do - stub_feature_flags(hash_oauth_secrets: false) - end - - it 'sets `@created` instance variable to `false`' do - get show_path - - expect(assigns[:created]).to eq(false) - end - end end end RSpec.shared_examples 'applications controller - POST #create' do - it "sets `#{OauthApplications::CREATED_SESSION_KEY}` session key to `true`" do - stub_feature_flags(hash_oauth_secrets: false) + it "sets `@created` instance variable to `true`" do create_application - expect(session[OauthApplications::CREATED_SESSION_KEY]).to eq(true) + expect(assigns[:created]).to eq(true) end end diff --git a/spec/support/shared_examples/services/packages/debian/generate_distribution_shared_examples.rb b/spec/support/shared_examples/services/packages/debian/generate_distribution_shared_examples.rb index a3042ac2e26..5abdac07431 100644 --- a/spec/support/shared_examples/services/packages/debian/generate_distribution_shared_examples.rb +++ b/spec/support/shared_examples/services/packages/debian/generate_distribution_shared_examples.rb @@ -29,26 +29,76 @@ RSpec.shared_examples 'Generate Debian Distribution and component files' do let_it_be(:architecture_amd64) { create("debian_#{container_type}_architecture", distribution: distribution, name: 'amd64') } let_it_be(:architecture_arm64) { create("debian_#{container_type}_architecture", distribution: distribution, name: 'arm64') } - let_it_be(:component_file1) { create("debian_#{container_type}_component_file", component: component_contrib, architecture: architecture_all, updated_at: '2020-01-24T08:00:00Z', file_sha256: 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855', file_md5: 'd41d8cd98f00b204e9800998ecf8427e', file_fixture: nil, size: 0) } # updated - let_it_be(:component_file2) { create("debian_#{container_type}_component_file", component: component_main, architecture: architecture_all, updated_at: '2020-01-24T09:00:00Z', file_sha256: 'a') } # destroyed - let_it_be(:component_file3) { create("debian_#{container_type}_component_file", component: component_main, architecture: architecture_amd64, updated_at: '2020-01-24T10:54:59Z', file_sha256: 'b') } # destroyed, 1 second before last generation - let_it_be(:component_file4) { create("debian_#{container_type}_component_file", component: component_contrib, architecture: architecture_all, updated_at: '2020-01-24T10:55:00Z', file_sha256: 'c') } # kept, last generation - let_it_be(:component_file5) { create("debian_#{container_type}_component_file", component: component_contrib, architecture: architecture_all, updated_at: '2020-01-24T10:55:00Z', file_sha256: 'd') } # kept, last generation - let_it_be(:component_file6) { create("debian_#{container_type}_component_file", component: component_contrib, architecture: architecture_amd64, updated_at: '2020-01-25T15:17:18Z', file_sha256: 'e') } # kept, less than 1 hour ago - - def check_component_file(release_date, component_name, component_file_type, architecture_name, expected_content) + let_it_be(:component_file_old_main_amd64) { create("debian_#{container_type}_component_file", component: component_main, architecture: architecture_amd64, updated_at: '2020-01-24T08:00:00Z', file_sha256: 'a') } # destroyed + + let_it_be(:component_file_oldest_kept_contrib_all) { create("debian_#{container_type}_component_file", component: component_contrib, architecture: architecture_all, updated_at: '2020-01-24T10:55:00Z', file_sha256: 'b') } # oldest kept + let_it_be(:component_file_oldest_kept_contrib_amd64) { create("debian_#{container_type}_component_file", component: component_contrib, architecture: architecture_amd64, updated_at: '2020-01-24T10:55:00Z', file_sha256: 'c') } # oldest kept + let_it_be(:component_file_recent_contrib_amd64) { create("debian_#{container_type}_component_file", component: component_contrib, architecture: architecture_amd64, updated_at: '2020-01-25T15:17:18Z', file_sha256: 'd') } # kept, less than 1 hour ago + + let_it_be(:component_file_empty_contrib_all_di) { create("debian_#{container_type}_component_file", :di_packages, :empty, component: component_contrib, architecture: architecture_all, updated_at: '2020-01-24T10:55:00Z') } # oldest kept + let_it_be(:component_file_empty_contrib_amd64_di) { create("debian_#{container_type}_component_file", :di_packages, :empty, component: component_contrib, architecture: architecture_amd64, updated_at: '2020-01-24T10:55:00Z') } # touched, as last empty + let_it_be(:component_file_recent_contrib_amd64_di) { create("debian_#{container_type}_component_file", :di_packages, component: component_contrib, architecture: architecture_amd64, updated_at: '2020-01-25T15:17:18Z', file_sha256: 'f') } # kept, less than 1 hour ago + + let(:pool_prefix) do + prefix = "pool/#{distribution.codename}" + prefix += "/#{project.id}" if container_type == :group + prefix += "/#{package.name[0]}/#{package.name}/#{package.version}" + prefix + end + + let(:expected_main_amd64_di_content) do + <<~MAIN_AMD64_DI_CONTENT + Section: misc + Priority: extra + Filename: #{pool_prefix}/sample-udeb_1.2.3~alpha2_amd64.udeb + Size: 409600 + SHA256: #{package.package_files.with_debian_file_type(:udeb).first.file_sha256} + MAIN_AMD64_DI_CONTENT + end + + let(:expected_main_amd64_di_sha256) { Digest::SHA256.hexdigest(expected_main_amd64_di_content) } + let!(:component_file_old_main_amd64_di) do # touched + create("debian_#{container_type}_component_file", :di_packages, component: component_main, architecture: architecture_amd64, updated_at: '2020-01-24T08:00:00Z', file_sha256: expected_main_amd64_di_sha256).tap do |cf| + cf.update! file: CarrierWaveStringFile.new(expected_main_amd64_di_content), size: expected_main_amd64_di_content.size + end + end + + def check_component_file( + release_date, component_name, component_file_type, architecture_name, expected_content, + updated: true, id_of: nil + ) component_file = distribution .component_files .with_component_name(component_name) .with_file_type(component_file_type) .with_architecture_name(architecture_name) + .with_compression_type(nil) .order_updated_asc .last + if expected_content.nil? + expect(component_file).to be_nil + return + end + expect(component_file).not_to be_nil - expect(component_file.updated_at).to eq(release_date) - unless expected_content.nil? + if id_of + expect(component_file&.id).to eq(id_of.id) + else + # created + expect(component_file&.id).to be > component_file_old_main_amd64_di.id + end + + if updated + expect(component_file.updated_at).to eq(release_date) + else + expect(component_file.updated_at).not_to eq(release_date) + end + + if expected_content == '' + expect(component_file.size).to eq(0) + else expect(expected_content).not_to include('MD5') component_file.file.use_file do |file_path| expect(File.read(file_path)).to eq(expected_content) @@ -57,30 +107,23 @@ RSpec.shared_examples 'Generate Debian Distribution and component files' do end it 'generates Debian distribution and component files', :aggregate_failures do - current_time = Time.utc(2020, 01, 25, 15, 17, 18, 123456) + current_time = Time.utc(2020, 1, 25, 15, 17, 19) travel_to(current_time) do expect(Gitlab::ErrorTracking).not_to receive(:log_exception) - components_count = 2 - architectures_count = 3 - - initial_count = 6 - destroyed_count = 2 - updated_count = 1 - created_count = components_count * (architectures_count * 2 + 1) - updated_count + initial_count = 8 + destroyed_count = 1 + created_count = 4 # main_amd64 + main_sources + empty contrib_all + empty contrib_amd64 expect { subject } .to not_change { Packages::Package.count } .and not_change { Packages::PackageFile.count } .and change { distribution.reload.updated_at }.to(current_time.round) .and change { distribution.component_files.reset.count }.from(initial_count).to(initial_count - destroyed_count + created_count) - .and change { component_file1.reload.updated_at }.to(current_time.round) + .and change { component_file_old_main_amd64_di.reload.updated_at }.to(current_time.round) package_files = package.package_files.order(id: :asc).preload_debian_file_metadata.to_a - pool_prefix = "pool/#{distribution.codename}" - pool_prefix += "/#{project.id}" if container_type == :group - pool_prefix += "/#{package.name[0]}/#{package.name}/#{package.version}" expected_main_amd64_content = <<~EOF Package: libsample0 Source: #{package.name} @@ -120,17 +163,9 @@ RSpec.shared_examples 'Generate Debian Distribution and component files' do SHA256: #{package_files[3].file_sha256} EOF - expected_main_amd64_di_content = <<~EOF - Section: misc - Priority: extra - Filename: #{pool_prefix}/sample-udeb_1.2.3~alpha2_amd64.udeb - Size: 409600 - SHA256: #{package_files[4].file_sha256} - EOF - expected_main_sources_content = <<~EOF Package: #{package.name} - Binary: sample-dev, libsample0, sample-udeb + Binary: sample-dev, libsample0, sample-udeb, sample-ddeb Version: #{package.version} Maintainer: #{package_files[1].debian_fields['Maintainer']} Build-Depends: debhelper-compat (= 13) @@ -139,13 +174,13 @@ RSpec.shared_examples 'Generate Debian Distribution and component files' do Format: 3.0 (native) Files: #{package_files[1].file_md5} #{package_files[1].size} #{package_files[1].file_name} - d5ca476e4229d135a88f9c729c7606c9 864 sample_1.2.3~alpha2.tar.xz + #{package_files[0].file_md5} 964 #{package_files[0].file_name} Checksums-Sha256: #{package_files[1].file_sha256} #{package_files[1].size} #{package_files[1].file_name} - 40e4682bb24a73251ccd7c7798c0094a649091e5625d6a14bcec9b4e7174f3da 864 sample_1.2.3~alpha2.tar.xz + #{package_files[0].file_sha256} 964 #{package_files[0].file_name} Checksums-Sha1: #{package_files[1].file_sha1} #{package_files[1].size} #{package_files[1].file_name} - c5cfc111ea924842a89a06d5673f07dfd07de8ca 864 sample_1.2.3~alpha2.tar.xz + #{package_files[0].file_sha1} 964 #{package_files[0].file_name} Homepage: #{package_files[1].debian_fields['Homepage']} Section: misc Priority: extra @@ -157,42 +192,38 @@ RSpec.shared_examples 'Generate Debian Distribution and component files' do check_component_file(current_time.round, 'main', :packages, 'arm64', nil) check_component_file(current_time.round, 'main', :di_packages, 'all', nil) - check_component_file(current_time.round, 'main', :di_packages, 'amd64', expected_main_amd64_di_content) + check_component_file(current_time.round, 'main', :di_packages, 'amd64', expected_main_amd64_di_content, id_of: component_file_old_main_amd64_di) check_component_file(current_time.round, 'main', :di_packages, 'arm64', nil) check_component_file(current_time.round, 'main', :sources, nil, expected_main_sources_content) - check_component_file(current_time.round, 'contrib', :packages, 'all', nil) - check_component_file(current_time.round, 'contrib', :packages, 'amd64', nil) + check_component_file(current_time.round, 'contrib', :packages, 'all', '') + check_component_file(current_time.round, 'contrib', :packages, 'amd64', '') check_component_file(current_time.round, 'contrib', :packages, 'arm64', nil) - check_component_file(current_time.round, 'contrib', :di_packages, 'all', nil) - check_component_file(current_time.round, 'contrib', :di_packages, 'amd64', nil) + check_component_file(current_time.round, 'contrib', :di_packages, 'all', '', updated: false, id_of: component_file_empty_contrib_all_di) + check_component_file(current_time.round, 'contrib', :di_packages, 'amd64', '', id_of: component_file_empty_contrib_amd64_di) check_component_file(current_time.round, 'contrib', :di_packages, 'arm64', nil) check_component_file(current_time.round, 'contrib', :sources, nil, nil) - main_amd64_size = expected_main_amd64_content.length - main_amd64_sha256 = Digest::SHA256.hexdigest(expected_main_amd64_content) + expected_main_amd64_size = expected_main_amd64_content.length + expected_main_amd64_sha256 = Digest::SHA256.hexdigest(expected_main_amd64_content) - contrib_all_size = component_file1.size - contrib_all_sha256 = component_file1.file_sha256 + expected_main_amd64_di_size = expected_main_amd64_di_content.length - main_amd64_di_size = expected_main_amd64_di_content.length - main_amd64_di_sha256 = Digest::SHA256.hexdigest(expected_main_amd64_di_content) - - main_sources_size = expected_main_sources_content.length - main_sources_sha256 = Digest::SHA256.hexdigest(expected_main_sources_content) + expected_main_sources_size = expected_main_sources_content.length + expected_main_sources_sha256 = Digest::SHA256.hexdigest(expected_main_sources_content) expected_release_content = <<~EOF Codename: #{distribution.codename} - Date: Sat, 25 Jan 2020 15:17:18 +0000 - Valid-Until: Mon, 27 Jan 2020 15:17:18 +0000 + Date: Sat, 25 Jan 2020 15:17:19 +0000 + Valid-Until: Mon, 27 Jan 2020 15:17:19 +0000 Acquire-By-Hash: yes Architectures: all amd64 arm64 Components: contrib main SHA256: - #{contrib_all_sha256} #{contrib_all_size.to_s.rjust(8)} contrib/binary-all/Packages + e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 contrib/binary-all/Packages e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 contrib/debian-installer/binary-all/Packages e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 contrib/binary-amd64/Packages e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 contrib/debian-installer/binary-amd64/Packages @@ -201,11 +232,11 @@ RSpec.shared_examples 'Generate Debian Distribution and component files' do e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 contrib/source/Sources e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 main/binary-all/Packages e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 main/debian-installer/binary-all/Packages - #{main_amd64_sha256} #{main_amd64_size.to_s.rjust(8)} main/binary-amd64/Packages - #{main_amd64_di_sha256} #{main_amd64_di_size.to_s.rjust(8)} main/debian-installer/binary-amd64/Packages + #{expected_main_amd64_sha256} #{expected_main_amd64_size.to_s.rjust(8)} main/binary-amd64/Packages + #{expected_main_amd64_di_sha256} #{expected_main_amd64_di_size.to_s.rjust(8)} main/debian-installer/binary-amd64/Packages e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 main/binary-arm64/Packages e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 main/debian-installer/binary-arm64/Packages - #{main_sources_sha256} #{main_sources_size.to_s.rjust(8)} main/source/Sources + #{expected_main_sources_sha256} #{expected_main_sources_size.to_s.rjust(8)} main/source/Sources EOF expected_release_content = "Suite: #{distribution.suite}\n#{expected_release_content}" if distribution.suite @@ -222,7 +253,7 @@ RSpec.shared_examples 'Generate Debian Distribution and component files' do context 'without components and architectures' do it 'generates minimal distribution', :aggregate_failures do - travel_to(Time.utc(2020, 01, 25, 15, 17, 18, 123456)) do + travel_to(Time.utc(2020, 1, 25, 15, 17, 18, 123456)) do expect(Gitlab::ErrorTracking).not_to receive(:log_exception) expect { subject } diff --git a/spec/support/shared_examples/services/projects/update_repository_storage_service_shared_examples.rb b/spec/support/shared_examples/services/projects/update_repository_storage_service_shared_examples.rb index 9f940d27341..2070cac24b0 100644 --- a/spec/support/shared_examples/services/projects/update_repository_storage_service_shared_examples.rb +++ b/spec/support/shared_examples/services/projects/update_repository_storage_service_shared_examples.rb @@ -63,35 +63,6 @@ RSpec.shared_examples 'moves repository to another storage' do |repository_type| expect(gitlab_shell.repository_exists?('default', old_project_repository_path)).to be(false) expect(gitlab_shell.repository_exists?('default', old_repository_path)).to be(false) end - - context ':repack_after_shard_migration feature flag disabled' do - before do - stub_feature_flags(repack_after_shard_migration: false) - end - - it 'does not enqueue a GC run' do - expect { subject.execute } - .not_to change { Projects::GitGarbageCollectWorker.jobs.count } - end - end - - context ':repack_after_shard_migration feature flag enabled' do - before do - stub_feature_flags(repack_after_shard_migration: true) - end - - it 'does not enqueue a GC run if housekeeping is disabled' do - stub_application_setting(housekeeping_enabled: false) - - expect { subject.execute } - .not_to change { Projects::GitGarbageCollectWorker.jobs.count } - end - - it 'enqueues a GC run' do - expect { subject.execute } - .to change { Projects::GitGarbageCollectWorker.jobs.count }.by(1) - end - end end context 'when the filesystems are the same' do diff --git a/spec/support/shared_examples/services/security/ci_configuration/create_service_shared_examples.rb b/spec/support/shared_examples/services/security/ci_configuration/create_service_shared_examples.rb index 209be09c807..9fcdd296ebe 100644 --- a/spec/support/shared_examples/services/security/ci_configuration/create_service_shared_examples.rb +++ b/spec/support/shared_examples/services/security/ci_configuration/create_service_shared_examples.rb @@ -145,7 +145,7 @@ RSpec.shared_examples_for 'services security ci configuration create service' do let_it_be(:repository) { project.repository } it 'is successful' do - expect(repository).to receive(:root_ref_sha).and_raise(StandardError) + expect(repository).to receive(:commit).and_return(nil) expect(result.status).to eq(:success) end end @@ -168,7 +168,7 @@ RSpec.shared_examples_for 'services security ci configuration create service' do it 'returns an error' do expect { result }.to raise_error { |error| expect(error).to be_a(Gitlab::Graphql::Errors::MutationError) - expect(error.message).to eq('You must <a target="_blank" rel="noopener noreferrer" ' \ + expect(error.message).to eq('UF: You must <a target="_blank" rel="noopener noreferrer" ' \ 'href="http://localhost/help/user/project/repository/index.md' \ '#add-files-to-a-repository">add at least one file to the repository' \ '</a> before using Security features.') diff --git a/spec/support/shared_examples/services/snowplow_tracking_shared_examples.rb b/spec/support/shared_examples/services/snowplow_tracking_shared_examples.rb index e72e8e79411..d3b3434b339 100644 --- a/spec/support/shared_examples/services/snowplow_tracking_shared_examples.rb +++ b/spec/support/shared_examples/services/snowplow_tracking_shared_examples.rb @@ -5,7 +5,6 @@ RSpec.shared_examples 'issue_edit snowplow tracking' do let(:action) { Gitlab::UsageDataCounters::IssueActivityUniqueCounter::ISSUE_ACTION } let(:label) { Gitlab::UsageDataCounters::IssueActivityUniqueCounter::ISSUE_LABEL } let(:namespace) { project.namespace } - let(:feature_flag_name) { :route_hll_to_snowplow_phase2 } it_behaves_like 'Snowplow event tracking with RedisHLL context' end diff --git a/spec/support/stub_dot_com_check.rb b/spec/support/stub_dot_com_check.rb new file mode 100644 index 00000000000..6934b33d111 --- /dev/null +++ b/spec/support/stub_dot_com_check.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +RSpec.configure do |config| + %i[saas saas_registration].each do |metadata| + config.before(:context, metadata) do + # Ensure Gitlab.com? returns true during context. + # This is needed for let_it_be which is shared across examples, + # therefore the value must be changed in before_all, + # but RSpec prevent stubbing method calls in before_all, + # therefore we have to resort to temporarily swap url value. + @_original_gitlab_url = Gitlab.config.gitlab['url'] + Gitlab.config.gitlab['url'] = Gitlab::Saas.com_url + end + + config.after(:context, metadata) do + # Swap back original value + Gitlab.config.gitlab['url'] = @_original_gitlab_url + end + end +end |