diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-11-19 11:27:35 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-11-19 11:27:35 +0300 |
commit | 7e9c479f7de77702622631cff2628a9c8dcbc627 (patch) | |
tree | c8f718a08e110ad7e1894510980d2155a6549197 /spec/support | |
parent | e852b0ae16db4052c1c567d9efa4facc81146e88 (diff) |
Add latest changes from gitlab-org/gitlab@13-6-stable-eev13.6.0-rc42
Diffstat (limited to 'spec/support')
87 files changed, 3011 insertions, 1085 deletions
diff --git a/spec/support/cycle_analytics_helpers/test_generation.rb b/spec/support/cycle_analytics_helpers/test_generation.rb index c577e5cc665..f866220b919 100644 --- a/spec/support/cycle_analytics_helpers/test_generation.rb +++ b/spec/support/cycle_analytics_helpers/test_generation.rb @@ -6,7 +6,7 @@ # multiple nested contexts. This shouldn't count as a violation. module CycleAnalyticsHelpers module TestGeneration - # Generate the most common set of specs that all cycle analytics phases need to have. + # Generate the most common set of specs that all value stream analytics phases need to have. # # Arguments: # @@ -14,10 +14,10 @@ module CycleAnalyticsHelpers # data_fn: A function that returns a hash, constituting initial data for the test case # start_time_conditions: An array of `conditions`. Each condition is an tuple of `condition_name` and `condition_fn`. `condition_fn` is called with # `context` (no lexical scope, so need to do `context.create` for factories, for example) and `data` (from the `data_fn`). - # Each `condition_fn` is expected to implement a case which consitutes the start of the given cycle analytics phase. + # Each `condition_fn` is expected to implement a case which consitutes the start of the given value stream analytics phase. # end_time_conditions: An array of `conditions`. Each condition is an tuple of `condition_name` and `condition_fn`. `condition_fn` is called with # `context` (no lexical scope, so need to do `context.create` for factories, for example) and `data` (from the `data_fn`). - # Each `condition_fn` is expected to implement a case which consitutes the end of the given cycle analytics phase. + # Each `condition_fn` is expected to implement a case which consitutes the end of the given value stream analytics phase. # before_end_fn: This function is run before calling the end time conditions. Used for setup that needs to be run between the start and end conditions. # post_fn: Code that needs to be run after running the end time conditions. diff --git a/spec/support/helpers/admin_mode_helpers.rb b/spec/support/helpers/admin_mode_helpers.rb index 36ed262f8ae..a6e31791127 100644 --- a/spec/support/helpers/admin_mode_helpers.rb +++ b/spec/support/helpers/admin_mode_helpers.rb @@ -13,6 +13,8 @@ module AdminModeHelper def enable_admin_mode!(user) fake_user_mode = instance_double(Gitlab::Auth::CurrentUserMode) + allow(Gitlab::Auth::CurrentUserMode).to receive(:new).and_call_original + allow(Gitlab::Auth::CurrentUserMode).to receive(:new).with(user).and_return(fake_user_mode) allow(fake_user_mode).to receive(:admin_mode?).and_return(user&.admin?) end diff --git a/spec/support/helpers/api_helpers.rb b/spec/support/helpers/api_helpers.rb index b1e6078c4f2..d3cc7367b6e 100644 --- a/spec/support/helpers/api_helpers.rb +++ b/spec/support/helpers/api_helpers.rb @@ -61,7 +61,6 @@ module ApiHelpers def expect_response_contain_exactly(*items) expect(response).to have_gitlab_http_status(:ok) expect(json_response).to be_an Array - expect(json_response.length).to eq(items.size) expect(json_response.map { |item| item['id'] }).to contain_exactly(*items) end diff --git a/spec/support/helpers/dependency_proxy_helpers.rb b/spec/support/helpers/dependency_proxy_helpers.rb new file mode 100644 index 00000000000..545b9d1f4d0 --- /dev/null +++ b/spec/support/helpers/dependency_proxy_helpers.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module DependencyProxyHelpers + include StubRequests + + def stub_registry_auth(image, token, status = 200, body = nil) + auth_body = { 'token' => token }.to_json + auth_link = registry.auth_url(image) + + stub_full_request(auth_link) + .to_return(status: status, body: body || auth_body) + end + + def stub_manifest_download(image, tag, status = 200, body = nil) + manifest_url = registry.manifest_url(image, tag) + + stub_full_request(manifest_url) + .to_return(status: status, body: body || manifest) + end + + def stub_blob_download(image, blob_sha, status = 200, body = '123456') + download_link = registry.blob_url(image, blob_sha) + + stub_full_request(download_link) + .to_return(status: status, body: body) + end + + private + + def registry + @registry ||= DependencyProxy::Registry + end +end diff --git a/spec/support/helpers/features/members_table_helpers.rb b/spec/support/helpers/features/members_table_helpers.rb new file mode 100644 index 00000000000..5394e370900 --- /dev/null +++ b/spec/support/helpers/features/members_table_helpers.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +module Spec + module Support + module Helpers + module Features + module MembersHelpers + def members_table + page.find('[data-testid="members-table"]') + end + + def all_rows + page.within(members_table) do + page.all('tbody > tr') + end + end + + def first_row + all_rows[0] + end + + def second_row + all_rows[1] + end + + def third_row + all_rows[2] + end + + def invite_users_form + page.find('[data-testid="invite-users-form"]') + end + end + end + end + end +end diff --git a/spec/support/helpers/features/releases_helpers.rb b/spec/support/helpers/features/releases_helpers.rb index 0d46918b05c..44087f71cfa 100644 --- a/spec/support/helpers/features/releases_helpers.rb +++ b/spec/support/helpers/features/releases_helpers.rb @@ -66,7 +66,7 @@ module Spec focused_element.send_keys(:enter) # Wait for the dropdown to be rendered - page.find('.project-milestone-combobox .dropdown-menu') + page.find('.milestone-combobox .dropdown-menu') # Clear any existing input focused_element.attribute('value').length.times { focused_element.send_keys(:backspace) } @@ -75,7 +75,7 @@ module Spec focused_element.send_keys(milestone_title, :enter) # Wait for the search to return - page.find('.project-milestone-combobox .dropdown-item', text: milestone_title, match: :first) + page.find('.milestone-combobox .dropdown-item', text: milestone_title, match: :first) focused_element.send_keys(:arrow_down, :arrow_down, :enter) diff --git a/spec/support/helpers/features/web_ide_spec_helpers.rb b/spec/support/helpers/features/web_ide_spec_helpers.rb index 123bd9b5070..12d3cecd052 100644 --- a/spec/support/helpers/features/web_ide_spec_helpers.rb +++ b/spec/support/helpers/features/web_ide_spec_helpers.rb @@ -22,6 +22,8 @@ module WebIdeSpecHelpers click_link('Web IDE') wait_for_requests + + save_monaco_editor_reference end def ide_tree_body @@ -36,8 +38,8 @@ module WebIdeSpecHelpers ".js-ide-#{mode}-mode" end - def ide_file_row_open?(row) - row.matches_css?('.is-open') + def ide_folder_row_open?(row) + row.matches_css?('.folder.is-open') end # Creates a file in the IDE by expanding directories @@ -63,6 +65,17 @@ module WebIdeSpecHelpers ide_set_editor_value(content) end + def ide_rename_file(path, new_path) + container = ide_traverse_to_file(path) + + click_file_action(container, 'Rename/Move') + + within '#ide-new-entry' do + find('input').fill_in(with: new_path) + click_button('Rename file') + end + end + # Deletes a file by traversing to `path` # then clicking the 'Delete' action. # @@ -90,8 +103,22 @@ module WebIdeSpecHelpers container end + def ide_close_file(name) + within page.find('.multi-file-tabs') do + click_button("Close #{name}") + end + end + + def ide_open_file(path) + row = ide_traverse_to_file(path) + + ide_open_file_row(row) + + wait_for_requests + end + def ide_open_file_row(row) - return if ide_file_row_open?(row) + return if ide_folder_row_open?(row) row.click end @@ -103,6 +130,10 @@ module WebIdeSpecHelpers execute_script("monaco.editor.getModel('#{uri}').setValue('#{escape_javascript(value)}')") end + def ide_set_editor_position(line, col) + execute_script("TEST_EDITOR.setPosition(#{{ lineNumber: line, column: col }.to_json})") + end + def ide_editor_value editor = find('.monaco-editor') uri = editor['data-uri'] @@ -149,4 +180,8 @@ module WebIdeSpecHelpers wait_for_requests end end + + def save_monaco_editor_reference + evaluate_script("monaco.editor.onDidCreateEditor(editor => { window.TEST_EDITOR = editor; })") + end end diff --git a/spec/support/helpers/graphql_helpers.rb b/spec/support/helpers/graphql_helpers.rb index db769041f1e..a1b4e6eee92 100644 --- a/spec/support/helpers/graphql_helpers.rb +++ b/spec/support/helpers/graphql_helpers.rb @@ -17,8 +17,8 @@ module GraphqlHelpers # ready, then the early return is returned instead. # # Then the resolve method is called. - def resolve(resolver_class, obj: nil, args: {}, ctx: {}, field: nil) - resolver = resolver_class.new(object: obj, context: ctx, field: field) + def resolve(resolver_class, args: {}, **resolver_args) + resolver = resolver_instance(resolver_class, **resolver_args) ready, early_return = sync_all { resolver.ready?(**args) } return early_return unless ready @@ -26,6 +26,15 @@ module GraphqlHelpers resolver.resolve(**args) end + def resolver_instance(resolver_class, obj: nil, ctx: {}, field: nil, schema: GitlabSchema) + if ctx.is_a?(Hash) + q = double('Query', schema: schema) + ctx = GraphQL::Query::Context.new(query: q, object: obj, values: ctx) + end + + resolver_class.new(object: obj, context: ctx, field: field) + end + # Eagerly run a loader's named resolver # (syncs any lazy values returned by resolve) def eager_resolve(resolver_class, **opts) @@ -112,6 +121,16 @@ module GraphqlHelpers end end + def resolve_field(name, object, args = {}) + context = double("Context", + schema: GitlabSchema, + query: GraphQL::Query.new(GitlabSchema), + parent: nil) + field = described_class.fields[name] + instance = described_class.authorized_new(object, context) + field.resolve_field(instance, {}, context) + end + # Recursively convert a Hash with Ruby-style keys to GraphQL fieldname-style keys # # prepare_input_for_mutation({ 'my_key' => 1 }) @@ -468,6 +487,8 @@ module GraphqlHelpers use Gitlab::Graphql::Authorize use Gitlab::Graphql::Pagination::Connections + lazy_resolve ::Gitlab::Graphql::Lazy, :force + query(query_type) end diff --git a/spec/support/helpers/kubernetes_helpers.rb b/spec/support/helpers/kubernetes_helpers.rb index 113bb31e4be..ff61cceba06 100644 --- a/spec/support/helpers/kubernetes_helpers.rb +++ b/spec/support/helpers/kubernetes_helpers.rb @@ -33,8 +33,8 @@ module KubernetesHelpers kube_response(kube_deployments_body) end - def kube_ingresses_response - kube_response(kube_ingresses_body) + def kube_ingresses_response(with_canary: false) + kube_response(kube_ingresses_body(with_canary: with_canary)) end def stub_kubeclient_discover_base(api_url) @@ -155,12 +155,12 @@ module KubernetesHelpers WebMock.stub_request(:get, deployments_url).to_return(response || kube_deployments_response) end - def stub_kubeclient_ingresses(namespace, status: nil) + def stub_kubeclient_ingresses(namespace, status: nil, method: :get, resource_path: "", response: kube_ingresses_response) stub_kubeclient_discover(service.api_url) - ingresses_url = service.api_url + "/apis/extensions/v1beta1/namespaces/#{namespace}/ingresses" + ingresses_url = service.api_url + "/apis/extensions/v1beta1/namespaces/#{namespace}/ingresses#{resource_path}" response = { status: status } if status - WebMock.stub_request(:get, ingresses_url).to_return(response || kube_ingresses_response) + WebMock.stub_request(method, ingresses_url).to_return(response) end def stub_kubeclient_knative_services(options = {}) @@ -250,6 +250,11 @@ module KubernetesHelpers .to_return(kube_response({})) end + def stub_kubeclient_delete_role_binding(api_url, name, namespace: 'default') + WebMock.stub_request(:delete, api_url + "/apis/rbac.authorization.k8s.io/v1/namespaces/#{namespace}/rolebindings/#{name}") + .to_return(kube_response({})) + end + def stub_kubeclient_put_role_binding(api_url, name, namespace: 'default') WebMock.stub_request(:put, api_url + "/apis/rbac.authorization.k8s.io/v1/namespaces/#{namespace}/rolebindings/#{name}") .to_return(kube_response({})) @@ -541,10 +546,12 @@ module KubernetesHelpers } end - def kube_ingresses_body + def kube_ingresses_body(with_canary: false) + items = with_canary ? [kube_ingress, kube_ingress(track: :canary)] : [kube_ingress] + { "kind" => "List", - "items" => [kube_ingress] + "items" => items } end diff --git a/spec/support/helpers/lfs_http_helpers.rb b/spec/support/helpers/lfs_http_helpers.rb index 0537b122040..199d5e70e32 100644 --- a/spec/support/helpers/lfs_http_helpers.rb +++ b/spec/support/helpers/lfs_http_helpers.rb @@ -31,16 +31,16 @@ module LfsHttpHelpers post(url, params: params, headers: headers) end - def batch_url(project) - "#{project.http_url_to_repo}/info/lfs/objects/batch" + def batch_url(container) + "#{container.http_url_to_repo}/info/lfs/objects/batch" end - def objects_url(project, oid = nil, size = nil) - File.join(["#{project.http_url_to_repo}/gitlab-lfs/objects", oid, size].compact.map(&:to_s)) + def objects_url(container, oid = nil, size = nil) + File.join(["#{container.http_url_to_repo}/gitlab-lfs/objects", oid, size].compact.map(&:to_s)) end - def authorize_url(project, oid, size) - File.join(objects_url(project, oid, size), 'authorize') + def authorize_url(container, oid, size) + File.join(objects_url(container, oid, size), 'authorize') end def download_body(objects) diff --git a/spec/support/helpers/navbar_structure_helper.rb b/spec/support/helpers/navbar_structure_helper.rb index 11e67ba394c..e18a708e41c 100644 --- a/spec/support/helpers/navbar_structure_helper.rb +++ b/spec/support/helpers/navbar_structure_helper.rb @@ -36,4 +36,12 @@ module NavbarStructureHelper new_sub_nav_item_name: _('Container Registry') ) end + + def insert_dependency_proxy_nav(within) + insert_after_sub_nav_item( + _('Package Registry'), + within: _('Packages & Registries'), + new_sub_nav_item_name: _('Dependency Proxy') + ) + end end diff --git a/spec/support/helpers/require_migration.rb b/spec/support/helpers/require_migration.rb index d3f192a4142..c2902aa4ec7 100644 --- a/spec/support/helpers/require_migration.rb +++ b/spec/support/helpers/require_migration.rb @@ -3,26 +3,46 @@ require 'find' class RequireMigration - MIGRATION_FOLDERS = %w(db/migrate db/post_migrate ee/db/geo/migrate ee/db/geo/post_migrate).freeze + class AutoLoadError < RuntimeError + MESSAGE = "Can not find any migration file for `%{file_name}`!\n" \ + "You can try to provide the migration file name manually." + + def initialize(file_name) + message = format(MESSAGE, file_name: file_name) + + super(message) + end + end + + MIGRATION_FOLDERS = %w[db/migrate db/post_migrate].freeze SPEC_FILE_PATTERN = /.+\/(?<file_name>.+)_spec\.rb/.freeze class << self def require_migration!(file_name) file_paths = search_migration_file(file_name) + raise AutoLoadError.new(file_name) unless file_paths.first require file_paths.first end def search_migration_file(file_name) - MIGRATION_FOLDERS.flat_map do |path| + migration_folders.flat_map do |path| migration_path = Rails.root.join(path).to_s Find.find(migration_path).grep(/\d+_#{file_name}\.rb/) end end + + private + + def migration_folders + MIGRATION_FOLDERS + end end end +RequireMigration.prepend_if_ee('EE::RequireMigration') + def require_migration!(file_name = nil) location_info = caller_locations.first.path.match(RequireMigration::SPEC_FILE_PATTERN) file_name ||= location_info[:file_name] diff --git a/spec/support/helpers/search_helpers.rb b/spec/support/helpers/search_helpers.rb index 328f272724a..3d4ff4801a7 100644 --- a/spec/support/helpers/search_helpers.rb +++ b/spec/support/helpers/search_helpers.rb @@ -3,13 +3,14 @@ module SearchHelpers def fill_in_search(text) page.within('.search-input-wrap') do + find('#search').click fill_in('search', with: text) end wait_for_all_requests end - def submit_search(query, scope: nil) + def submit_search(query) page.within('.search-form, .search-page-form') do field = find_field('search') field.fill_in(with: query) diff --git a/spec/support/helpers/snowplow_helpers.rb b/spec/support/helpers/snowplow_helpers.rb index 3bde01c6fbf..15eac1b24fc 100644 --- a/spec/support/helpers/snowplow_helpers.rb +++ b/spec/support/helpers/snowplow_helpers.rb @@ -32,16 +32,8 @@ module SnowplowHelpers # end # end def expect_snowplow_event(category:, action:, **kwargs) - # This check will no longer be needed with Ruby 2.7 which - # would not pass any arguments when using **kwargs. - # https://gitlab.com/gitlab-org/gitlab/-/issues/263430 - if kwargs.present? - expect(Gitlab::Tracking).to have_received(:event) - .with(category, action, **kwargs).at_least(:once) - else - expect(Gitlab::Tracking).to have_received(:event) - .with(category, action).at_least(:once) - end + expect(Gitlab::Tracking).to have_received(:event) # rubocop:disable RSpec/ExpectGitlabTracking + .with(category, action, **kwargs).at_least(:once) end # Asserts that no call to `Gitlab::Tracking#event` was made. @@ -56,6 +48,6 @@ module SnowplowHelpers # end # end def expect_no_snowplow_event - expect(Gitlab::Tracking).not_to have_received(:event) + expect(Gitlab::Tracking).not_to have_received(:event) # rubocop:disable RSpec/ExpectGitlabTracking end end diff --git a/spec/support/helpers/table_schema_helpers.rb b/spec/support/helpers/table_schema_helpers.rb new file mode 100644 index 00000000000..28794211190 --- /dev/null +++ b/spec/support/helpers/table_schema_helpers.rb @@ -0,0 +1,112 @@ +# frozen_string_literal: true + +module TableSchemaHelpers + def connection + ActiveRecord::Base.connection + end + + def expect_table_to_be_replaced(original_table:, replacement_table:, archived_table:) + original_oid = table_oid(original_table) + replacement_oid = table_oid(replacement_table) + + yield + + expect(table_oid(original_table)).to eq(replacement_oid) + expect(table_oid(archived_table)).to eq(original_oid) + expect(table_oid(replacement_table)).to be_nil + end + + def expect_index_to_exist(name, schema: nil) + expect(index_exists_by_name(name, schema: schema)).to eq(true) + end + + def expect_index_not_to_exist(name, schema: nil) + expect(index_exists_by_name(name, schema: schema)).to be_nil + end + + def expect_primary_keys_after_tables(tables, schema: nil) + tables.each do |table| + primary_key = primary_key_constraint_name(table, schema: schema) + + expect(primary_key).to eq("#{table}_pkey") + end + end + + def table_oid(name) + connection.select_value(<<~SQL) + SELECT oid + FROM pg_catalog.pg_class + WHERE relname = '#{name}' + SQL + end + + def table_type(name) + connection.select_value(<<~SQL) + SELECT + CASE class.relkind + WHEN 'r' THEN 'normal' + WHEN 'p' THEN 'partitioned' + ELSE 'other' + END as table_type + FROM pg_catalog.pg_class class + WHERE class.relname = '#{name}' + SQL + end + + def sequence_owned_by(table_name, column_name) + connection.select_value(<<~SQL) + SELECT + sequence.relname as name + FROM pg_catalog.pg_class as sequence + INNER JOIN pg_catalog.pg_depend depend + ON depend.objid = sequence.oid + INNER JOIN pg_catalog.pg_class class + ON class.oid = depend.refobjid + INNER JOIN pg_catalog.pg_attribute attribute + ON attribute.attnum = depend.refobjsubid + AND attribute.attrelid = depend.refobjid + WHERE class.relname = '#{table_name}' + AND attribute.attname = '#{column_name}' + SQL + end + + def default_expression_for(table_name, column_name) + connection.select_value(<<~SQL) + SELECT + pg_get_expr(attrdef.adbin, attrdef.adrelid) AS default_value + FROM pg_catalog.pg_attribute attribute + INNER JOIN pg_catalog.pg_attrdef attrdef + ON attribute.attrelid = attrdef.adrelid + AND attribute.attnum = attrdef.adnum + WHERE attribute.attrelid = '#{table_name}'::regclass + AND attribute.attname = '#{column_name}' + SQL + end + + def primary_key_constraint_name(table_name, schema: nil) + table_name = schema ? "#{schema}.#{table_name}" : table_name + + connection.select_value(<<~SQL) + SELECT + conname AS constraint_name + FROM pg_catalog.pg_constraint + WHERE pg_constraint.conrelid = '#{table_name}'::regclass + AND pg_constraint.contype = 'p' + SQL + end + + def index_exists_by_name(index, schema: nil) + schema = schema ? "'#{schema}'" : 'current_schema' + + connection.select_value(<<~SQL) + SELECT true + FROM pg_catalog.pg_index i + INNER JOIN pg_catalog.pg_class c + ON c.oid = i.indexrelid + INNER JOIN pg_catalog.pg_namespace n + ON c.relnamespace = n.oid + WHERE c.relname = '#{index}' + AND n.nspname = #{schema} + SQL + end +end diff --git a/spec/support/helpers/test_env.rb b/spec/support/helpers/test_env.rb index 641ed24207e..4c78ca0117c 100644 --- a/spec/support/helpers/test_env.rb +++ b/spec/support/helpers/test_env.rb @@ -517,6 +517,8 @@ module TestEnv return false if component_matches_git_sha?(component_folder, expected_version) + return false if component_ahead_of_target?(component_folder, expected_version) + version = File.read(File.join(component_folder, 'VERSION')).strip # Notice that this will always yield true when using branch versions @@ -527,6 +529,20 @@ module TestEnv true end + def component_ahead_of_target?(component_folder, expected_version) + # The HEAD of the component_folder will be used as heuristic for the version + # of the binaries, allowing to use Git to determine if HEAD is later than + # the expected version. Note: Git considers HEAD to be an anchestor of HEAD. + _out, exit_status = Gitlab::Popen.popen(%W[ + #{Gitlab.config.git.bin_path} + -C #{component_folder} + merge-base --is-ancestor + #{expected_version} HEAD +]) + + exit_status == 0 + end + def component_matches_git_sha?(component_folder, expected_version) # Not a git SHA, so return early return false unless expected_version =~ ::Gitlab::Git::COMMIT_ID diff --git a/spec/support/helpers/usage_data_helpers.rb b/spec/support/helpers/usage_data_helpers.rb index 2592d9f8b42..8e8aeea2ea1 100644 --- a/spec/support/helpers/usage_data_helpers.rb +++ b/spec/support/helpers/usage_data_helpers.rb @@ -98,6 +98,7 @@ module UsageDataHelpers projects_with_repositories_enabled projects_with_error_tracking_enabled projects_with_alerts_service_enabled + projects_with_enabled_alert_integrations projects_with_prometheus_alerts projects_with_tracing_enabled projects_with_expiration_policy_enabled diff --git a/spec/support/helpers/user_login_helper.rb b/spec/support/helpers/user_login_helper.rb index 66606832883..47e858cb68c 100644 --- a/spec/support/helpers/user_login_helper.rb +++ b/spec/support/helpers/user_login_helper.rb @@ -1,18 +1,25 @@ # frozen_string_literal: true module UserLoginHelper - def ensure_tab_pane_correctness(visit_path = true) - if visit_path - visit new_user_session_path - end - - ensure_tab_pane_counts + def ensure_tab_pane_correctness(tab_names) + ensure_tab_pane_counts(tab_names.size) + ensure_tab_labels(tab_names) ensure_one_active_tab ensure_one_active_pane end - def ensure_tab_pane_counts - tabs_count = page.all('[role="tab"]').size + def ensure_no_tabs + expect(page.all('[role="tab"]').size).to eq(0) + end + + def ensure_tab_labels(tab_names) + tab_labels = page.all('[role="tab"]').map(&:text) + + expect(tab_names).to match_array(tab_labels) + end + + def ensure_tab_pane_counts(tabs_count) + expect(page.all('[role="tab"]').size).to eq(tabs_count) expect(page).to have_selector('[role="tabpanel"]', visible: :all, count: tabs_count) end diff --git a/spec/support/helpers/wiki_helpers.rb b/spec/support/helpers/wiki_helpers.rb index 8873a90579d..e276c896da2 100644 --- a/spec/support/helpers/wiki_helpers.rb +++ b/spec/support/helpers/wiki_helpers.rb @@ -4,7 +4,6 @@ module WikiHelpers extend self def stub_group_wikis(enabled) - stub_feature_flags(group_wikis: enabled) stub_licensed_features(group_wikis: enabled) end diff --git a/spec/support/import_export/common_util.rb b/spec/support/import_export/common_util.rb index c0c3559cca0..ae951ea35af 100644 --- a/spec/support/import_export/common_util.rb +++ b/spec/support/import_export/common_util.rb @@ -15,7 +15,7 @@ module ImportExport export_path = [prefix, 'spec', 'fixtures', 'lib', 'gitlab', 'import_export', name].compact export_path = File.join(*export_path) - allow_any_instance_of(Gitlab::ImportExport).to receive(:export_path) { export_path } + allow(Gitlab::ImportExport).to receive(:export_path) { export_path } end def setup_reader(reader) diff --git a/spec/support/matchers/graphql_matchers.rb b/spec/support/matchers/graphql_matchers.rb index 7fa06e25405..8c4ba387a74 100644 --- a/spec/support/matchers/graphql_matchers.rb +++ b/spec/support/matchers/graphql_matchers.rb @@ -109,15 +109,82 @@ RSpec::Matchers.define :have_graphql_arguments do |*expected| end end -RSpec::Matchers.define :have_graphql_type do |expected| - match do |field| - expect(field.type).to eq(expected) +module GraphQLTypeHelpers + def message(object, expected, **opts) + non_null = expected.non_null? || (opts.key?(:null) && !opts[:null]) + + actual = object.type + actual_type = actual.unwrap.graphql_name + actual_type += '!' if actual.non_null? + + expected_type = expected.unwrap.graphql_name + expected_type += '!' if non_null + + "expected #{describe_object(object)} to have GraphQL type #{expected_type}, but got #{actual_type}" + end + + def describe_object(object) + case object + when Types::BaseField + "#{describe_object(object.owner_type)}.#{object.graphql_name}" + when Types::BaseArgument + "#{describe_object(object.owner)}.#{object.graphql_name}" + when Class + object.try(:graphql_name) || object.name + else + object.to_s + end + end + + def nullified(type, can_be_nil) + return type if can_be_nil.nil? # unknown! + return type if can_be_nil + + type.to_non_null_type + end +end + +RSpec::Matchers.define :have_graphql_type do |expected, opts = {}| + include GraphQLTypeHelpers + + match do |object| + expect(object.type).to eq(nullified(expected, opts[:null])) + end + + failure_message do |object| + message(object, expected, **opts) + end +end + +RSpec::Matchers.define :have_nullable_graphql_type do |expected| + include GraphQLTypeHelpers + + match do |object| + expect(object).to have_graphql_type(expected.unwrap, { null: true }) + end + + description do + "have nullable GraphQL type #{expected.graphql_name}" + end + + failure_message do |object| + message(object, expected, null: true) end end RSpec::Matchers.define :have_non_null_graphql_type do |expected| - match do |field| - expect(field.type.to_graphql).to eq(!expected.to_graphql) + include GraphQLTypeHelpers + + match do |object| + expect(object).to have_graphql_type(expected, { null: false }) + end + + description do + "have non-null GraphQL type #{expected.graphql_name}" + end + + failure_message do |object| + message(object, expected, null: false) end end diff --git a/spec/support/patches/rspec_mocks_prepended_methods.rb b/spec/support/patches/rspec_mocks_prepended_methods.rb new file mode 100644 index 00000000000..fa3a74c670c --- /dev/null +++ b/spec/support/patches/rspec_mocks_prepended_methods.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +# This patch allows stubbing of prepended methods +# Based on https://github.com/rspec/rspec-mocks/pull/1218 + +module RSpec + module Mocks + module InstanceMethodStasherForPrependedMethods + private + + def method_owned_by_klass? + owner = @klass.instance_method(@method).owner + owner = owner.class unless Module === owner + + owner == @klass || + # When `extend self` is used, and not under any instance of + (owner.singleton_class == @klass && !Mocks.space.any_instance_recorder_for(owner, true)) || + !method_defined_on_klass?(owner) + end + end + end +end + +module RSpec + module Mocks + module MethodDoubleForPrependedMethods + def restore_original_method + return show_frozen_warning if object_singleton_class.frozen? + return unless @method_is_proxied + + remove_method_from_definition_target + + if @method_stasher.method_is_stashed? + @method_stasher.restore + restore_original_visibility + end + + @method_is_proxied = false + end + + def restore_original_visibility + method_owner.__send__(@original_visibility, @method_name) + end + + private + + def method_owner + @method_owner ||= Object.instance_method(:method).bind(object).call(@method_name).owner + end + end + end +end + +RSpec::Mocks::InstanceMethodStasher.prepend(RSpec::Mocks::InstanceMethodStasherForPrependedMethods) +RSpec::Mocks::MethodDouble.prepend(RSpec::Mocks::MethodDoubleForPrependedMethods) diff --git a/spec/support/rspec.rb b/spec/support/rspec.rb index 861b57c9efa..32f738faa9b 100644 --- a/spec/support/rspec.rb +++ b/spec/support/rspec.rb @@ -5,6 +5,13 @@ require_relative "helpers/stub_metrics" require_relative "helpers/stub_object_storage" require_relative "helpers/stub_env" require_relative "helpers/fast_rails_root" + +# so we need to load rubocop here due to the rubocop support file loading cop_helper +# which monkey patches class Cop +# if cop helper is loaded before rubocop (where class Cop is defined as class Cop < Base) +# we get a `superclass mismatch for class Cop` error when running a rspec for a locally defined +# rubocop cop - therefore we need rubocop required first since it had an inheritance added to the Cop class +require 'rubocop' require 'rubocop/rspec/support' RSpec.configure do |config| diff --git a/spec/support/services/issuable_import_csv_service_shared_examples.rb b/spec/support/services/issuable_import_csv_service_shared_examples.rb new file mode 100644 index 00000000000..20ac2ff5c7c --- /dev/null +++ b/spec/support/services/issuable_import_csv_service_shared_examples.rb @@ -0,0 +1,142 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.shared_examples 'issuable import csv service' do |issuable_type| + let_it_be_with_refind(:project) { create(:project) } + let_it_be(:user) { create(:user) } + + subject { service.execute } + + shared_examples_for 'an issuable importer' do + if issuable_type == 'issue' + it 'records the import attempt if resource is an issue' do + expect { subject } + .to change { Issues::CsvImport.where(project: project, user: user).count } + .by 1 + end + 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 + + describe '#execute' do + context 'invalid file' do + let(:file) { fixture_file_upload('spec/fixtures/banana_sample.gif') } + + it 'returns invalid file error' do + expect(subject[:success]).to eq(0) + expect(subject[:parse_error]).to eq(true) + end + + it_behaves_like 'importer with email notification' + it_behaves_like 'an issuable importer' + end + + context 'file without headers' do + let(:file) { fixture_file_upload('spec/fixtures/csv_no_headers.csv') } + + it 'returns invalid file error' do + expect(subject[:success]).to eq(0) + expect(subject[:parse_error]).to eq(true) + end + + it_behaves_like 'importer with email notification' + it_behaves_like 'an issuable importer' + end + + context 'with a file generated by Gitlab CSV export' do + let(:file) { fixture_file_upload('spec/fixtures/csv_gitlab_export.csv') } + + it 'imports the CSV without errors' do + expect(subject[:success]).to eq(4) + expect(subject[:error_lines]).to eq([]) + expect(subject[:parse_error]).to eq(false) + end + + it 'correctly sets the issuable attributes' do + expect { subject }.to change { issuables.count }.by 4 + + expect(issuables.reload.last).to have_attributes( + title: 'Test Title', + description: 'Test Description' + ) + end + + it_behaves_like 'importer with email notification' + it_behaves_like 'an issuable importer' + end + + context 'comma delimited file' do + let(:file) { fixture_file_upload('spec/fixtures/csv_comma.csv') } + + it 'imports CSV without errors' do + expect(subject[:success]).to eq(3) + expect(subject[:error_lines]).to eq([]) + expect(subject[:parse_error]).to eq(false) + end + + it 'correctly sets the issuable attributes' do + expect { subject }.to change { issuables.count }.by 3 + + expect(issuables.reload.last).to have_attributes( + title: 'Title with quote"', + description: 'Description' + ) + end + + it_behaves_like 'importer with email notification' + it_behaves_like 'an issuable importer' + end + + context 'tab delimited file with error row' do + let(:file) { fixture_file_upload('spec/fixtures/csv_tab.csv') } + + it 'imports CSV with some error rows' do + expect(subject[:success]).to eq(2) + expect(subject[:error_lines]).to eq([3]) + expect(subject[:parse_error]).to eq(false) + end + + it 'correctly sets the issuable attributes' do + expect { subject }.to change { issuables.count }.by 2 + + expect(issuables.reload.last).to have_attributes( + title: 'Hello', + description: 'World' + ) + end + + it_behaves_like 'importer with email notification' + it_behaves_like 'an issuable importer' + end + + context 'semicolon delimited file with CRLF' do + let(:file) { fixture_file_upload('spec/fixtures/csv_semicolon.csv') } + + it 'imports CSV with a blank row' do + expect(subject[:success]).to eq(3) + expect(subject[:error_lines]).to eq([4]) + expect(subject[:parse_error]).to eq(false) + end + + it 'correctly sets the issuable attributes' do + expect { subject }.to change { issuables.count }.by 3 + + expect(issuables.reload.last).to have_attributes( + title: 'Hello', + description: 'World' + ) + end + + it_behaves_like 'importer with email notification' + it_behaves_like 'an issuable importer' + end + end +end diff --git a/spec/support/shared_contexts/design_management_shared_contexts.rb b/spec/support/shared_contexts/design_management_shared_contexts.rb index 3ff6a521338..e6ae7e03664 100644 --- a/spec/support/shared_contexts/design_management_shared_contexts.rb +++ b/spec/support/shared_contexts/design_management_shared_contexts.rb @@ -18,12 +18,14 @@ RSpec.shared_context 'four designs in three versions' do modified_designs: [], deleted_designs: []) end + let_it_be(:second_version) do create(:design_version, issue: issue, created_designs: [design_b, design_c, design_d], modified_designs: [design_a], deleted_designs: []) end + let_it_be(:third_version) do create(:design_version, issue: issue, created_designs: [], diff --git a/spec/support/shared_contexts/finders/group_projects_finder_shared_contexts.rb b/spec/support/shared_contexts/finders/group_projects_finder_shared_contexts.rb index 2b6edb4c07d..68ff16922d8 100644 --- a/spec/support/shared_contexts/finders/group_projects_finder_shared_contexts.rb +++ b/spec/support/shared_contexts/finders/group_projects_finder_shared_contexts.rb @@ -1,21 +1,21 @@ # frozen_string_literal: true RSpec.shared_context 'GroupProjectsFinder context' do - let(:group) { create(:group) } - let(:subgroup) { create(:group, parent: group) } - let(:current_user) { create(:user) } + let_it_be(:group) { create(:group) } + let_it_be(:subgroup) { create(:group, parent: group) } + let_it_be(:current_user) { create(:user) } let(:params) { {} } let(:options) { {} } let(:finder) { described_class.new(group: group, current_user: current_user, params: params, options: options) } - let!(:public_project) { create(:project, :public, group: group, path: '1') } - let!(:private_project) { create(:project, :private, group: group, path: '2') } - let!(:shared_project_1) { create(:project, :public, path: '3') } - let!(:shared_project_2) { create(:project, :private, path: '4') } - let!(:shared_project_3) { create(:project, :internal, path: '5') } - let!(:subgroup_project) { create(:project, :public, path: '6', group: subgroup) } - let!(:subgroup_private_project) { create(:project, :private, path: '7', group: subgroup) } + let_it_be(:public_project) { create(:project, :public, group: group, path: '1') } + let_it_be(:private_project) { create(:project, :private, group: group, path: '2') } + let_it_be(:shared_project_1) { create(:project, :public, path: '3') } + let_it_be(:shared_project_2) { create(:project, :private, path: '4') } + let_it_be(:shared_project_3) { create(:project, :internal, path: '5') } + let_it_be(:subgroup_project) { create(:project, :public, path: '6', group: subgroup) } + let_it_be(:subgroup_private_project) { create(:project, :private, path: '7', group: subgroup) } before do shared_project_1.project_group_links.create!(group_access: Gitlab::Access::MAINTAINER, group: group) diff --git a/spec/support/shared_contexts/finders/merge_requests_finder_shared_contexts.rb b/spec/support/shared_contexts/finders/merge_requests_finder_shared_contexts.rb index 010c445d8df..88c31bf9cfd 100644 --- a/spec/support/shared_contexts/finders/merge_requests_finder_shared_contexts.rb +++ b/spec/support/shared_contexts/finders/merge_requests_finder_shared_contexts.rb @@ -23,6 +23,7 @@ RSpec.shared_context 'MergeRequestsFinder multiple projects with merge requests # We cannot use `let_it_be` here otherwise we get: # Failure/Error: allow(RepositoryForkWorker).to receive(:perform_async).and_return(true) # The use of doubles or partial doubles from rspec-mocks outside of the per-test lifecycle is not supported. + let!(:project2) do allow_gitaly_n_plus_1 do fork_project(project1, user) @@ -40,9 +41,11 @@ RSpec.shared_context 'MergeRequestsFinder multiple projects with merge requests let_it_be(:project4, reload: true) do allow_gitaly_n_plus_1 { create(:project, :repository, group: subgroup) } end + let_it_be(:project5, reload: true) do allow_gitaly_n_plus_1 { create(:project, group: subgroup) } end + let_it_be(:project6, reload: true) do allow_gitaly_n_plus_1 { create(:project, group: subgroup) } end diff --git a/spec/support/shared_contexts/lib/gitlab/middleware/with_a_mocked_gitlab_instance_shared_context.rb b/spec/support/shared_contexts/lib/gitlab/middleware/with_a_mocked_gitlab_instance_shared_context.rb new file mode 100644 index 00000000000..3830b89f1ff --- /dev/null +++ b/spec/support/shared_contexts/lib/gitlab/middleware/with_a_mocked_gitlab_instance_shared_context.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +RSpec.shared_context 'with a mocked GitLab instance' do + let(:rack_stack) do + rack = Rack::Builder.new do + use ActionDispatch::Session::CacheStore + use ActionDispatch::Flash + end + + rack.run(subject) + rack.to_app + end + + let(:observe_env) do + Module.new do + attr_reader :env + + def call(env) + @env = env + super + end + end + end + + let(:request) { Rack::MockRequest.new(rack_stack) } + + subject do + described_class.new(fake_app).tap do |app| + app.extend(observe_env) + end + end +end diff --git a/spec/support/shared_contexts/navbar_structure_context.rb b/spec/support/shared_contexts/navbar_structure_context.rb index 9ebfdcb9522..ed74c3f179f 100644 --- a/spec/support/shared_contexts/navbar_structure_context.rb +++ b/spec/support/shared_contexts/navbar_structure_context.rb @@ -56,6 +56,7 @@ RSpec.shared_context 'project navbar structure' do nav_item: _('CI / CD'), nav_sub_items: [ _('Pipelines'), + s_('Pipelines|Editor'), _('Jobs'), _('Artifacts'), _('Schedules') @@ -71,6 +72,7 @@ RSpec.shared_context 'project navbar structure' do _('Alerts'), _('Incidents'), _('Serverless'), + _('Terraform'), _('Kubernetes'), _('Environments'), _('Feature Flags'), diff --git a/spec/support/shared_contexts/policies/project_policy_table_shared_context.rb b/spec/support/shared_contexts/policies/project_policy_table_shared_context.rb index efd82ecb15a..8c9a60fa703 100644 --- a/spec/support/shared_contexts/policies/project_policy_table_shared_context.rb +++ b/spec/support/shared_contexts/policies/project_policy_table_shared_context.rb @@ -3,6 +3,8 @@ RSpec.shared_context 'ProjectPolicyTable context' do using RSpec::Parameterized::TableSyntax + include AdminModeHelper + let(:pendings) { {} } let(:pending?) do pendings.include?( @@ -10,106 +12,117 @@ RSpec.shared_context 'ProjectPolicyTable context' do project_level: project_level, feature_access_level: feature_access_level, membership: membership, + admin_mode: admin_mode, expected_count: expected_count } ) end # rubocop:disable Metrics/AbcSize - # project_level, :feature_access_level, :membership, :expected_count + # project_level, :feature_access_level, :membership, :admin_mode, :expected_count def permission_table_for_reporter_feature_access - :public | :enabled | :admin | 1 - :public | :enabled | :reporter | 1 - :public | :enabled | :guest | 1 - :public | :enabled | :non_member | 1 - :public | :enabled | :anonymous | 1 - - :public | :private | :admin | 1 - :public | :private | :reporter | 1 - :public | :private | :guest | 0 - :public | :private | :non_member | 0 - :public | :private | :anonymous | 0 - - :public | :disabled | :reporter | 0 - :public | :disabled | :guest | 0 - :public | :disabled | :non_member | 0 - :public | :disabled | :anonymous | 0 - - :internal | :enabled | :admin | 1 - :internal | :enabled | :reporter | 1 - :internal | :enabled | :guest | 1 - :internal | :enabled | :non_member | 1 - :internal | :enabled | :anonymous | 0 - - :internal | :private | :admin | 1 - :internal | :private | :reporter | 1 - :internal | :private | :guest | 0 - :internal | :private | :non_member | 0 - :internal | :private | :anonymous | 0 - - :internal | :disabled | :reporter | 0 - :internal | :disabled | :guest | 0 - :internal | :disabled | :non_member | 0 - :internal | :disabled | :anonymous | 0 - - :private | :private | :admin | 1 - :private | :private | :reporter | 1 - :private | :private | :guest | 0 - :private | :private | :non_member | 0 - :private | :private | :anonymous | 0 - - :private | :disabled | :reporter | 0 - :private | :disabled | :guest | 0 - :private | :disabled | :non_member | 0 - :private | :disabled | :anonymous | 0 + :public | :enabled | :admin | true | 1 + :public | :enabled | :admin | false | 1 + :public | :enabled | :reporter | nil | 1 + :public | :enabled | :guest | nil | 1 + :public | :enabled | :non_member | nil | 1 + :public | :enabled | :anonymous | nil | 1 + + :public | :private | :admin | true | 1 + :public | :private | :admin | false | 0 + :public | :private | :reporter | nil | 1 + :public | :private | :guest | nil | 0 + :public | :private | :non_member | nil | 0 + :public | :private | :anonymous | nil | 0 + + :public | :disabled | :reporter | nil | 0 + :public | :disabled | :guest | nil | 0 + :public | :disabled | :non_member | nil | 0 + :public | :disabled | :anonymous | nil | 0 + + :internal | :enabled | :admin | true | 1 + :internal | :enabled | :admin | false | 1 + :internal | :enabled | :reporter | nil | 1 + :internal | :enabled | :guest | nil | 1 + :internal | :enabled | :non_member | nil | 1 + :internal | :enabled | :anonymous | nil | 0 + + :internal | :private | :admin | true | 1 + :internal | :private | :admin | false | 0 + :internal | :private | :reporter | nil | 1 + :internal | :private | :guest | nil | 0 + :internal | :private | :non_member | nil | 0 + :internal | :private | :anonymous | nil | 0 + + :internal | :disabled | :reporter | nil | 0 + :internal | :disabled | :guest | nil | 0 + :internal | :disabled | :non_member | nil | 0 + :internal | :disabled | :anonymous | nil | 0 + + :private | :private | :admin | true | 1 + :private | :private | :admin | false | 0 + :private | :private | :reporter | nil | 1 + :private | :private | :guest | nil | 0 + :private | :private | :non_member | nil | 0 + :private | :private | :anonymous | nil | 0 + + :private | :disabled | :reporter | nil | 0 + :private | :disabled | :guest | nil | 0 + :private | :disabled | :non_member | nil | 0 + :private | :disabled | :anonymous | nil | 0 end - # project_level, :feature_access_level, :membership, :expected_count + # project_level, :feature_access_level, :membership, :admin_mode, :expected_count def permission_table_for_guest_feature_access - :public | :enabled | :admin | 1 - :public | :enabled | :reporter | 1 - :public | :enabled | :guest | 1 - :public | :enabled | :non_member | 1 - :public | :enabled | :anonymous | 1 - - :public | :private | :admin | 1 - :public | :private | :reporter | 1 - :public | :private | :guest | 1 - :public | :private | :non_member | 0 - :public | :private | :anonymous | 0 - - :public | :disabled | :reporter | 0 - :public | :disabled | :guest | 0 - :public | :disabled | :non_member | 0 - :public | :disabled | :anonymous | 0 - - :internal | :enabled | :admin | 1 - :internal | :enabled | :reporter | 1 - :internal | :enabled | :guest | 1 - :internal | :enabled | :non_member | 1 - :internal | :enabled | :anonymous | 0 - - :internal | :private | :admin | 1 - :internal | :private | :reporter | 1 - :internal | :private | :guest | 1 - :internal | :private | :non_member | 0 - :internal | :private | :anonymous | 0 - - :internal | :disabled | :reporter | 0 - :internal | :disabled | :guest | 0 - :internal | :disabled | :non_member | 0 - :internal | :disabled | :anonymous | 0 - - :private | :private | :admin | 1 - :private | :private | :reporter | 1 - :private | :private | :guest | 1 - :private | :private | :non_member | 0 - :private | :private | :anonymous | 0 - - :private | :disabled | :reporter | 0 - :private | :disabled | :guest | 0 - :private | :disabled | :non_member | 0 - :private | :disabled | :anonymous | 0 + :public | :enabled | :admin | true | 1 + :public | :enabled | :admin | false | 1 + :public | :enabled | :reporter | nil | 1 + :public | :enabled | :guest | nil | 1 + :public | :enabled | :non_member | nil | 1 + :public | :enabled | :anonymous | nil | 1 + + :public | :private | :admin | true | 1 + :public | :private | :admin | false | 0 + :public | :private | :reporter | nil | 1 + :public | :private | :guest | nil | 1 + :public | :private | :non_member | nil | 0 + :public | :private | :anonymous | nil | 0 + + :public | :disabled | :reporter | nil | 0 + :public | :disabled | :guest | nil | 0 + :public | :disabled | :non_member | nil | 0 + :public | :disabled | :anonymous | nil | 0 + + :internal | :enabled | :admin | true | 1 + :internal | :enabled | :admin | false | 1 + :internal | :enabled | :reporter | nil | 1 + :internal | :enabled | :guest | nil | 1 + :internal | :enabled | :non_member | nil | 1 + :internal | :enabled | :anonymous | nil | 0 + + :internal | :private | :admin | true | 1 + :internal | :private | :admin | false | 0 + :internal | :private | :reporter | nil | 1 + :internal | :private | :guest | nil | 1 + :internal | :private | :non_member | nil | 0 + :internal | :private | :anonymous | nil | 0 + + :internal | :disabled | :reporter | nil | 0 + :internal | :disabled | :guest | nil | 0 + :internal | :disabled | :non_member | nil | 0 + :internal | :disabled | :anonymous | nil | 0 + + :private | :private | :admin | true | 1 + :private | :private | :admin | false | 0 + :private | :private | :reporter | nil | 1 + :private | :private | :guest | nil | 1 + :private | :private | :non_member | nil | 0 + :private | :private | :anonymous | nil | 0 + + :private | :disabled | :reporter | nil | 0 + :private | :disabled | :guest | nil | 0 + :private | :disabled | :non_member | nil | 0 + :private | :disabled | :anonymous | nil | 0 end # This table is based on permission_table_for_guest_feature_access, @@ -121,184 +134,208 @@ RSpec.shared_context 'ProjectPolicyTable context' do # e.g. `repository` feature has minimum requirement of GUEST, # but a GUEST are prohibited from reading code if project is private. # - # project_level, :feature_access_level, :membership, :expected_count + # project_level, :feature_access_level, :membership, :admin_mode, :expected_count def permission_table_for_guest_feature_access_and_non_private_project_only - :public | :enabled | :admin | 1 - :public | :enabled | :reporter | 1 - :public | :enabled | :guest | 1 - :public | :enabled | :non_member | 1 - :public | :enabled | :anonymous | 1 - - :public | :private | :admin | 1 - :public | :private | :reporter | 1 - :public | :private | :guest | 1 - :public | :private | :non_member | 0 - :public | :private | :anonymous | 0 - - :public | :disabled | :reporter | 0 - :public | :disabled | :guest | 0 - :public | :disabled | :non_member | 0 - :public | :disabled | :anonymous | 0 - - :internal | :enabled | :admin | 1 - :internal | :enabled | :reporter | 1 - :internal | :enabled | :guest | 1 - :internal | :enabled | :non_member | 1 - :internal | :enabled | :anonymous | 0 - - :internal | :private | :admin | 1 - :internal | :private | :reporter | 1 - :internal | :private | :guest | 1 - :internal | :private | :non_member | 0 - :internal | :private | :anonymous | 0 - - :internal | :disabled | :reporter | 0 - :internal | :disabled | :guest | 0 - :internal | :disabled | :non_member | 0 - :internal | :disabled | :anonymous | 0 - - :private | :private | :admin | 1 - :private | :private | :reporter | 1 - :private | :private | :guest | 0 - :private | :private | :non_member | 0 - :private | :private | :anonymous | 0 - - :private | :disabled | :reporter | 0 - :private | :disabled | :guest | 0 - :private | :disabled | :non_member | 0 - :private | :disabled | :anonymous | 0 + :public | :enabled | :admin | true | 1 + :public | :enabled | :admin | false | 1 + :public | :enabled | :reporter | nil | 1 + :public | :enabled | :guest | nil | 1 + :public | :enabled | :non_member | nil | 1 + :public | :enabled | :anonymous | nil | 1 + + :public | :private | :admin | true | 1 + :public | :private | :admin | false | 0 + :public | :private | :reporter | nil | 1 + :public | :private | :guest | nil | 1 + :public | :private | :non_member | nil | 0 + :public | :private | :anonymous | nil | 0 + + :public | :disabled | :reporter | nil | 0 + :public | :disabled | :guest | nil | 0 + :public | :disabled | :non_member | nil | 0 + :public | :disabled | :anonymous | nil | 0 + + :internal | :enabled | :admin | true | 1 + :internal | :enabled | :admin | false | 1 + :internal | :enabled | :reporter | nil | 1 + :internal | :enabled | :guest | nil | 1 + :internal | :enabled | :non_member | nil | 1 + :internal | :enabled | :anonymous | nil | 0 + + :internal | :private | :admin | true | 1 + :internal | :private | :admin | false | 0 + :internal | :private | :reporter | nil | 1 + :internal | :private | :guest | nil | 1 + :internal | :private | :non_member | nil | 0 + :internal | :private | :anonymous | nil | 0 + + :internal | :disabled | :reporter | nil | 0 + :internal | :disabled | :guest | nil | 0 + :internal | :disabled | :non_member | nil | 0 + :internal | :disabled | :anonymous | nil | 0 + + :private | :private | :admin | true | 1 + :private | :private | :admin | false | 0 + :private | :private | :reporter | nil | 1 + :private | :private | :guest | nil | 0 + :private | :private | :non_member | nil | 0 + :private | :private | :anonymous | nil | 0 + + :private | :disabled | :reporter | nil | 0 + :private | :disabled | :guest | nil | 0 + :private | :disabled | :non_member | nil | 0 + :private | :disabled | :anonymous | nil | 0 end - # :project_level, :issues_access_level, :merge_requests_access_level, :membership, :expected_count + # :project_level, :issues_access_level, :merge_requests_access_level, :membership, :admin_mode, :expected_count def permission_table_for_milestone_access - :public | :enabled | :enabled | :admin | 1 - :public | :enabled | :enabled | :reporter | 1 - :public | :enabled | :enabled | :guest | 1 - :public | :enabled | :enabled | :non_member | 1 - :public | :enabled | :enabled | :anonymous | 1 - - :public | :enabled | :private | :admin | 1 - :public | :enabled | :private | :reporter | 1 - :public | :enabled | :private | :guest | 1 - :public | :enabled | :private | :non_member | 1 - :public | :enabled | :private | :anonymous | 1 - - :public | :enabled | :disabled | :admin | 1 - :public | :enabled | :disabled | :reporter | 1 - :public | :enabled | :disabled | :guest | 1 - :public | :enabled | :disabled | :non_member | 1 - :public | :enabled | :disabled | :anonymous | 1 - - :public | :private | :enabled | :admin | 1 - :public | :private | :enabled | :reporter | 1 - :public | :private | :enabled | :guest | 1 - :public | :private | :enabled | :non_member | 1 - :public | :private | :enabled | :anonymous | 1 - - :public | :private | :private | :admin | 1 - :public | :private | :private | :reporter | 1 - :public | :private | :private | :guest | 1 - :public | :private | :private | :non_member | 0 - :public | :private | :private | :anonymous | 0 - - :public | :private | :disabled | :admin | 1 - :public | :private | :disabled | :reporter | 1 - :public | :private | :disabled | :guest | 1 - :public | :private | :disabled | :non_member | 0 - :public | :private | :disabled | :anonymous | 0 - - :public | :disabled | :enabled | :admin | 1 - :public | :disabled | :enabled | :reporter | 1 - :public | :disabled | :enabled | :guest | 1 - :public | :disabled | :enabled | :non_member | 1 - :public | :disabled | :enabled | :anonymous | 1 - - :public | :disabled | :private | :admin | 1 - :public | :disabled | :private | :reporter | 1 - :public | :disabled | :private | :guest | 0 - :public | :disabled | :private | :non_member | 0 - :public | :disabled | :private | :anonymous | 0 - - :public | :disabled | :disabled | :reporter | 0 - :public | :disabled | :disabled | :guest | 0 - :public | :disabled | :disabled | :non_member | 0 - :public | :disabled | :disabled | :anonymous | 0 - - :internal | :enabled | :enabled | :admin | 1 - :internal | :enabled | :enabled | :reporter | 1 - :internal | :enabled | :enabled | :guest | 1 - :internal | :enabled | :enabled | :non_member | 1 - :internal | :enabled | :enabled | :anonymous | 0 - - :internal | :enabled | :private | :admin | 1 - :internal | :enabled | :private | :reporter | 1 - :internal | :enabled | :private | :guest | 1 - :internal | :enabled | :private | :non_member | 1 - :internal | :enabled | :private | :anonymous | 0 - - :internal | :enabled | :disabled | :admin | 1 - :internal | :enabled | :disabled | :reporter | 1 - :internal | :enabled | :disabled | :guest | 1 - :internal | :enabled | :disabled | :non_member | 1 - :internal | :enabled | :disabled | :anonymous | 0 - - :internal | :private | :enabled | :admin | 1 - :internal | :private | :enabled | :reporter | 1 - :internal | :private | :enabled | :guest | 1 - :internal | :private | :enabled | :non_member | 1 - :internal | :private | :enabled | :anonymous | 0 - - :internal | :private | :private | :admin | 1 - :internal | :private | :private | :reporter | 1 - :internal | :private | :private | :guest | 1 - :internal | :private | :private | :non_member | 0 - :internal | :private | :private | :anonymous | 0 - - :internal | :private | :disabled | :admin | 1 - :internal | :private | :disabled | :reporter | 1 - :internal | :private | :disabled | :guest | 1 - :internal | :private | :disabled | :non_member | 0 - :internal | :private | :disabled | :anonymous | 0 - - :internal | :disabled | :enabled | :admin | 1 - :internal | :disabled | :enabled | :reporter | 1 - :internal | :disabled | :enabled | :guest | 1 - :internal | :disabled | :enabled | :non_member | 1 - :internal | :disabled | :enabled | :anonymous | 0 - - :internal | :disabled | :private | :admin | 1 - :internal | :disabled | :private | :reporter | 1 - :internal | :disabled | :private | :guest | 0 - :internal | :disabled | :private | :non_member | 0 - :internal | :disabled | :private | :anonymous | 0 - - :internal | :disabled | :disabled | :reporter | 0 - :internal | :disabled | :disabled | :guest | 0 - :internal | :disabled | :disabled | :non_member | 0 - :internal | :disabled | :disabled | :anonymous | 0 - - :private | :private | :private | :admin | 1 - :private | :private | :private | :reporter | 1 - :private | :private | :private | :guest | 1 - :private | :private | :private | :non_member | 0 - :private | :private | :private | :anonymous | 0 - - :private | :private | :disabled | :admin | 1 - :private | :private | :disabled | :reporter | 1 - :private | :private | :disabled | :guest | 1 - :private | :private | :disabled | :non_member | 0 - :private | :private | :disabled | :anonymous | 0 - - :private | :disabled | :private | :admin | 1 - :private | :disabled | :private | :reporter | 1 - :private | :disabled | :private | :guest | 0 - :private | :disabled | :private | :non_member | 0 - :private | :disabled | :private | :anonymous | 0 - - :private | :disabled | :disabled | :reporter | 0 - :private | :disabled | :disabled | :guest | 0 - :private | :disabled | :disabled | :non_member | 0 - :private | :disabled | :disabled | :anonymous | 0 + :public | :enabled | :enabled | :admin | true | 1 + :public | :enabled | :enabled | :admin | false | 1 + :public | :enabled | :enabled | :reporter | nil | 1 + :public | :enabled | :enabled | :guest | nil | 1 + :public | :enabled | :enabled | :non_member | nil | 1 + :public | :enabled | :enabled | :anonymous | nil | 1 + + :public | :enabled | :private | :admin | true | 1 + :public | :enabled | :private | :admin | false | 1 + :public | :enabled | :private | :reporter | nil | 1 + :public | :enabled | :private | :guest | nil | 1 + :public | :enabled | :private | :non_member | nil | 1 + :public | :enabled | :private | :anonymous | nil | 1 + + :public | :enabled | :disabled | :admin | true | 1 + :public | :enabled | :disabled | :admin | false | 1 + :public | :enabled | :disabled | :reporter | nil | 1 + :public | :enabled | :disabled | :guest | nil | 1 + :public | :enabled | :disabled | :non_member | nil | 1 + :public | :enabled | :disabled | :anonymous | nil | 1 + + :public | :private | :enabled | :admin | true | 1 + :public | :private | :enabled | :admin | false | 1 + :public | :private | :enabled | :reporter | nil | 1 + :public | :private | :enabled | :guest | nil | 1 + :public | :private | :enabled | :non_member | nil | 1 + :public | :private | :enabled | :anonymous | nil | 1 + + :public | :private | :private | :admin | true | 1 + :public | :private | :private | :admin | false | 0 + :public | :private | :private | :reporter | nil | 1 + :public | :private | :private | :guest | nil | 1 + :public | :private | :private | :non_member | nil | 0 + :public | :private | :private | :anonymous | nil | 0 + + :public | :private | :disabled | :admin | true | 1 + :public | :private | :disabled | :admin | false | 0 + :public | :private | :disabled | :reporter | nil | 1 + :public | :private | :disabled | :guest | nil | 1 + :public | :private | :disabled | :non_member | nil | 0 + :public | :private | :disabled | :anonymous | nil | 0 + + :public | :disabled | :enabled | :admin | true | 1 + :public | :disabled | :enabled | :admin | false | 1 + :public | :disabled | :enabled | :reporter | nil | 1 + :public | :disabled | :enabled | :guest | nil | 1 + :public | :disabled | :enabled | :non_member | nil | 1 + :public | :disabled | :enabled | :anonymous | nil | 1 + + :public | :disabled | :private | :admin | true | 1 + :public | :disabled | :private | :admin | false | 0 + :public | :disabled | :private | :reporter | nil | 1 + :public | :disabled | :private | :guest | nil | 0 + :public | :disabled | :private | :non_member | nil | 0 + :public | :disabled | :private | :anonymous | nil | 0 + + :public | :disabled | :disabled | :reporter | nil | 0 + :public | :disabled | :disabled | :guest | nil | 0 + :public | :disabled | :disabled | :non_member | nil | 0 + :public | :disabled | :disabled | :anonymous | nil | 0 + + :internal | :enabled | :enabled | :admin | true | 1 + :internal | :enabled | :enabled | :admin | false | 1 + :internal | :enabled | :enabled | :reporter | nil | 1 + :internal | :enabled | :enabled | :guest | nil | 1 + :internal | :enabled | :enabled | :non_member | nil | 1 + :internal | :enabled | :enabled | :anonymous | nil | 0 + + :internal | :enabled | :private | :admin | true | 1 + :internal | :enabled | :private | :admin | false | 1 + :internal | :enabled | :private | :reporter | nil | 1 + :internal | :enabled | :private | :guest | nil | 1 + :internal | :enabled | :private | :non_member | nil | 1 + :internal | :enabled | :private | :anonymous | nil | 0 + + :internal | :enabled | :disabled | :admin | true | 1 + :internal | :enabled | :disabled | :admin | false | 1 + :internal | :enabled | :disabled | :reporter | nil | 1 + :internal | :enabled | :disabled | :guest | nil | 1 + :internal | :enabled | :disabled | :non_member | nil | 1 + :internal | :enabled | :disabled | :anonymous | nil | 0 + + :internal | :private | :enabled | :admin | true | 1 + :internal | :private | :enabled | :admin | false | 1 + :internal | :private | :enabled | :reporter | nil | 1 + :internal | :private | :enabled | :guest | nil | 1 + :internal | :private | :enabled | :non_member | nil | 1 + :internal | :private | :enabled | :anonymous | nil | 0 + + :internal | :private | :private | :admin | true | 1 + :internal | :private | :private | :admin | false | 0 + :internal | :private | :private | :reporter | nil | 1 + :internal | :private | :private | :guest | nil | 1 + :internal | :private | :private | :non_member | nil | 0 + :internal | :private | :private | :anonymous | nil | 0 + + :internal | :private | :disabled | :admin | true | 1 + :internal | :private | :disabled | :admin | false | 0 + :internal | :private | :disabled | :reporter | nil | 1 + :internal | :private | :disabled | :guest | nil | 1 + :internal | :private | :disabled | :non_member | nil | 0 + :internal | :private | :disabled | :anonymous | nil | 0 + + :internal | :disabled | :enabled | :admin | true | 1 + :internal | :disabled | :enabled | :admin | false | 1 + :internal | :disabled | :enabled | :reporter | nil | 1 + :internal | :disabled | :enabled | :guest | nil | 1 + :internal | :disabled | :enabled | :non_member | nil | 1 + :internal | :disabled | :enabled | :anonymous | nil | 0 + + :internal | :disabled | :private | :admin | true | 1 + :internal | :disabled | :private | :admin | false | 0 + :internal | :disabled | :private | :reporter | nil | 1 + :internal | :disabled | :private | :guest | nil | 0 + :internal | :disabled | :private | :non_member | nil | 0 + :internal | :disabled | :private | :anonymous | nil | 0 + + :internal | :disabled | :disabled | :reporter | nil | 0 + :internal | :disabled | :disabled | :guest | nil | 0 + :internal | :disabled | :disabled | :non_member | nil | 0 + :internal | :disabled | :disabled | :anonymous | nil | 0 + + :private | :private | :private | :admin | true | 1 + :private | :private | :private | :admin | false | 0 + :private | :private | :private | :reporter | nil | 1 + :private | :private | :private | :guest | nil | 1 + :private | :private | :private | :non_member | nil | 0 + :private | :private | :private | :anonymous | nil | 0 + + :private | :private | :disabled | :admin | true | 1 + :private | :private | :disabled | :admin | false | 0 + :private | :private | :disabled | :reporter | nil | 1 + :private | :private | :disabled | :guest | nil | 1 + :private | :private | :disabled | :non_member | nil | 0 + :private | :private | :disabled | :anonymous | nil | 0 + + :private | :disabled | :private | :admin | true | 1 + :private | :disabled | :private | :admin | false | 0 + :private | :disabled | :private | :reporter | nil | 1 + :private | :disabled | :private | :guest | nil | 0 + :private | :disabled | :private | :non_member | nil | 0 + :private | :disabled | :private | :anonymous | nil | 0 + + :private | :disabled | :disabled | :reporter | nil | 0 + :private | :disabled | :disabled | :guest | nil | 0 + :private | :disabled | :disabled | :non_member | nil | 0 + :private | :disabled | :disabled | :anonymous | nil | 0 end # :project_level, :membership, :expected_count @@ -321,166 +358,192 @@ RSpec.shared_context 'ProjectPolicyTable context' do # :snippet_level, :project_level, :feature_access_level, :membership, :expected_count def permission_table_for_project_snippet_access - :public | :public | :enabled | :admin | 1 - :public | :public | :enabled | :reporter | 1 - :public | :public | :enabled | :guest | 1 - :public | :public | :enabled | :non_member | 1 - :public | :public | :enabled | :anonymous | 1 - - :public | :public | :private | :admin | 1 - :public | :public | :private | :reporter | 1 - :public | :public | :private | :guest | 1 - :public | :public | :private | :non_member | 0 - :public | :public | :private | :anonymous | 0 - - :public | :public | :disabled | :admin | 1 - :public | :public | :disabled | :reporter | 0 - :public | :public | :disabled | :guest | 0 - :public | :public | :disabled | :non_member | 0 - :public | :public | :disabled | :anonymous | 0 - - :public | :internal | :enabled | :admin | 1 - :public | :internal | :enabled | :reporter | 1 - :public | :internal | :enabled | :guest | 1 - :public | :internal | :enabled | :non_member | 1 - :public | :internal | :enabled | :anonymous | 0 - - :public | :internal | :private | :admin | 1 - :public | :internal | :private | :reporter | 1 - :public | :internal | :private | :guest | 1 - :public | :internal | :private | :non_member | 0 - :public | :internal | :private | :anonymous | 0 - - :public | :internal | :disabled | :admin | 1 - :public | :internal | :disabled | :reporter | 0 - :public | :internal | :disabled | :guest | 0 - :public | :internal | :disabled | :non_member | 0 - :public | :internal | :disabled | :anonymous | 0 - - :public | :private | :private | :admin | 1 - :public | :private | :private | :reporter | 1 - :public | :private | :private | :guest | 1 - :public | :private | :private | :non_member | 0 - :public | :private | :private | :anonymous | 0 - - :public | :private | :disabled | :reporter | 0 - :public | :private | :disabled | :guest | 0 - :public | :private | :disabled | :non_member | 0 - :public | :private | :disabled | :anonymous | 0 - - :internal | :public | :enabled | :admin | 1 - :internal | :public | :enabled | :reporter | 1 - :internal | :public | :enabled | :guest | 1 - :internal | :public | :enabled | :non_member | 1 - :internal | :public | :enabled | :anonymous | 0 - - :internal | :public | :private | :admin | 1 - :internal | :public | :private | :reporter | 1 - :internal | :public | :private | :guest | 1 - :internal | :public | :private | :non_member | 0 - :internal | :public | :private | :anonymous | 0 - - :internal | :public | :disabled | :admin | 1 - :internal | :public | :disabled | :reporter | 0 - :internal | :public | :disabled | :guest | 0 - :internal | :public | :disabled | :non_member | 0 - :internal | :public | :disabled | :anonymous | 0 - - :internal | :internal | :enabled | :admin | 1 - :internal | :internal | :enabled | :reporter | 1 - :internal | :internal | :enabled | :guest | 1 - :internal | :internal | :enabled | :non_member | 1 - :internal | :internal | :enabled | :anonymous | 0 - - :internal | :internal | :private | :admin | 1 - :internal | :internal | :private | :reporter | 1 - :internal | :internal | :private | :guest | 1 - :internal | :internal | :private | :non_member | 0 - :internal | :internal | :private | :anonymous | 0 - - :internal | :internal | :disabled | :admin | 1 - :internal | :internal | :disabled | :reporter | 0 - :internal | :internal | :disabled | :guest | 0 - :internal | :internal | :disabled | :non_member | 0 - :internal | :internal | :disabled | :anonymous | 0 - - :internal | :private | :private | :admin | 1 - :internal | :private | :private | :reporter | 1 - :internal | :private | :private | :guest | 1 - :internal | :private | :private | :non_member | 0 - :internal | :private | :private | :anonymous | 0 - - :internal | :private | :disabled | :admin | 1 - :internal | :private | :disabled | :reporter | 0 - :internal | :private | :disabled | :guest | 0 - :internal | :private | :disabled | :non_member | 0 - :internal | :private | :disabled | :anonymous | 0 - - :private | :public | :enabled | :admin | 1 - :private | :public | :enabled | :reporter | 1 - :private | :public | :enabled | :guest | 1 - :private | :public | :enabled | :non_member | 0 - :private | :public | :enabled | :anonymous | 0 - - :private | :public | :private | :admin | 1 - :private | :public | :private | :reporter | 1 - :private | :public | :private | :guest | 1 - :private | :public | :private | :non_member | 0 - :private | :public | :private | :anonymous | 0 - - :private | :public | :disabled | :admin | 1 - :private | :public | :disabled | :reporter | 0 - :private | :public | :disabled | :guest | 0 - :private | :public | :disabled | :non_member | 0 - :private | :public | :disabled | :anonymous | 0 - - :private | :internal | :enabled | :admin | 1 - :private | :internal | :enabled | :reporter | 1 - :private | :internal | :enabled | :guest | 1 - :private | :internal | :enabled | :non_member | 0 - :private | :internal | :enabled | :anonymous | 0 - - :private | :internal | :private | :admin | 1 - :private | :internal | :private | :reporter | 1 - :private | :internal | :private | :guest | 1 - :private | :internal | :private | :non_member | 0 - :private | :internal | :private | :anonymous | 0 - - :private | :internal | :disabled | :admin | 1 - :private | :internal | :disabled | :reporter | 0 - :private | :internal | :disabled | :guest | 0 - :private | :internal | :disabled | :non_member | 0 - :private | :internal | :disabled | :anonymous | 0 - - :private | :private | :private | :admin | 1 - :private | :private | :private | :reporter | 1 - :private | :private | :private | :guest | 1 - :private | :private | :private | :non_member | 0 - :private | :private | :private | :anonymous | 0 - - :private | :private | :disabled | :admin | 1 - :private | :private | :disabled | :reporter | 0 - :private | :private | :disabled | :guest | 0 - :private | :private | :disabled | :non_member | 0 - :private | :private | :disabled | :anonymous | 0 + :public | :public | :enabled | :admin | true | 1 + :public | :public | :enabled | :admin | false | 1 + :public | :public | :enabled | :reporter | nil | 1 + :public | :public | :enabled | :guest | nil | 1 + :public | :public | :enabled | :non_member | nil | 1 + :public | :public | :enabled | :anonymous | nil | 1 + + :public | :public | :private | :admin | true | 1 + :public | :public | :private | :admin | false | 0 + :public | :public | :private | :reporter | nil | 1 + :public | :public | :private | :guest | nil | 1 + :public | :public | :private | :non_member | nil | 0 + :public | :public | :private | :anonymous | nil | 0 + + :public | :public | :disabled | :admin | true | 1 + :public | :public | :disabled | :admin | false | 0 + :public | :public | :disabled | :reporter | nil | 0 + :public | :public | :disabled | :guest | nil | 0 + :public | :public | :disabled | :non_member | nil | 0 + :public | :public | :disabled | :anonymous | nil | 0 + + :public | :internal | :enabled | :admin | true | 1 + :public | :internal | :enabled | :admin | false | 1 + :public | :internal | :enabled | :reporter | nil | 1 + :public | :internal | :enabled | :guest | nil | 1 + :public | :internal | :enabled | :non_member | nil | 1 + :public | :internal | :enabled | :anonymous | nil | 0 + + :public | :internal | :private | :admin | true | 1 + :public | :internal | :private | :admin | false | 0 + :public | :internal | :private | :reporter | nil | 1 + :public | :internal | :private | :guest | nil | 1 + :public | :internal | :private | :non_member | nil | 0 + :public | :internal | :private | :anonymous | nil | 0 + + :public | :internal | :disabled | :admin | true | 1 + :public | :internal | :disabled | :admin | false | 0 + :public | :internal | :disabled | :reporter | nil | 0 + :public | :internal | :disabled | :guest | nil | 0 + :public | :internal | :disabled | :non_member | nil | 0 + :public | :internal | :disabled | :anonymous | nil | 0 + + :public | :private | :private | :admin | true | 1 + :public | :private | :private | :admin | false | 0 + :public | :private | :private | :reporter | nil | 1 + :public | :private | :private | :guest | nil | 1 + :public | :private | :private | :non_member | nil | 0 + :public | :private | :private | :anonymous | nil | 0 + + :public | :private | :disabled | :reporter | nil | 0 + :public | :private | :disabled | :guest | nil | 0 + :public | :private | :disabled | :non_member | nil | 0 + :public | :private | :disabled | :anonymous | nil | 0 + + :internal | :public | :enabled | :admin | true | 1 + :internal | :public | :enabled | :admin | false | 1 + :internal | :public | :enabled | :reporter | nil | 1 + :internal | :public | :enabled | :guest | nil | 1 + :internal | :public | :enabled | :non_member | nil | 1 + :internal | :public | :enabled | :anonymous | nil | 0 + + :internal | :public | :private | :admin | true | 1 + :internal | :public | :private | :admin | false | 0 + :internal | :public | :private | :reporter | nil | 1 + :internal | :public | :private | :guest | nil | 1 + :internal | :public | :private | :non_member | nil | 0 + :internal | :public | :private | :anonymous | nil | 0 + + :internal | :public | :disabled | :admin | true | 1 + :internal | :public | :disabled | :admin | false | 0 + :internal | :public | :disabled | :reporter | nil | 0 + :internal | :public | :disabled | :guest | nil | 0 + :internal | :public | :disabled | :non_member | nil | 0 + :internal | :public | :disabled | :anonymous | nil | 0 + + :internal | :internal | :enabled | :admin | true | 1 + :internal | :internal | :enabled | :admin | false | 1 + :internal | :internal | :enabled | :reporter | nil | 1 + :internal | :internal | :enabled | :guest | nil | 1 + :internal | :internal | :enabled | :non_member | nil | 1 + :internal | :internal | :enabled | :anonymous | nil | 0 + + :internal | :internal | :private | :admin | true | 1 + :internal | :internal | :private | :admin | false | 0 + :internal | :internal | :private | :reporter | nil | 1 + :internal | :internal | :private | :guest | nil | 1 + :internal | :internal | :private | :non_member | nil | 0 + :internal | :internal | :private | :anonymous | nil | 0 + + :internal | :internal | :disabled | :admin | true | 1 + :internal | :internal | :disabled | :admin | false | 0 + :internal | :internal | :disabled | :reporter | nil | 0 + :internal | :internal | :disabled | :guest | nil | 0 + :internal | :internal | :disabled | :non_member | nil | 0 + :internal | :internal | :disabled | :anonymous | nil | 0 + + :internal | :private | :private | :admin | true | 1 + :internal | :private | :private | :admin | false | 0 + :internal | :private | :private | :reporter | nil | 1 + :internal | :private | :private | :guest | nil | 1 + :internal | :private | :private | :non_member | nil | 0 + :internal | :private | :private | :anonymous | nil | 0 + + :internal | :private | :disabled | :admin | true | 1 + :internal | :private | :disabled | :admin | false | 0 + :internal | :private | :disabled | :reporter | nil | 0 + :internal | :private | :disabled | :guest | nil | 0 + :internal | :private | :disabled | :non_member | nil | 0 + :internal | :private | :disabled | :anonymous | nil | 0 + + :private | :public | :enabled | :admin | true | 1 + :private | :public | :enabled | :admin | false | 0 + :private | :public | :enabled | :reporter | nil | 1 + :private | :public | :enabled | :guest | nil | 1 + :private | :public | :enabled | :non_member | nil | 0 + :private | :public | :enabled | :anonymous | nil | 0 + + :private | :public | :private | :admin | true | 1 + :private | :public | :private | :admin | false | 0 + :private | :public | :private | :reporter | nil | 1 + :private | :public | :private | :guest | nil | 1 + :private | :public | :private | :non_member | nil | 0 + :private | :public | :private | :anonymous | nil | 0 + + :private | :public | :disabled | :admin | true | 1 + :private | :public | :disabled | :admin | false | 0 + :private | :public | :disabled | :reporter | nil | 0 + :private | :public | :disabled | :guest | nil | 0 + :private | :public | :disabled | :non_member | nil | 0 + :private | :public | :disabled | :anonymous | nil | 0 + + :private | :internal | :enabled | :admin | true | 1 + :private | :internal | :enabled | :admin | false | 0 + :private | :internal | :enabled | :reporter | nil | 1 + :private | :internal | :enabled | :guest | nil | 1 + :private | :internal | :enabled | :non_member | nil | 0 + :private | :internal | :enabled | :anonymous | nil | 0 + + :private | :internal | :private | :admin | true | 1 + :private | :internal | :private | :admin | false | 0 + :private | :internal | :private | :reporter | nil | 1 + :private | :internal | :private | :guest | nil | 1 + :private | :internal | :private | :non_member | nil | 0 + :private | :internal | :private | :anonymous | nil | 0 + + :private | :internal | :disabled | :admin | true | 1 + :private | :internal | :disabled | :admin | false | 0 + :private | :internal | :disabled | :reporter | nil | 0 + :private | :internal | :disabled | :guest | nil | 0 + :private | :internal | :disabled | :non_member | nil | 0 + :private | :internal | :disabled | :anonymous | nil | 0 + + :private | :private | :private | :admin | true | 1 + :private | :private | :private | :admin | false | 0 + :private | :private | :private | :reporter | nil | 1 + :private | :private | :private | :guest | nil | 1 + :private | :private | :private | :non_member | nil | 0 + :private | :private | :private | :anonymous | nil | 0 + + :private | :private | :disabled | :admin | true | 1 + :private | :private | :disabled | :admin | false | 0 + :private | :private | :disabled | :reporter | nil | 0 + :private | :private | :disabled | :guest | nil | 0 + :private | :private | :disabled | :non_member | nil | 0 + :private | :private | :disabled | :anonymous | nil | 0 end # :snippet_level, :membership, :expected_count def permission_table_for_personal_snippet_access - :public | :admin | 1 - :public | :author | 1 - :public | :non_member | 1 - :public | :anonymous | 1 - - :internal | :admin | 1 - :internal | :author | 1 - :internal | :non_member | 1 - :internal | :anonymous | 0 - - :private | :admin | 1 - :private | :author | 1 - :private | :non_member | 0 - :private | :anonymous | 0 + :public | :admin | true | 1 + :public | :admin | false | 1 + :public | :author | nil | 1 + :public | :non_member | nil | 1 + :public | :anonymous | nil | 1 + + :internal | :admin | true | 1 + :internal | :admin | false | 1 + :internal | :author | nil | 1 + :internal | :non_member | nil | 1 + :internal | :anonymous | nil | 0 + + :private | :admin | true | 1 + :private | :admin | false | 0 + :private | :author | nil | 1 + :private | :non_member | nil | 0 + :private | :anonymous | nil | 0 end # rubocop:enable Metrics/AbcSize end diff --git a/spec/support/shared_contexts/requests/api/graphql/jira_import/jira_projects_context.rb b/spec/support/shared_contexts/requests/api/graphql/jira_import/jira_projects_context.rb index edc5b313220..de40b926a1c 100644 --- a/spec/support/shared_contexts/requests/api/graphql/jira_import/jira_projects_context.rb +++ b/spec/support/shared_contexts/requests/api/graphql/jira_import/jira_projects_context.rb @@ -116,6 +116,7 @@ RSpec.shared_context 'Jira projects request context' do "uuid": "14935009-f8aa-481e-94bc-f7251f320b0e" }]' end + let_it_be(:empty_jira_projects_json) do '{ "self": "https://your-domain.atlassian.net/rest/api/2/project/search?startAt=0&maxResults=2", diff --git a/spec/support/shared_contexts/requests/api/npm_packages_shared_context.rb b/spec/support/shared_contexts/requests/api/npm_packages_shared_context.rb new file mode 100644 index 00000000000..7c23ec33cf8 --- /dev/null +++ b/spec/support/shared_contexts/requests/api/npm_packages_shared_context.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +RSpec.shared_context 'npm api setup' do + include PackagesManagerApiSpecHelpers + include HttpBasicAuthHelpers + + let_it_be(:user) { create(:user) } + let_it_be(:group) { create(:group) } + let_it_be(:project, reload: true) { create(:project, :public, namespace: group) } + let_it_be(:package, reload: true) { create(:npm_package, project: project) } + let_it_be(:token) { create(:oauth_access_token, scopes: 'api', resource_owner: user) } + let_it_be(:personal_access_token) { create(:personal_access_token, user: user) } + let_it_be(:job, reload: true) { create(:ci_build, user: user, status: :running) } + let_it_be(:deploy_token) { create(:deploy_token, read_package_registry: true, write_package_registry: true) } + let_it_be(:project_deploy_token) { create(:project_deploy_token, deploy_token: deploy_token, project: project) } + + let(:package_name) { package.name } + + before do + project.add_developer(user) + 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 84910d0dfe4..38a5ed244c4 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 @@ -48,7 +48,7 @@ RSpec.shared_examples 'multiple issue boards' do expect(page).to have_button('This is a new board') end - it 'deletes board' do + it 'deletes board', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/280554' do in_boards_switcher_dropdown do click_button 'Delete board' end diff --git a/spec/support/shared_examples/cached_response_shared_examples.rb b/spec/support/shared_examples/cached_response_shared_examples.rb deleted file mode 100644 index 34e5f741b4e..00000000000 --- a/spec/support/shared_examples/cached_response_shared_examples.rb +++ /dev/null @@ -1,12 +0,0 @@ -# frozen_string_literal: true -# -# Negates lib/gitlab/no_cache_headers.rb -# - -RSpec.shared_examples 'cached response' do - it 'defines a cached header response' do - expect(response.headers["Cache-Control"]).not_to include("no-store", "no-cache") - expect(response.headers["Pragma"]).not_to eq("no-cache") - expect(response.headers["Expires"]).not_to eq("Fri, 01 Jan 1990 00:00:00 GMT") - end -end diff --git a/spec/support/shared_examples/controllers/access_tokens_controller_shared_examples.rb b/spec/support/shared_examples/controllers/access_tokens_controller_shared_examples.rb index 54d41f9a68c..dd71107455f 100644 --- a/spec/support/shared_examples/controllers/access_tokens_controller_shared_examples.rb +++ b/spec/support/shared_examples/controllers/access_tokens_controller_shared_examples.rb @@ -73,7 +73,23 @@ RSpec.shared_examples 'project access tokens available #create' do end end - it { expect(subject).to render_template(:index) } + it 'does not create the token' do + expect { subject }.not_to change { PersonalAccessToken.count } + end + + it 'does not add the project bot as a member' do + expect { subject }.not_to change { Member.count } + end + + it 'does not create the project bot user' do + expect { subject }.not_to change { User.count } + end + + it 'shows a failure alert' do + subject + + expect(response.flash[:alert]).to match("Failed to create new project access token: Failed!") + end end end diff --git a/spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb b/spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb index 2fcc88ef36a..5a4322f73b6 100644 --- a/spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb +++ b/spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb @@ -145,6 +145,8 @@ RSpec.shared_examples 'a GitHub-ish import controller: GET status' do group.add_owner(user) client = stub_client(repos: repos, orgs: [org], org_repos: [org_repo]) allow(client).to receive(:each_page).and_return([OpenStruct.new(objects: repos)].to_enum) + # GitHub controller has filtering done using GitHub Search API + stub_feature_flags(remove_legacy_github_client: false) end it 'filters list of repositories by name' do @@ -157,6 +159,16 @@ RSpec.shared_examples 'a GitHub-ish import controller: GET status' do expect(json_response.dig("namespaces", 0, "id")).to eq(group.id) end + it 'filters the list, ignoring the case of the name' do + get :status, params: { filter: 'EMACS' }, format: :json + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response.dig("imported_projects").count).to eq(0) + expect(json_response.dig("provider_repos").count).to eq(1) + expect(json_response.dig("provider_repos", 0, "id")).to eq(repo_2.id) + expect(json_response.dig("namespaces", 0, "id")).to eq(group.id) + end + context 'when user input contains html' do let(:expected_filter) { 'test' } let(:filter) { "<html>#{expected_filter}</html>" } @@ -167,6 +179,23 @@ RSpec.shared_examples 'a GitHub-ish import controller: GET status' do expect(assigns(:filter)).to eq(expected_filter) end end + + context 'when the client returns a non-string name' do + before do + repos = [build(:project, name: 2, path: 'test')] + + client = stub_client(repos: repos) + allow(client).to receive(:each_page).and_return([OpenStruct.new(objects: repos)].to_enum) + end + + it 'does not raise an error' do + get :status, params: { filter: '2' }, format: :json + + expect(response).to have_gitlab_http_status :ok + + expect(json_response.dig("provider_repos").count).to eq(1) + end + end end end diff --git a/spec/support/shared_examples/controllers/trackable_shared_examples.rb b/spec/support/shared_examples/controllers/trackable_shared_examples.rb index e82c27c43f5..dac7d8c94ff 100644 --- a/spec/support/shared_examples/controllers/trackable_shared_examples.rb +++ b/spec/support/shared_examples/controllers/trackable_shared_examples.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true RSpec.shared_examples 'a Trackable Controller' do - describe '#track_event' do + describe '#track_event', :snowplow do before do sign_in user end @@ -14,9 +14,10 @@ RSpec.shared_examples 'a Trackable Controller' do end end - it 'tracks the action name' do - expect(Gitlab::Tracking).to receive(:event).with('AnonymousController', 'index', {}) + it 'tracks the action name', :snowplow do get :index + + expect_snowplow_event(category: 'AnonymousController', action: 'index') end end @@ -29,8 +30,9 @@ RSpec.shared_examples 'a Trackable Controller' do end it 'tracks with the specified param' do - expect(Gitlab::Tracking).to receive(:event).with('SomeCategory', 'some_event', label: 'errorlabel') get :index + + expect_snowplow_event(category: 'SomeCategory', action: 'some_event', label: 'errorlabel') end end end diff --git a/spec/support/shared_examples/features/discussion_comments_shared_example.rb b/spec/support/shared_examples/features/discussion_comments_shared_example.rb index 9fc5d8933e5..560cfbfb117 100644 --- a/spec/support/shared_examples/features/discussion_comments_shared_example.rb +++ b/spec/support/shared_examples/features/discussion_comments_shared_example.rb @@ -56,12 +56,12 @@ RSpec.shared_examples 'thread comments' do |resource_name| expect(items.first).to have_content 'Comment' expect(items.first).to have_content "Add a general comment to this #{resource_name}." - expect(items.first).to have_selector '.fa-check' + expect(items.first).to have_selector '[data-testid="check-icon"]' expect(items.first['class']).to match 'droplab-item-selected' expect(items.last).to have_content 'Start thread' expect(items.last).to have_content "Discuss a specific suggestion or question#{' that needs to be resolved' if resource_name == 'merge request'}." - expect(items.last).not_to have_selector '.fa-check' + expect(items.last).not_to have_selector '[data-testid="check-icon"]' expect(items.last['class']).not_to match 'droplab-item-selected' end @@ -228,11 +228,11 @@ RSpec.shared_examples 'thread comments' do |resource_name| items = all("#{menu_selector} li") expect(items.first).to have_content 'Comment' - expect(items.first).not_to have_selector '.fa-check' + expect(items.first).not_to have_selector '[data-testid="check-icon"]' expect(items.first['class']).not_to match 'droplab-item-selected' expect(items.last).to have_content 'Start thread' - expect(items.last).to have_selector '.fa-check' + expect(items.last).to have_selector '[data-testid="check-icon"]' expect(items.last['class']).to match 'droplab-item-selected' end @@ -274,11 +274,11 @@ RSpec.shared_examples 'thread comments' do |resource_name| aggregate_failures do expect(items.first).to have_content 'Comment' - expect(items.first).to have_selector '.fa-check' + expect(items.first).to have_selector '[data-testid="check-icon"]' expect(items.first['class']).to match 'droplab-item-selected' expect(items.last).to have_content 'Start thread' - expect(items.last).not_to have_selector '.fa-check' + expect(items.last).not_to have_selector '[data-testid="check-icon"]' expect(items.last['class']).not_to match 'droplab-item-selected' end end diff --git a/spec/support/shared_examples/features/master_manages_access_requests_shared_example.rb b/spec/support/shared_examples/features/master_manages_access_requests_shared_example.rb index ffe4fb83283..724d6db2705 100644 --- a/spec/support/shared_examples/features/master_manages_access_requests_shared_example.rb +++ b/spec/support/shared_examples/features/master_manages_access_requests_shared_example.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true RSpec.shared_examples 'Maintainer manages access requests' do + include Spec::Support::Helpers::Features::MembersHelpers + let(:user) { create(:user) } let(:maintainer) { create(:user) } @@ -26,7 +28,7 @@ RSpec.shared_examples 'Maintainer manages access requests' do expect_no_visible_access_request(entity, user) - page.within('[data-qa-selector="members_list"]') do + page.within(members_table) do expect(page).to have_content user.name end end @@ -35,7 +37,7 @@ RSpec.shared_examples 'Maintainer manages access requests' do expect_visible_access_request(entity, user) # Open modal - click_on 'Deny access request' + click_on 'Deny access' expect(page).not_to have_field "Also unassign this user from related issues and merge requests" diff --git a/spec/support/shared_examples/features/variable_list_shared_examples.rb b/spec/support/shared_examples/features/variable_list_shared_examples.rb index 218ef070221..e0d169c6868 100644 --- a/spec/support/shared_examples/features/variable_list_shared_examples.rb +++ b/spec/support/shared_examples/features/variable_list_shared_examples.rb @@ -1,387 +1,295 @@ # frozen_string_literal: true RSpec.shared_examples 'variable list' do - it 'shows list of variables' do - page.within('.js-ci-variable-list-section') do - expect(first('.js-ci-variable-input-key').value).to eq(variable.key) + it 'shows a list of variables' do + page.within('.ci-variable-table') do + expect(find('.js-ci-variable-row:nth-child(1) td[data-label="Key"]').text).to eq(variable.key) end end - it 'adds new CI variable' do - page.within('.js-ci-variable-list-section .js-row:last-child') do - find('.js-ci-variable-input-key').set('key') - find('.js-ci-variable-input-value').set('key_value') + it 'adds a new CI variable' do + click_button('Add Variable') + + fill_variable('key', 'key_value') do + click_button('Add variable') end - click_button('Save variables') wait_for_requests - visit page_path - - # We check the first row because it re-sorts to alphabetical order on refresh - page.within('.js-ci-variable-list-section .js-row:nth-child(2)') do - expect(find('.js-ci-variable-input-key').value).to eq('key') - expect(find('.js-ci-variable-input-value', visible: false).value).to eq('key_value') + page.within('.ci-variable-table') do + expect(find('.js-ci-variable-row:nth-child(1) td[data-label="Key"]').text).to eq('key') end end it 'adds a new protected variable' do - page.within('.js-ci-variable-list-section .js-row:last-child') do - find('.js-ci-variable-input-key').set('key') - find('.js-ci-variable-input-value').set('key_value') + click_button('Add Variable') - expect(find('.js-ci-variable-input-protected', visible: false).value).to eq('true') + fill_variable('key', 'key_value') do + click_button('Add variable') end - click_button('Save variables') wait_for_requests - visit page_path - - # We check the first row because it re-sorts to alphabetical order on refresh - page.within('.js-ci-variable-list-section .js-row:nth-child(2)') do - expect(find('.js-ci-variable-input-key').value).to eq('key') - expect(find('.js-ci-variable-input-value', visible: false).value).to eq('key_value') - expect(find('.js-ci-variable-input-protected', visible: false).value).to eq('true') + page.within('.ci-variable-table') do + expect(find('.js-ci-variable-row:nth-child(1) td[data-label="Key"]').text).to eq('key') + expect(find('.js-ci-variable-row:nth-child(1) td[data-label="Protected"] svg[data-testid="mobile-issue-close-icon"]')).to be_present end end it 'defaults to unmasked' do - page.within('.js-ci-variable-list-section .js-row:last-child') do - find('.js-ci-variable-input-key').set('key') - find('.js-ci-variable-input-value').set('key_value') + click_button('Add Variable') - expect(find('.js-ci-variable-input-masked', visible: false).value).to eq('false') + fill_variable('key', 'key_value') do + click_button('Add variable') end - click_button('Save variables') wait_for_requests - visit page_path - - # We check the first row because it re-sorts to alphabetical order on refresh - page.within('.js-ci-variable-list-section .js-row:nth-child(2)') do - expect(find('.js-ci-variable-input-key').value).to eq('key') - expect(find('.js-ci-variable-input-value', visible: false).value).to eq('key_value') - expect(find('.js-ci-variable-input-masked', visible: false).value).to eq('false') - end - end - - context 'defaults to the application setting' do - context 'application setting is true' do - before do - stub_application_setting(protected_ci_variables: true) - - visit page_path - end - - it 'defaults to protected' do - page.within('.js-ci-variable-list-section .js-row:last-child') do - find('.js-ci-variable-input-key').set('key') - end - - values = all('.js-ci-variable-input-protected', visible: false).map(&:value) - - expect(values).to eq %w(false true true) - end - - it 'shows a message regarding the changed default' do - expect(page).to have_content 'Environment variables are configured by your administrator to be protected by default' - end - end - - context 'application setting is false' do - before do - stub_application_setting(protected_ci_variables: false) - - visit page_path - end - - it 'defaults to unprotected' do - page.within('.js-ci-variable-list-section .js-row:last-child') do - find('.js-ci-variable-input-key').set('key') - end - - values = all('.js-ci-variable-input-protected', visible: false).map(&:value) - - expect(values).to eq %w(false false false) - end - - it 'does not show a message regarding the default' do - expect(page).not_to have_content 'Environment variables are configured by your administrator to be protected by default' - end + page.within('.ci-variable-table') do + expect(find('.js-ci-variable-row:nth-child(1) td[data-label="Key"]').text).to eq('key') + expect(find('.js-ci-variable-row:nth-child(1) td[data-label="Masked"] svg[data-testid="close-icon"]')).to be_present end end it 'reveals and hides variables' do - page.within('.js-ci-variable-list-section') do - expect(first('.js-ci-variable-input-key').value).to eq(variable.key) - expect(first('.js-ci-variable-input-value', visible: false).value).to eq(variable.value) + page.within('.ci-variable-table') do + expect(first('.js-ci-variable-row td[data-label="Key"]').text).to eq(variable.key) expect(page).to have_content('*' * 17) click_button('Reveal value') - expect(first('.js-ci-variable-input-key').value).to eq(variable.key) - expect(first('.js-ci-variable-input-value').value).to eq(variable.value) + expect(first('.js-ci-variable-row td[data-label="Key"]').text).to eq(variable.key) + expect(first('.js-ci-variable-row td[data-label="Value"]').text).to eq(variable.value) expect(page).not_to have_content('*' * 17) click_button('Hide value') - expect(first('.js-ci-variable-input-key').value).to eq(variable.key) - expect(first('.js-ci-variable-input-value', visible: false).value).to eq(variable.value) + expect(first('.js-ci-variable-row td[data-label="Key"]').text).to eq(variable.key) expect(page).to have_content('*' * 17) end end - it 'deletes variable' do - page.within('.js-ci-variable-list-section') do - expect(page).to have_selector('.js-row', count: 2) + it 'deletes a variable' do + expect(page).to have_selector('.js-ci-variable-row', count: 1) - first('.js-row-remove-button').click - - click_button('Save variables') - wait_for_requests - - expect(page).to have_selector('.js-row', count: 1) + page.within('.ci-variable-table') do + click_button('Edit') end - end - it 'edits variable' do - page.within('.js-ci-variable-list-section') do - click_button('Reveal value') - - page.within('.js-row:nth-child(2)') do - find('.js-ci-variable-input-key').set('new_key') - find('.js-ci-variable-input-value').set('new_value') - end + page.within('#add-ci-variable') do + click_button('Delete variable') + end - click_button('Save variables') - wait_for_requests + wait_for_requests - visit page_path + expect(first('.js-ci-variable-row').text).to eq('There are no variables yet.') + end - page.within('.js-row:nth-child(2)') do - expect(find('.js-ci-variable-input-key').value).to eq('new_key') - expect(find('.js-ci-variable-input-value', visible: false).value).to eq('new_value') - end + it 'edits a variable' do + page.within('.ci-variable-table') do + click_button('Edit') end - end - it 'edits variable to be protected' do - # Create the unprotected variable - page.within('.js-ci-variable-list-section .js-row:last-child') do - find('.js-ci-variable-input-key').set('unprotected_key') - find('.js-ci-variable-input-value').set('unprotected_value') - find('.ci-variable-protected-item .js-project-feature-toggle').click + page.within('#add-ci-variable') do + find('[data-qa-selector="ci_variable_key_field"] input').set('new_key') - expect(find('.js-ci-variable-input-protected', visible: false).value).to eq('false') + click_button('Update variable') end - click_button('Save variables') wait_for_requests - visit page_path + expect(first('.js-ci-variable-row td[data-label="Key"]').text).to eq('new_key') + end + + it 'edits a variable to be unmasked' do + page.within('.ci-variable-table') do + click_button('Edit') + end - # We check the first row because it re-sorts to alphabetical order on refresh - page.within('.js-ci-variable-list-section .js-row:nth-child(3)') do - find('.ci-variable-protected-item .js-project-feature-toggle').click + page.within('#add-ci-variable') do + find('[data-testid="ci-variable-protected-checkbox"]').click + find('[data-testid="ci-variable-masked-checkbox"]').click - expect(find('.js-ci-variable-input-protected', visible: false).value).to eq('true') + click_button('Update variable') end - click_button('Save variables') wait_for_requests - visit page_path - - # We check the first row because it re-sorts to alphabetical order on refresh - page.within('.js-ci-variable-list-section .js-row:nth-child(3)') do - expect(find('.js-ci-variable-input-key').value).to eq('unprotected_key') - expect(find('.js-ci-variable-input-value', visible: false).value).to eq('unprotected_value') - expect(find('.js-ci-variable-input-protected', visible: false).value).to eq('true') + page.within('.ci-variable-table') do + expect(find('.js-ci-variable-row:nth-child(1) td[data-label="Masked"] svg[data-testid="close-icon"]')).to be_present end end - it 'edits variable to be unprotected' do - # Create the protected variable - page.within('.js-ci-variable-list-section .js-row:last-child') do - find('.js-ci-variable-input-key').set('protected_key') - find('.js-ci-variable-input-value').set('protected_value') + it 'edits a variable to be masked' do + page.within('.ci-variable-table') do + click_button('Edit') + end + + page.within('#add-ci-variable') do + find('[data-testid="ci-variable-masked-checkbox"]').click - expect(find('.js-ci-variable-input-protected', visible: false).value).to eq('true') + click_button('Update variable') end - click_button('Save variables') wait_for_requests - visit page_path + page.within('.ci-variable-table') do + click_button('Edit') + end - page.within('.js-ci-variable-list-section .js-row:nth-child(2)') do - find('.ci-variable-protected-item .js-project-feature-toggle').click + page.within('#add-ci-variable') do + find('[data-testid="ci-variable-masked-checkbox"]').click - expect(find('.js-ci-variable-input-protected', visible: false).value).to eq('false') + click_button('Update variable') end - click_button('Save variables') - wait_for_requests + page.within('.ci-variable-table') do + expect(find('.js-ci-variable-row:nth-child(1) td[data-label="Masked"] svg[data-testid="mobile-issue-close-icon"]')).to be_present + end + end - visit page_path + it 'shows a validation error box about duplicate keys' do + click_button('Add Variable') - page.within('.js-ci-variable-list-section .js-row:nth-child(2)') do - expect(find('.js-ci-variable-input-key').value).to eq('protected_key') - expect(find('.js-ci-variable-input-value', visible: false).value).to eq('protected_value') - expect(find('.js-ci-variable-input-protected', visible: false).value).to eq('false') + fill_variable('key', 'key_value') do + click_button('Add variable') end - end - it 'edits variable to be unmasked' do - page.within('.js-ci-variable-list-section .js-row:last-child') do - find('.js-ci-variable-input-key').set('unmasked_key') - find('.js-ci-variable-input-value').set('unmasked_value') - expect(find('.js-ci-variable-input-masked', visible: false).value).to eq('false') + wait_for_requests - find('.ci-variable-masked-item .js-project-feature-toggle').click + click_button('Add Variable') - expect(find('.js-ci-variable-input-masked', visible: false).value).to eq('true') + fill_variable('key', 'key_value') do + click_button('Add variable') end - click_button('Save variables') wait_for_requests - visit page_path + expect(find('.flash-container')).to be_present + expect(find('.flash-text').text).to have_content('Variables key (key) has already been taken') + end - page.within('.js-ci-variable-list-section .js-row:nth-child(2)') do - expect(find('.js-ci-variable-input-masked', visible: false).value).to eq('true') + it 'prevents a variable to be added if no values are provided when a variable is set to masked' do + click_button('Add Variable') - find('.ci-variable-masked-item .js-project-feature-toggle').click + page.within('#add-ci-variable') do + find('[data-qa-selector="ci_variable_key_field"] input').set('empty_mask_key') + find('[data-testid="ci-variable-protected-checkbox"]').click + find('[data-testid="ci-variable-masked-checkbox"]').click - expect(find('.js-ci-variable-input-masked', visible: false).value).to eq('false') + expect(find_button('Add variable', disabled: true)).to be_present end + end - click_button('Save variables') - wait_for_requests - - visit page_path + it 'shows validation error box about unmaskable values' do + click_button('Add Variable') - page.within('.js-ci-variable-list-section .js-row:nth-child(2)') do - expect(find('.js-ci-variable-input-masked', visible: false).value).to eq('false') + fill_variable('empty_mask_key', '???', protected: true, masked: true) do + expect(page).to have_content('This variable can not be masked') + expect(find_button('Add variable', disabled: true)).to be_present end end - it 'edits variable to be masked' do - page.within('.js-ci-variable-list-section .js-row:last-child') do - find('.js-ci-variable-input-key').set('masked_key') - find('.js-ci-variable-input-value').set('masked_value') - expect(find('.js-ci-variable-input-masked', visible: false).value).to eq('false') + it 'handles multiple edits and a deletion' do + # Create two variables + click_button('Add Variable') - find('.ci-variable-masked-item .js-project-feature-toggle').click - - expect(find('.js-ci-variable-input-masked', visible: false).value).to eq('true') + fill_variable('akey', 'akeyvalue') do + click_button('Add variable') end - click_button('Save variables') wait_for_requests - visit page_path + click_button('Add Variable') - page.within('.js-ci-variable-list-section .js-row:nth-child(2)') do - expect(find('.js-ci-variable-input-masked', visible: false).value).to eq('true') + fill_variable('zkey', 'zkeyvalue') do + click_button('Add variable') end - end - - it 'handles multiple edits and deletion in the middle' do - page.within('.js-ci-variable-list-section') do - # Create 2 variables - page.within('.js-row:last-child') do - find('.js-ci-variable-input-key').set('akey') - find('.js-ci-variable-input-value').set('akeyvalue') - end - page.within('.js-row:last-child') do - find('.js-ci-variable-input-key').set('zkey') - find('.js-ci-variable-input-value').set('zkeyvalue') - end - click_button('Save variables') - wait_for_requests + wait_for_requests - expect(page).to have_selector('.js-row', count: 4) + expect(page).to have_selector('.js-ci-variable-row', count: 3) - # Remove the `akey` variable - page.within('.js-row:nth-child(3)') do - first('.js-row-remove-button').click + # Remove the `akey` variable + page.within('.ci-variable-table') do + page.within('.js-ci-variable-row:first-child') do + click_button('Edit') end + end - # Add another variable - page.within('.js-row:last-child') do - find('.js-ci-variable-input-key').set('ckey') - find('.js-ci-variable-input-value').set('ckeyvalue') - end + page.within('#add-ci-variable') do + click_button('Delete variable') + end - click_button('Save variables') - wait_for_requests + wait_for_requests - visit page_path + # Add another variable + click_button('Add Variable') - # Expect to find 3 variables(4 rows) in alphbetical order - expect(page).to have_selector('.js-row', count: 4) - row_keys = all('.js-ci-variable-input-key') - expect(row_keys[0].value).to eq('ckey') - expect(row_keys[1].value).to eq('test_key') - expect(row_keys[2].value).to eq('zkey') - expect(row_keys[3].value).to eq('') + fill_variable('ckey', 'ckeyvalue') do + click_button('Add variable') end + + wait_for_requests + + # expect to find 3 rows of variables in alphabetical order + expect(page).to have_selector('.js-ci-variable-row', count: 3) + rows = all('.js-ci-variable-row') + expect(rows[0].find('td[data-label="Key"]').text).to eq('ckey') + expect(rows[1].find('td[data-label="Key"]').text).to eq('test_key') + expect(rows[2].find('td[data-label="Key"]').text).to eq('zkey') end - it 'shows validation error box about duplicate keys' do - page.within('.js-ci-variable-list-section .js-row:last-child') do - find('.js-ci-variable-input-key').set('samekey') - find('.js-ci-variable-input-value').set('value123') - end - page.within('.js-ci-variable-list-section .js-row:last-child') do - find('.js-ci-variable-input-key').set('samekey') - find('.js-ci-variable-input-value').set('value456') - end + context 'defaults to the application setting' do + context 'application setting is true' do + before do + stub_application_setting(protected_ci_variables: true) - click_button('Save variables') - wait_for_requests + visit page_path + end - expect(all('.js-ci-variable-list-section .js-ci-variable-error-box ul li').count).to eq(1) + it 'defaults to protected' do + click_button('Add Variable') - # We check the first row because it re-sorts to alphabetical order on refresh - page.within('.js-ci-variable-list-section') do - expect(find('.js-ci-variable-error-box')).to have_content(/Validation failed Variables have duplicate values \(.+\)/) - end - end + page.within('#add-ci-variable') do + expect(find('[data-testid="ci-variable-protected-checkbox"]')).to be_checked + end + end - it 'shows validation error box about masking empty values' do - page.within('.js-ci-variable-list-section .js-row:last-child') do - find('.js-ci-variable-input-key').set('empty_value') - find('.js-ci-variable-input-value').set('') - find('.ci-variable-masked-item .js-project-feature-toggle').click + it 'shows a message regarding the changed default' do + expect(page).to have_content 'Environment variables are configured by your administrator to be protected by default' + end end - click_button('Save variables') - wait_for_requests + context 'application setting is false' do + before do + stub_application_setting(protected_ci_variables: false) - page.within('.js-ci-variable-list-section') do - expect(all('.js-ci-variable-error-box ul li').count).to eq(1) - expect(find('.js-ci-variable-error-box')).to have_content(/Validation failed Variables value is invalid/) - end - end + visit page_path + end - it 'shows validation error box about unmaskable values' do - page.within('.js-ci-variable-list-section .js-row:last-child') do - find('.js-ci-variable-input-key').set('unmaskable_value') - find('.js-ci-variable-input-value').set('???') - find('.ci-variable-masked-item .js-project-feature-toggle').click + it 'defaults to unprotected' do + click_button('Add Variable') + + page.within('#add-ci-variable') do + expect(find('[data-testid="ci-variable-protected-checkbox"]')).not_to be_checked + end + end + + it 'does not show a message regarding the default' do + expect(page).not_to have_content 'Environment variables are configured by your administrator to be protected by default' + end end + end - click_button('Save variables') - wait_for_requests + def fill_variable(key, value, protected: false, masked: false) + page.within('#add-ci-variable') do + find('[data-qa-selector="ci_variable_key_field"] input').set(key) + find('[data-qa-selector="ci_variable_value_field"]').set(value) if value.present? + find('[data-testid="ci-variable-protected-checkbox"]').click if protected + find('[data-testid="ci-variable-masked-checkbox"]').click if masked - page.within('.js-ci-variable-list-section') do - expect(all('.js-ci-variable-error-box ul li').count).to eq(1) - expect(find('.js-ci-variable-error-box')).to have_content(/Validation failed Variables value is invalid/) + yield end end end diff --git a/spec/support/shared_examples/features/wiki/user_deletes_wiki_page_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_deletes_wiki_page_shared_examples.rb index e1fd9c8dbec..ee0261771f9 100644 --- a/spec/support/shared_examples/features/wiki/user_deletes_wiki_page_shared_examples.rb +++ b/spec/support/shared_examples/features/wiki/user_deletes_wiki_page_shared_examples.rb @@ -17,8 +17,8 @@ RSpec.shared_examples 'User deletes wiki page' do it 'deletes a page', :js do click_on('Edit') click_on('Delete') - find('.modal-footer .btn-danger').click + find('[data-testid="confirm_deletion_button"]').click - expect(page).to have_content('Page was successfully deleted') + expect(page).to have_content('Wiki page was successfully deleted.') 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 1a5f8d7d8df..3350e54a8a7 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 @@ -213,11 +213,11 @@ RSpec.shared_examples 'User updates wiki page' do visit wiki_page_path(wiki_page.wiki, wiki_page, action: :edit) end - it 'allows changing the title if the content does not change' do + it 'allows changing the title if the content does not change', :js do fill_in 'Title', with: 'new title' click_on 'Save changes' - expect(page).to have_content('Wiki was successfully updated.') + expect(page).to have_content('Wiki page was successfully updated.') end it 'shows a validation error when trying to change the content' do diff --git a/spec/support/shared_examples/features/wiki/user_views_wiki_page_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_views_wiki_page_shared_examples.rb index 85eedbf4cc5..af769be6d4b 100644 --- a/spec/support/shared_examples/features/wiki/user_views_wiki_page_shared_examples.rb +++ b/spec/support/shared_examples/features/wiki/user_views_wiki_page_shared_examples.rb @@ -33,7 +33,7 @@ RSpec.shared_examples 'User views a wiki page' do click_on('Create page') end - expect(page).to have_content('Wiki was successfully updated.') + expect(page).to have_content('Wiki page was successfully created.') end it 'shows the history of a page that has a path' do @@ -49,7 +49,7 @@ RSpec.shared_examples 'User views a wiki page' do end end - it 'shows an old version of a page' do + it 'shows an old version of a page', :js do expect(current_path).to include('one/two/three-test') expect(find('.wiki-pages')).to have_content('three') @@ -65,7 +65,7 @@ RSpec.shared_examples 'User views a wiki page' do fill_in('Content', with: 'Updated Wiki Content') click_on('Save changes') - expect(page).to have_content('Wiki was successfully updated.') + expect(page).to have_content('Wiki page was successfully updated.') click_on('Page history') diff --git a/spec/support/shared_examples/finders/security/jobs_finder_shared_examples.rb b/spec/support/shared_examples/finders/security/jobs_finder_shared_examples.rb new file mode 100644 index 00000000000..a332b213866 --- /dev/null +++ b/spec/support/shared_examples/finders/security/jobs_finder_shared_examples.rb @@ -0,0 +1,87 @@ +# frozen_string_literal: true + +RSpec.shared_examples ::Security::JobsFinder do |default_job_types| + let(:pipeline) { create(:ci_pipeline) } + + describe '#new' do + it "does not get initialized for unsupported job types" do + expect { described_class.new(pipeline: pipeline, job_types: [:abcd]) }.to raise_error( + ArgumentError, + "job_types must be from the following: #{default_job_types}" + ) + end + end + + describe '#execute' do + let(:finder) { described_class.new(pipeline: pipeline) } + + subject { finder.execute } + + shared_examples 'JobsFinder core functionality' do + context 'when the pipeline has no jobs' do + it { is_expected.to be_empty } + end + + context 'when the pipeline has no Secure jobs' do + before do + create(:ci_build, pipeline: pipeline) + end + + it { is_expected.to be_empty } + end + + context 'when the pipeline only has jobs without report artifacts' do + before do + create(:ci_build, pipeline: pipeline, options: { artifacts: { file: 'test.file' } }) + end + + it { is_expected.to be_empty } + end + + context 'when the pipeline only has jobs with reports unrelated to Secure products' do + before do + create(:ci_build, pipeline: pipeline, options: { artifacts: { reports: { file: 'test.file' } } }) + end + + it { is_expected.to be_empty } + end + + context 'when the pipeline only has jobs with reports with paths similar but not identical to Secure reports' do + before do + create(:ci_build, pipeline: pipeline, options: { artifacts: { reports: { file: 'report:sast:result.file' } } }) + end + + it { is_expected.to be_empty } + end + + context 'when there is more than one pipeline' do + let(:job_type) { default_job_types.first } + let!(:build) { create(:ci_build, job_type, pipeline: pipeline) } + + before do + create(:ci_build, job_type, pipeline: create(:ci_pipeline)) + end + + it 'returns jobs associated with provided pipeline' do + is_expected.to eq([build]) + end + end + end + + context 'when using legacy CI build metadata config storage' do + before do + stub_feature_flags(ci_build_metadata_config: false) + end + + it_behaves_like 'JobsFinder core functionality' + end + + context 'when using the new CI build metadata config storage' do + before do + stub_feature_flags(ci_build_metadata_config: true) + end + + it_behaves_like 'JobsFinder core functionality' + end + end +end diff --git a/spec/support/shared_examples/graphql/label_fields.rb b/spec/support/shared_examples/graphql/label_fields.rb index b1bfb395bc6..caf5dae409a 100644 --- a/spec/support/shared_examples/graphql/label_fields.rb +++ b/spec/support/shared_examples/graphql/label_fields.rb @@ -106,13 +106,11 @@ RSpec.shared_examples 'querying a GraphQL type with labels' do end it 'batches queries for labels by title' do - pending('See: https://gitlab.com/gitlab-org/gitlab/-/issues/217767') - multi_selection = query_for(label_b, label_c) single_selection = query_for(label_d) expect { run_query(multi_selection) } - .to issue_same_number_of_queries_as { run_query(single_selection) } + .to issue_same_number_of_queries_as { run_query(single_selection) }.ignoring_cached_queries end end diff --git a/spec/support/shared_examples/graphql/mutations/boards_create_shared_examples.rb b/spec/support/shared_examples/graphql/mutations/boards_create_shared_examples.rb index ec64519cd9c..9c0b398a5c1 100644 --- a/spec/support/shared_examples/graphql/mutations/boards_create_shared_examples.rb +++ b/spec/support/shared_examples/graphql/mutations/boards_create_shared_examples.rb @@ -65,7 +65,7 @@ RSpec.shared_examples 'boards create mutation' do let(:params) { { name: name } } it_behaves_like 'a mutation that returns top-level errors', - errors: ['group_path or project_path arguments are required'] + errors: ['Exactly one of group_path or project_path arguments is required'] it 'does not create the board' do expect { subject }.not_to change { Board.count } diff --git a/spec/support/shared_examples/graphql/mutations/create_todo_shared_examples.rb b/spec/support/shared_examples/graphql/mutations/create_todo_shared_examples.rb new file mode 100644 index 00000000000..fbef8be9e88 --- /dev/null +++ b/spec/support/shared_examples/graphql/mutations/create_todo_shared_examples.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'create todo mutation' do + let_it_be(:current_user) { create(:user) } + + let(:mutation) { described_class.new(object: nil, context: { current_user: current_user }, field: nil) } + + context 'when user does not have permission to create todo' do + it 'raises error' do + expect { mutation.resolve(target_id: global_id_of(target)) } + .to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) + end + end + + context 'when user has permission to create todo' do + it 'creates a todo' do + target.resource_parent.add_reporter(current_user) + + result = mutation.resolve(target_id: global_id_of(target)) + + expect(result[:todo]).to be_valid + expect(result[:todo].target).to eq(target) + expect(result[:todo].state).to eq('pending') + end + end +end diff --git a/spec/support/shared_examples/graphql/mutations/issues/permission_check_shared_examples.rb b/spec/support/shared_examples/graphql/mutations/issues/permission_check_shared_examples.rb new file mode 100644 index 00000000000..34c58f524cd --- /dev/null +++ b/spec/support/shared_examples/graphql/mutations/issues/permission_check_shared_examples.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'permission level for issue mutation is correctly verified' do |raises_for_all_errors = false| + before do + issue.assignees = [] + issue.author = user + end + + shared_examples_for 'when the user does not have access to the resource' do |raise_for_assigned| + it 'raises an error' do + expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) + end + + context 'even if assigned to the issue' do + before do + issue.assignees.push(user) + end + + it 'does not modify issue' do + if raises_for_all_errors || raise_for_assigned + expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) + else + expect(subject[:issue]).to eq issue + end + end + end + + context 'even if author of the issue' do + before do + issue.author = user + end + + it 'raises an error' do + expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) + end + end + end + + context 'when the user is not a project member' do + it_behaves_like 'when the user does not have access to the resource', true + end + + context 'when the user is a project member' do + context 'with guest role' do + before do + issue.project.add_guest(user) + end + + it_behaves_like 'when the user does not have access to the resource', false + end + end +end diff --git a/spec/support/shared_examples/graphql/mutations/merge_requests/permission_check_shared_examples.rb b/spec/support/shared_examples/graphql/mutations/merge_requests/permission_check_shared_examples.rb new file mode 100644 index 00000000000..1ddbad1cea7 --- /dev/null +++ b/spec/support/shared_examples/graphql/mutations/merge_requests/permission_check_shared_examples.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'permission level for merge request mutation is correctly verified' do + before do + merge_request.assignees = [] + merge_request.reviewers = [] + merge_request.author = nil + end + + shared_examples_for 'when the user does not have access to the resource' do |raise_for_assigned| + it 'raises an error' do + expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) + end + + context 'even if assigned to the merge request' do + before do + merge_request.assignees.push(user) + end + + it 'does not modify merge request' do + if raise_for_assigned + expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) + else + # In some cases we simply do nothing instead of raising + # https://gitlab.com/gitlab-org/gitlab/-/issues/196241 + expect(subject[:merge_request]).to eq merge_request + end + end + end + + context 'even if reviewer of the merge request' do + before do + merge_request.reviewers.push(user) + end + + it 'raises an error' do + expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) + end + end + + context 'even if author of the merge request' do + before do + merge_request.author = user + end + + it 'raises an error' do + expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) + end + end + end + + context 'when the user is not a project member' do + it_behaves_like 'when the user does not have access to the resource', true + end + + context 'when the user is a project member' do + context 'with guest role' do + before do + merge_request.project.add_guest(user) + end + + it_behaves_like 'when the user does not have access to the resource', true + end + + context 'with reporter role' do + before do + merge_request.project.add_reporter(user) + end + + it_behaves_like 'when the user does not have access to the resource', false + end + end +end diff --git a/spec/support/shared_examples/helm_commands_shared_examples.rb b/spec/support/shared_examples/helm_commands_shared_examples.rb index 0a94c6648cc..64f176c5ae9 100644 --- a/spec/support/shared_examples/helm_commands_shared_examples.rb +++ b/spec/support/shared_examples/helm_commands_shared_examples.rb @@ -15,6 +15,18 @@ RSpec.shared_examples 'helm command generator' do end RSpec.shared_examples 'helm command' do + describe 'HELM_VERSION' do + subject { command.class::HELM_VERSION } + + it { is_expected.to match(/\d+\.\d+\.\d+/) } + end + + describe '#env' do + subject { command.env } + + it { is_expected.to be_a Hash } + end + describe '#rbac?' do subject { command.rbac? } diff --git a/spec/support/shared_examples/lib/gitlab/cycle_analytics/event_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/cycle_analytics/event_shared_examples.rb index d0e41605e00..145a7290ac8 100644 --- a/spec/support/shared_examples/lib/gitlab/cycle_analytics/event_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/cycle_analytics/event_shared_examples.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -RSpec.shared_examples_for 'cycle analytics event' do +RSpec.shared_examples_for 'value stream analytics event' do let(:params) { {} } let(:instance) { described_class.new(params) } diff --git a/spec/support/shared_examples/lib/gitlab/database/background_migration_job_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/database/background_migration_job_shared_examples.rb new file mode 100644 index 00000000000..20f3270526e --- /dev/null +++ b/spec/support/shared_examples/lib/gitlab/database/background_migration_job_shared_examples.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'marks background migration job records' do + it 'marks each job record as succeeded after processing' do + create(:background_migration_job, class_name: "::#{described_class.name}", + arguments: arguments) + + expect(::Gitlab::Database::BackgroundMigrationJob).to receive(:mark_all_as_succeeded).and_call_original + + expect do + subject.perform(*arguments) + end.to change { ::Gitlab::Database::BackgroundMigrationJob.succeeded.count }.from(0).to(1) + end + + it 'returns the number of job records marked as succeeded' do + create(:background_migration_job, class_name: "::#{described_class.name}", + arguments: arguments) + + jobs_updated = subject.perform(*arguments) + + expect(jobs_updated).to eq(1) + end +end diff --git a/spec/support/shared_examples/lib/gitlab/database/postgres_model_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/database/postgres_model_shared_examples.rb new file mode 100644 index 00000000000..ffebbabca58 --- /dev/null +++ b/spec/support/shared_examples/lib/gitlab/database/postgres_model_shared_examples.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'a postgres model' do + describe '.by_identifier' do + it "finds the #{described_class}" do + expect(find(identifier)).to be_a(described_class) + end + + it 'raises an error if not found' do + expect { find('public.idontexist') }.to raise_error(ActiveRecord::RecordNotFound) + end + + it 'raises ArgumentError if given a non-fully qualified identifier' do + expect { find('foo') }.to raise_error(ArgumentError, /not fully qualified/) + end + end + + describe '#to_s' do + it 'returns the name' do + expect(find(identifier).to_s).to eq(name) + end + end + + describe '#schema' do + it 'returns the schema' do + expect(find(identifier).schema).to eq(schema) + end + end + + describe '#name' do + it 'returns the name' do + expect(find(identifier).name).to eq(name) + end + end +end diff --git a/spec/support/shared_examples/lib/gitlab/middleware/read_only_gitlab_instance_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/middleware/read_only_gitlab_instance_shared_examples.rb new file mode 100644 index 00000000000..e07d3e2dec9 --- /dev/null +++ b/spec/support/shared_examples/lib/gitlab/middleware/read_only_gitlab_instance_shared_examples.rb @@ -0,0 +1,189 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'write access for a read-only GitLab instance' do + include Rack::Test::Methods + using RSpec::Parameterized::TableSyntax + + include_context 'with a mocked GitLab instance' + + context 'normal requests to a read-only GitLab instance' do + let(:fake_app) { lambda { |env| [200, { 'Content-Type' => 'text/plain' }, ['OK']] } } + + it 'expects PATCH requests to be disallowed' do + response = request.patch('/test_request') + + expect(response).to be_redirect + expect(subject).to disallow_request + end + + it 'expects PUT requests to be disallowed' do + response = request.put('/test_request') + + expect(response).to be_redirect + expect(subject).to disallow_request + end + + it 'expects POST requests to be disallowed' do + response = request.post('/test_request') + + expect(response).to be_redirect + expect(subject).to disallow_request + end + + it 'expects a internal POST request to be allowed after a disallowed request' do + response = request.post('/test_request') + + expect(response).to be_redirect + + response = request.post("/api/#{API::API.version}/internal") + + expect(response).not_to be_redirect + end + + it 'expects DELETE requests to be disallowed' do + response = request.delete('/test_request') + + expect(response).to be_redirect + expect(subject).to disallow_request + end + + it 'expects POST of new file that looks like an LFS batch url to be disallowed' do + expect(Rails.application.routes).to receive(:recognize_path).and_call_original + response = request.post('/root/gitlab-ce/new/master/app/info/lfs/objects/batch') + + expect(response).to be_redirect + expect(subject).to disallow_request + end + + it 'returns last_vistited_url for disallowed request' do + response = request.post('/test_request') + + expect(response.location).to eq 'http://localhost/' + end + + context 'allowlisted requests' do + it 'expects a POST internal request to be allowed' do + expect(Rails.application.routes).not_to receive(:recognize_path) + response = request.post("/api/#{API::API.version}/internal") + + expect(response).not_to be_redirect + expect(subject).not_to disallow_request + end + + it 'expects a graphql request to be allowed' do + response = request.post("/api/graphql") + + expect(response).not_to be_redirect + expect(subject).not_to disallow_request + end + + context 'relative URL is configured' do + before do + stub_config_setting(relative_url_root: '/gitlab') + end + + it 'expects a graphql request to be allowed' do + response = request.post("/gitlab/api/graphql") + + expect(response).not_to be_redirect + expect(subject).not_to disallow_request + end + end + + context 'sidekiq admin requests' do + where(:mounted_at) do + [ + '', + '/', + '/gitlab', + '/gitlab/', + '/gitlab/gitlab', + '/gitlab/gitlab/' + ] + end + + with_them do + before do + stub_config_setting(relative_url_root: mounted_at) + end + + it 'allows requests' do + path = File.join(mounted_at, 'admin/sidekiq') + response = request.post(path) + + expect(response).not_to be_redirect + expect(subject).not_to disallow_request + + response = request.get(path) + + expect(response).not_to be_redirect + expect(subject).not_to disallow_request + end + end + end + + where(:description, :path) do + 'LFS request to batch' | '/root/rouge.git/info/lfs/objects/batch' + 'request to git-upload-pack' | '/root/rouge.git/git-upload-pack' + end + + with_them do + it "expects a POST #{description} URL to be allowed" do + expect(Rails.application.routes).to receive(:recognize_path).and_call_original + response = request.post(path) + + expect(response).not_to be_redirect + expect(subject).not_to disallow_request + end + end + + where(:description, :path) do + 'LFS request to locks verify' | '/root/rouge.git/info/lfs/locks/verify' + 'LFS request to locks create' | '/root/rouge.git/info/lfs/locks' + 'LFS request to locks unlock' | '/root/rouge.git/info/lfs/locks/1/unlock' + end + + with_them do + it "expects a POST #{description} URL not to be allowed" do + response = request.post(path) + + expect(response).to be_redirect + expect(subject).to disallow_request + end + end + end + end + + context 'json requests to a read-only GitLab instance' do + let(:fake_app) { lambda { |env| [200, { 'Content-Type' => 'application/json' }, ['OK']] } } + let(:content_json) { { 'CONTENT_TYPE' => 'application/json' } } + + before do + allow(Gitlab::Database).to receive(:read_only?) { true } + end + + it 'expects PATCH requests to be disallowed' do + response = request.patch('/test_request', content_json) + + expect(response).to disallow_request_in_json + end + + it 'expects PUT requests to be disallowed' do + response = request.put('/test_request', content_json) + + expect(response).to disallow_request_in_json + end + + it 'expects POST requests to be disallowed' do + response = request.post('/test_request', content_json) + + expect(response).to disallow_request_in_json + end + + it 'expects DELETE requests to be disallowed' do + response = request.delete('/test_request', content_json) + + expect(response).to disallow_request_in_json + end + end +end diff --git a/spec/support/shared_examples/lib/gitlab/repository_size_checker_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/repository_size_checker_shared_examples.rb index bb909ffe82a..30413f206f8 100644 --- a/spec/support/shared_examples/lib/gitlab/repository_size_checker_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/repository_size_checker_shared_examples.rb @@ -17,35 +17,58 @@ RSpec.shared_examples 'checker size not over limit' do end RSpec.shared_examples 'checker size exceeded' do - context 'when current size is below or equal to the limit' do - let(:current_size) { 50 } + context 'when no change size provided' do + context 'when current size is below the limit' do + let(:current_size) { limit - 1 } - it 'returns zero' do - expect(subject.exceeded_size).to eq(0) + it 'returns zero' do + expect(subject.exceeded_size).to eq(0) + end end - end - context 'when current size is over the limit' do - let(:current_size) { 51 } + context 'when current size is equal to the limit' do + let(:current_size) { limit } - it 'returns zero' do - expect(subject.exceeded_size).to eq(1.megabytes) + it 'returns zero' do + expect(subject.exceeded_size).to eq(0) + end end - end - context 'when change size will be over the limit' do - let(:current_size) { 50 } + context 'when current size is over the limit' do + let(:current_size) { limit + 1 } + let(:total_repository_size_excess) { 1 } - it 'returns zero' do - expect(subject.exceeded_size(1.megabytes)).to eq(1.megabytes) + it 'returns a positive number' do + expect(subject.exceeded_size).to eq(1.megabyte) + end end end - context 'when change size will not be over the limit' do - let(:current_size) { 49 } + context 'when a change size is provided' do + let(:change_size) { 1.megabyte } + + context 'when change size will be over the limit' do + let(:current_size) { limit } + + it 'returns a positive number' do + expect(subject.exceeded_size(change_size)).to eq(1.megabyte) + end + end + + context 'when change size will be at the limit' do + let(:current_size) { limit - 1 } + + it 'returns zero' do + expect(subject.exceeded_size(change_size)).to eq(0) + end + end + + context 'when change size will be under the limit' do + let(:current_size) { limit - 2 } - it 'returns zero' do - expect(subject.exceeded_size(1.megabytes)).to eq(0) + it 'returns zero' do + expect(subject.exceeded_size(change_size)).to eq(0) + end end end end diff --git a/spec/support/shared_examples/lib/gitlab/search_confidential_filter_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/search_confidential_filter_shared_examples.rb index d0bef2ad730..e70dfec80b1 100644 --- a/spec/support/shared_examples/lib/gitlab/search_confidential_filter_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/search_confidential_filter_shared_examples.rb @@ -4,66 +4,27 @@ RSpec.shared_examples 'search results filtered by confidential' do context 'filter not provided (all behavior)' do let(:filters) { {} } - context 'when Feature search_filter_by_confidential enabled' do - it 'returns confidential and not confidential results', :aggregate_failures do - expect(results.objects('issues')).to include confidential_result - expect(results.objects('issues')).to include opened_result - end - end - - context 'when Feature search_filter_by_confidential not enabled' do - before do - stub_feature_flags(search_filter_by_confidential: false) - end - - it 'returns confidential and not confidential results', :aggregate_failures do - expect(results.objects('issues')).to include confidential_result - expect(results.objects('issues')).to include opened_result - end + it 'returns confidential and not confidential results', :aggregate_failures do + expect(results.objects('issues')).to include confidential_result + expect(results.objects('issues')).to include opened_result end end context 'confidential filter' do let(:filters) { { confidential: true } } - context 'when Feature search_filter_by_confidential enabled' do - it 'returns only confidential results', :aggregate_failures do - expect(results.objects('issues')).to include confidential_result - expect(results.objects('issues')).not_to include opened_result - end - end - - context 'when Feature search_filter_by_confidential not enabled' do - before do - stub_feature_flags(search_filter_by_confidential: false) - end - - it 'returns confidential and not confidential results', :aggregate_failures do - expect(results.objects('issues')).to include confidential_result - expect(results.objects('issues')).to include opened_result - end + it 'returns only confidential results', :aggregate_failures do + expect(results.objects('issues')).to include confidential_result + expect(results.objects('issues')).not_to include opened_result end end context 'not confidential filter' do let(:filters) { { confidential: false } } - context 'when Feature search_filter_by_confidential enabled' do - it 'returns not confidential results', :aggregate_failures do - expect(results.objects('issues')).not_to include confidential_result - expect(results.objects('issues')).to include opened_result - end - end - - context 'when Feature search_filter_by_confidential not enabled' do - before do - stub_feature_flags(search_filter_by_confidential: false) - end - - it 'returns confidential and not confidential results', :aggregate_failures do - expect(results.objects('issues')).to include confidential_result - expect(results.objects('issues')).to include opened_result - end + it 'returns not confidential results', :aggregate_failures do + expect(results.objects('issues')).not_to include confidential_result + expect(results.objects('issues')).to include opened_result end end end diff --git a/spec/support/shared_examples/lib/gitlab/search_results_sorted_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/search_results_sorted_shared_examples.rb index 765279a78fe..07d01d5c50e 100644 --- a/spec/support/shared_examples/lib/gitlab/search_results_sorted_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/search_results_sorted_shared_examples.rb @@ -2,7 +2,7 @@ RSpec.shared_examples 'search results sorted' do context 'sort: newest' do - let(:sort) { 'newest' } + let(:sort) { 'created_desc' } it 'sorts results by created_at' do expect(results.objects(scope).map(&:id)).to eq([new_result.id, old_result.id, very_old_result.id]) @@ -10,7 +10,7 @@ RSpec.shared_examples 'search results sorted' do end context 'sort: oldest' do - let(:sort) { 'oldest' } + let(:sort) { 'created_asc' } it 'sorts results by created_at' do expect(results.objects(scope).map(&:id)).to eq([very_old_result.id, old_result.id, new_result.id]) diff --git a/spec/support/shared_examples/lib/gitlab/sidekiq_middleware/strategy_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/sidekiq_middleware/strategy_shared_examples.rb new file mode 100644 index 00000000000..2936bb354cf --- /dev/null +++ b/spec/support/shared_examples/lib/gitlab/sidekiq_middleware/strategy_shared_examples.rb @@ -0,0 +1,139 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'deduplicating jobs when scheduling' do |strategy_name| + let(:fake_duplicate_job) do + instance_double(Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob) + end + + let(:expected_message) { "dropped #{strategy_name.to_s.humanize.downcase}" } + + subject(:strategy) { Gitlab::SidekiqMiddleware::DuplicateJobs::Strategies.for(strategy_name).new(fake_duplicate_job) } + + describe '#schedule' do + before do + allow(Gitlab::SidekiqLogging::DeduplicationLogger.instance).to receive(:log) + end + + it 'checks for duplicates before yielding' do + expect(fake_duplicate_job).to receive(:scheduled?).twice.ordered.and_return(false) + expect(fake_duplicate_job).to( + receive(:check!) + .with(Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob::DUPLICATE_KEY_TTL) + .ordered + .and_return('a jid')) + expect(fake_duplicate_job).to receive(:duplicate?).ordered.and_return(false) + + expect { |b| strategy.schedule({}, &b) }.to yield_control + end + + it 'checks worker options for scheduled jobs' do + expect(fake_duplicate_job).to receive(:scheduled?).ordered.and_return(true) + expect(fake_duplicate_job).to receive(:options).ordered.and_return({}) + expect(fake_duplicate_job).not_to receive(:check!) + + expect { |b| strategy.schedule({}, &b) }.to yield_control + end + + context 'job marking' do + it 'adds the jid of the existing job to the job hash' do + allow(fake_duplicate_job).to receive(:scheduled?).and_return(false) + allow(fake_duplicate_job).to receive(:check!).and_return('the jid') + allow(fake_duplicate_job).to receive(:droppable?).and_return(true) + allow(fake_duplicate_job).to receive(:options).and_return({}) + job_hash = {} + + expect(fake_duplicate_job).to receive(:duplicate?).and_return(true) + expect(fake_duplicate_job).to receive(:existing_jid).and_return('the jid') + + strategy.schedule(job_hash) {} + + expect(job_hash).to include('duplicate-of' => 'the jid') + end + + context 'scheduled jobs' do + let(:time_diff) { 1.minute } + + context 'scheduled in the past' do + it 'adds the jid of the existing job to the job hash' do + allow(fake_duplicate_job).to receive(:scheduled?).twice.and_return(true) + allow(fake_duplicate_job).to receive(:scheduled_at).and_return(Time.now - time_diff) + allow(fake_duplicate_job).to receive(:options).and_return({ including_scheduled: true }) + allow(fake_duplicate_job).to( + receive(:check!) + .with(Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob::DUPLICATE_KEY_TTL) + .and_return('the jid')) + allow(fake_duplicate_job).to receive(:droppable?).and_return(true) + job_hash = {} + + expect(fake_duplicate_job).to receive(:duplicate?).and_return(true) + expect(fake_duplicate_job).to receive(:existing_jid).and_return('the jid') + + strategy.schedule(job_hash) {} + + expect(job_hash).to include('duplicate-of' => 'the jid') + end + end + + context 'scheduled in the future' do + it 'adds the jid of the existing job to the job hash' do + freeze_time do + allow(fake_duplicate_job).to receive(:scheduled?).twice.and_return(true) + allow(fake_duplicate_job).to receive(:scheduled_at).and_return(Time.now + time_diff) + allow(fake_duplicate_job).to receive(:options).and_return({ including_scheduled: true }) + allow(fake_duplicate_job).to( + receive(:check!).with(time_diff.to_i).and_return('the jid')) + allow(fake_duplicate_job).to receive(:droppable?).and_return(true) + job_hash = {} + + expect(fake_duplicate_job).to receive(:duplicate?).and_return(true) + expect(fake_duplicate_job).to receive(:existing_jid).and_return('the jid') + + strategy.schedule(job_hash) {} + + expect(job_hash).to include('duplicate-of' => 'the jid') + end + end + end + end + end + + context "when the job is droppable" do + before do + allow(fake_duplicate_job).to receive(:scheduled?).and_return(false) + allow(fake_duplicate_job).to receive(:check!).and_return('the jid') + allow(fake_duplicate_job).to receive(:duplicate?).and_return(true) + allow(fake_duplicate_job).to receive(:options).and_return({}) + allow(fake_duplicate_job).to receive(:existing_jid).and_return('the jid') + allow(fake_duplicate_job).to receive(:droppable?).and_return(true) + end + + it 'drops the job' do + schedule_result = nil + + expect(fake_duplicate_job).to receive(:droppable?).and_return(true) + + expect { |b| schedule_result = strategy.schedule({}, &b) }.not_to yield_control + expect(schedule_result).to be(false) + end + + it 'logs that the job was dropped' do + fake_logger = instance_double(Gitlab::SidekiqLogging::DeduplicationLogger) + + expect(Gitlab::SidekiqLogging::DeduplicationLogger).to receive(:instance).and_return(fake_logger) + expect(fake_logger).to receive(:log).with(a_hash_including({ 'jid' => 'new jid' }), expected_message, {}) + + strategy.schedule({ 'jid' => 'new jid' }) {} + end + + it 'logs the deduplication options of the worker' do + fake_logger = instance_double(Gitlab::SidekiqLogging::DeduplicationLogger) + + expect(Gitlab::SidekiqLogging::DeduplicationLogger).to receive(:instance).and_return(fake_logger) + allow(fake_duplicate_job).to receive(:options).and_return({ foo: :bar }) + expect(fake_logger).to receive(:log).with(a_hash_including({ 'jid' => 'new jid' }), expected_message, { foo: :bar }) + + strategy.schedule({ 'jid' => 'new jid' }) {} + end + end + end +end diff --git a/spec/support/shared_examples/lib/gitlab/usage_data_counters/issue_activity_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/usage_data_counters/issue_activity_shared_examples.rb new file mode 100644 index 00000000000..286305f2506 --- /dev/null +++ b/spec/support/shared_examples/lib/gitlab/usage_data_counters/issue_activity_shared_examples.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'a tracked issue edit event' do |event| + before do + stub_application_setting(usage_ping_enabled: true) + end + + def count_unique(date_from:, date_to:) + Gitlab::UsageDataCounters::HLLRedisCounter.unique_events(event_names: action, start_date: date_from, end_date: date_to) + end + + specify do + aggregate_failures do + expect(track_action(author: user1)).to be_truthy + expect(track_action(author: user1)).to be_truthy + expect(track_action(author: user2)).to be_truthy + expect(track_action(author: user3, time: time - 3.days)).to be_truthy + + expect(count_unique(date_from: time, date_to: time)).to eq(2) + expect(count_unique(date_from: time - 5.days, date_to: 1.day.since(time))).to eq(3) + end + end + + it 'does not track edit actions if author is not present' do + expect(track_action(author: nil)).to be_nil + end + + context 'when feature flag track_issue_activity_actions is disabled' do + it 'does not track edit actions' do + stub_feature_flags(track_issue_activity_actions: false) + + expect(track_action(author: user1)).to be_nil + end + end +end diff --git a/spec/support/shared_examples/mailers/notify_shared_examples.rb b/spec/support/shared_examples/mailers/notify_shared_examples.rb index 7ce7b2161f6..0143bf693c7 100644 --- a/spec/support/shared_examples/mailers/notify_shared_examples.rb +++ b/spec/support/shared_examples/mailers/notify_shared_examples.rb @@ -273,3 +273,12 @@ RSpec.shared_examples 'no email is sent' do expect(subject.message).to be_a_kind_of(ActionMailer::Base::NullMail) end end + +RSpec.shared_examples 'does not render a manage notifications link' do + it do + aggregate_failures do + expect(subject).not_to have_body_text("Manage all notifications") + expect(subject).not_to have_body_text(profile_notifications_url) + end + end +end diff --git a/spec/support/shared_examples/models/application_setting_shared_examples.rb b/spec/support/shared_examples/models/application_setting_shared_examples.rb index 01513161d24..92fd4363134 100644 --- a/spec/support/shared_examples/models/application_setting_shared_examples.rb +++ b/spec/support/shared_examples/models/application_setting_shared_examples.rb @@ -1,84 +1,84 @@ # frozen_string_literal: true -RSpec.shared_examples 'string of domains' do |attribute| +RSpec.shared_examples 'string of domains' do |mapped_name, attribute| it 'sets single domain' do - setting.method("#{attribute}_raw=").call('example.com') + setting.method("#{mapped_name}_raw=").call('example.com') expect(setting.method(attribute).call).to eq(['example.com']) end it 'sets multiple domains with spaces' do - setting.method("#{attribute}_raw=").call('example.com *.example.com') + setting.method("#{mapped_name}_raw=").call('example.com *.example.com') expect(setting.method(attribute).call).to eq(['example.com', '*.example.com']) end it 'sets multiple domains with newlines and a space' do - setting.method("#{attribute}_raw=").call("example.com\n *.example.com") + setting.method("#{mapped_name}_raw=").call("example.com\n *.example.com") expect(setting.method(attribute).call).to eq(['example.com', '*.example.com']) end it 'sets multiple domains with commas' do - setting.method("#{attribute}_raw=").call("example.com, *.example.com") + setting.method("#{mapped_name}_raw=").call("example.com, *.example.com") expect(setting.method(attribute).call).to eq(['example.com', '*.example.com']) end it 'sets multiple domains with semicolon' do - setting.method("#{attribute}_raw=").call("example.com; *.example.com") + setting.method("#{mapped_name}_raw=").call("example.com; *.example.com") expect(setting.method(attribute).call).to contain_exactly('example.com', '*.example.com') end it 'sets multiple domains with mixture of everything' do - setting.method("#{attribute}_raw=").call("example.com; *.example.com\n test.com\sblock.com yes.com") + setting.method("#{mapped_name}_raw=").call("example.com; *.example.com\n test.com\sblock.com yes.com") expect(setting.method(attribute).call).to contain_exactly('example.com', '*.example.com', 'test.com', 'block.com', 'yes.com') end it 'removes duplicates' do - setting.method("#{attribute}_raw=").call("example.com; example.com; 127.0.0.1; 127.0.0.1") + setting.method("#{mapped_name}_raw=").call("example.com; example.com; 127.0.0.1; 127.0.0.1") expect(setting.method(attribute).call).to contain_exactly('example.com', '127.0.0.1') end it 'does not fail with garbage values' do - setting.method("#{attribute}_raw=").call("example;34543:garbage:fdh5654;") + setting.method("#{mapped_name}_raw=").call("example;34543:garbage:fdh5654;") expect(setting.method(attribute).call).to contain_exactly('example', '34543:garbage:fdh5654') end it 'does not raise error with nil' do - setting.method("#{attribute}_raw=").call(nil) + setting.method("#{mapped_name}_raw=").call(nil) expect(setting.method(attribute).call).to eq([]) end end RSpec.shared_examples 'application settings examples' do context 'restricted signup domains' do - it_behaves_like 'string of domains', :domain_whitelist + it_behaves_like 'string of domains', :domain_allowlist, :domain_allowlist end - context 'blacklisted signup domains' do - it_behaves_like 'string of domains', :domain_blacklist + context 'denied signup domains' do + it_behaves_like 'string of domains', :domain_denylist, :domain_denylist it 'sets multiple domain with file' do - setting.domain_blacklist_file = File.open(Rails.root.join('spec/fixtures/', 'domain_blacklist.txt')) - expect(setting.domain_blacklist).to contain_exactly('example.com', 'test.com', 'foo.bar') + setting.domain_denylist_file = File.open(Rails.root.join('spec/fixtures/', 'domain_denylist.txt')) + expect(setting.domain_denylist).to contain_exactly('example.com', 'test.com', 'foo.bar') end end context 'outbound_local_requests_whitelist' do - it_behaves_like 'string of domains', :outbound_local_requests_whitelist + it_behaves_like 'string of domains', :outbound_local_requests_allowlist, :outbound_local_requests_whitelist - it 'clears outbound_local_requests_whitelist_arrays memoization' do - setting.outbound_local_requests_whitelist_raw = 'example.com' + it 'clears outbound_local_requests_allowlist_arrays memoization' do + setting.outbound_local_requests_allowlist_raw = 'example.com' - expect(setting.outbound_local_requests_whitelist_arrays).to contain_exactly( + expect(setting.outbound_local_requests_allowlist_arrays).to contain_exactly( [], [an_object_having_attributes(domain: 'example.com')] ) - setting.outbound_local_requests_whitelist_raw = 'gitlab.com' - expect(setting.outbound_local_requests_whitelist_arrays).to contain_exactly( + setting.outbound_local_requests_allowlist_raw = 'gitlab.com' + expect(setting.outbound_local_requests_allowlist_arrays).to contain_exactly( [], [an_object_having_attributes(domain: 'gitlab.com')] ) end end - context 'outbound_local_requests_whitelist_arrays' do + context 'outbound_local_requests_allowlist_arrays' do it 'separates the IPs and domains' do setting.outbound_local_requests_whitelist = [ '192.168.1.1', @@ -118,7 +118,7 @@ RSpec.shared_examples 'application settings examples' do an_object_having_attributes(domain: 'example.com', port: 8080) ] - expect(setting.outbound_local_requests_whitelist_arrays).to contain_exactly( + expect(setting.outbound_local_requests_allowlist_arrays).to contain_exactly( ip_whitelist, domain_whitelist ) end @@ -139,10 +139,10 @@ RSpec.shared_examples 'application settings examples' do ) end - it 'clears outbound_local_requests_whitelist_arrays memoization' do + it 'clears outbound_local_requests_allowlist_arrays memoization' do setting.outbound_local_requests_whitelist = ['example.com'] - expect(setting.outbound_local_requests_whitelist_arrays).to contain_exactly( + expect(setting.outbound_local_requests_allowlist_arrays).to contain_exactly( [], [an_object_having_attributes(domain: 'example.com')] ) @@ -151,7 +151,7 @@ RSpec.shared_examples 'application settings examples' do ['example.com', 'gitlab.com'] ) - expect(setting.outbound_local_requests_whitelist_arrays).to contain_exactly( + expect(setting.outbound_local_requests_allowlist_arrays).to contain_exactly( [], [an_object_having_attributes(domain: 'example.com'), an_object_having_attributes(domain: 'gitlab.com')] ) @@ -163,7 +163,7 @@ RSpec.shared_examples 'application settings examples' do setting.add_to_outbound_local_requests_whitelist(['gitlab.com']) expect(setting.outbound_local_requests_whitelist).to contain_exactly('gitlab.com') - expect(setting.outbound_local_requests_whitelist_arrays).to contain_exactly( + expect(setting.outbound_local_requests_allowlist_arrays).to contain_exactly( [], [an_object_having_attributes(domain: 'gitlab.com')] ) end @@ -171,7 +171,7 @@ RSpec.shared_examples 'application settings examples' do it 'does not raise error with nil' do setting.outbound_local_requests_whitelist = nil - expect(setting.outbound_local_requests_whitelist_arrays).to contain_exactly([], []) + expect(setting.outbound_local_requests_allowlist_arrays).to contain_exactly([], []) end end diff --git a/spec/support/shared_examples/models/atomic_internal_id_shared_examples.rb b/spec/support/shared_examples/models/atomic_internal_id_shared_examples.rb index 62d56f2e86e..fe99b1cacd9 100644 --- a/spec/support/shared_examples/models/atomic_internal_id_shared_examples.rb +++ b/spec/support/shared_examples/models/atomic_internal_id_shared_examples.rb @@ -76,6 +76,26 @@ RSpec.shared_examples 'AtomicInternalId' do |validate_presence: true| end end + describe 'supply of internal ids' do + let(:scope_value) { scope_attrs.each_value.first } + let(:method_name) { :"with_#{scope}_#{internal_id_attribute}_supply" } + + it 'provides a persistent supply of IID values, sensitive to the current state' do + iid = rand(1..1000) + write_internal_id(iid) + instance.public_send(:"track_#{scope}_#{internal_id_attribute}!") + + # Allocate 3 IID values + described_class.public_send(method_name, scope_value) do |supply| + 3.times { supply.next_value } + end + + current_value = described_class.public_send(method_name, scope_value, &:current_value) + + expect(current_value).to eq(iid + 3) + end + end + describe "#reset_scope_internal_id_attribute" do it 'rewinds the allocated IID' do expect { ensure_scope_attribute! }.not_to raise_error diff --git a/spec/support/shared_examples/models/cluster_application_core_shared_examples.rb b/spec/support/shared_examples/models/cluster_application_core_shared_examples.rb index 85a7c90ee42..51071ae47c3 100644 --- a/spec/support/shared_examples/models/cluster_application_core_shared_examples.rb +++ b/spec/support/shared_examples/models/cluster_application_core_shared_examples.rb @@ -25,4 +25,21 @@ RSpec.shared_examples 'cluster application core specs' do |application_name| describe '.association_name' do it { expect(described_class.association_name).to eq(:"application_#{subject.name}") } end + + describe '#helm_command_module' do + using RSpec::Parameterized::TableSyntax + + where(:helm_major_version, :expected_helm_command_module) do + 2 | Gitlab::Kubernetes::Helm::V2 + 3 | Gitlab::Kubernetes::Helm::V3 + end + + with_them do + subject { described_class.new(cluster: cluster).helm_command_module } + + let(:cluster) { build(:cluster, helm_major_version: helm_major_version)} + + it { is_expected.to eq(expected_helm_command_module) } + end + end end diff --git a/spec/support/shared_examples/models/cluster_application_helm_cert_shared_examples.rb b/spec/support/shared_examples/models/cluster_application_helm_cert_shared_examples.rb index ac8022a4726..187a44ec3cd 100644 --- a/spec/support/shared_examples/models/cluster_application_helm_cert_shared_examples.rb +++ b/spec/support/shared_examples/models/cluster_application_helm_cert_shared_examples.rb @@ -6,7 +6,7 @@ RSpec.shared_examples 'cluster application helm specs' do |application_name| describe '#uninstall_command' do subject { application.uninstall_command } - it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::Helm::DeleteCommand) } + it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::Helm::V3::DeleteCommand) } it 'has files' do expect(subject.files).to eq(application.files) 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 8092f87383d..17948d648cb 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 @@ -1,6 +1,6 @@ # frozen_string_literal: true -RSpec.shared_examples 'cycle analytics stage' do +RSpec.shared_examples 'value stream analytics stage' do let(:valid_params) do { name: 'My Stage', @@ -111,7 +111,7 @@ RSpec.shared_examples 'cycle analytics stage' do end end -RSpec.shared_examples 'cycle analytics label based stage' do +RSpec.shared_examples 'value stream analytics label based stage' do context 'when creating label based event' do context 'when the label id is not passed' do it 'returns validation error when `start_event_label_id` is missing' do diff --git a/spec/support/shared_examples/quick_actions/issuable/time_tracking_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/time_tracking_quick_action_shared_examples.rb index 37ee2548dfe..17fd2b836d3 100644 --- a/spec/support/shared_examples/quick_actions/issuable/time_tracking_quick_action_shared_examples.rb +++ b/spec/support/shared_examples/quick_actions/issuable/time_tracking_quick_action_shared_examples.rb @@ -13,7 +13,7 @@ RSpec.shared_examples 'issuable time tracker' do |issuable_type| end it 'renders the sidebar component empty state' do - page.within '.time-tracking-no-tracking-pane' do + page.within '[data-testid="noTrackingPane"]' do expect(page).to have_content 'No estimate or time spent' end end @@ -22,7 +22,7 @@ RSpec.shared_examples 'issuable time tracker' do |issuable_type| submit_time('/estimate 3w 1d 1h') wait_for_requests - page.within '.time-tracking-estimate-only-pane' do + page.within '[data-testid="estimateOnlyPane"]' do expect(page).to have_content '3w 1d 1h' end end @@ -31,7 +31,7 @@ RSpec.shared_examples 'issuable time tracker' do |issuable_type| submit_time('/spend 3w 1d 1h') wait_for_requests - page.within '.time-tracking-spend-only-pane' do + page.within '[data-testid="spentOnlyPane"]' do expect(page).to have_content '3w 1d 1h' end end @@ -41,7 +41,7 @@ RSpec.shared_examples 'issuable time tracker' do |issuable_type| submit_time('/spend 3w 1d 1h') wait_for_requests - page.within '.time-tracking-comparison-pane' do + page.within '[data-testid="timeTrackingComparisonPane"]' do expect(page).to have_content '3w 1d 1h' end end diff --git a/spec/support/shared_examples/read_only_message_shared_examples.rb b/spec/support/shared_examples/read_only_message_shared_examples.rb new file mode 100644 index 00000000000..4ae97ea7748 --- /dev/null +++ b/spec/support/shared_examples/read_only_message_shared_examples.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'Read-only instance' do |message| + it 'shows read-only banner' do + visit root_dashboard_path + + expect(page).to have_content(message) + end +end + +RSpec.shared_examples 'Read-write instance' do |message| + it 'does not show read-only banner' do + visit root_dashboard_path + + expect(page).not_to have_content(message) + end +end 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 ec32cb4b2ff..f55043fe64f 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 @@ -20,7 +20,7 @@ RSpec.shared_context 'Debian repository shared context' do |object_type| let(:source_package) { 'sample' } let(:letter) { source_package[0..2] == 'lib' ? source_package[0..3] : source_package[0] } let(:package_name) { 'libsample0' } - let(:package_version) { '1.2.3~alpha2-1' } + let(:package_version) { '1.2.3~alpha2' } let(:file_name) { "#{package_name}_#{package_version}_#{architecture}.deb" } let(:method) { :get } 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 6315c10b0c4..a12cb24a513 100644 --- a/spec/support/shared_examples/requests/api/discussions_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/discussions_shared_examples.rb @@ -117,15 +117,10 @@ RSpec.shared_examples 'discussions API' do |parent_type, noteable_type, id_name, expect(response).to have_gitlab_http_status(:unauthorized) end - it 'tracks a Notes::CreateService event' do - expect(Gitlab::Tracking).to receive(:event) do |category, action, data| - expect(category).to eq('Notes::CreateService') - expect(action).to eq('execute') - expect(data[:label]).to eq('note') - expect(data[:value]).to be_an(Integer) - end - + it 'tracks a Notes::CreateService event', :snowplow do post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/discussions", user), params: { body: 'hi!' } + + expect_snowplow_event(category: 'Notes::CreateService', action: 'execute', label: 'note', value: anything) end context 'with notes_create_service_tracking feature flag disabled' do @@ -133,10 +128,10 @@ RSpec.shared_examples 'discussions API' do |parent_type, noteable_type, id_name, stub_feature_flags(notes_create_service_tracking: false) end - it 'does not track any events' do - expect(Gitlab::Tracking).not_to receive(:event) - + it 'does not track any events', :snowplow do post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/discussions"), params: { body: 'hi!' } + + expect_no_snowplow_event end end diff --git a/spec/support/shared_examples/requests/api/graphql/read_only_instance_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/read_only_instance_shared_examples.rb new file mode 100644 index 00000000000..be163d6aa0e --- /dev/null +++ b/spec/support/shared_examples/requests/api/graphql/read_only_instance_shared_examples.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'graphql on a read-only GitLab instance' do + include GraphqlHelpers + + context 'mutations' do + let(:current_user) { note.author } + let!(:note) { create(:note) } + + let(:mutation) do + variables = { + id: GitlabSchema.id_from_object(note).to_s + } + + graphql_mutation(:destroy_note, variables) + end + + it 'disallows the query' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(json_response['errors'].first['message']).to eq(Mutations::BaseMutation::ERROR_MESSAGE) + end + + it 'does not destroy the Note' do + expect do + post_graphql_mutation(mutation, current_user: current_user) + end.not_to change { Note.count } + end + end + + context 'read-only queries' do + let(:current_user) { create(:user) } + let(:project) { create(:project, :repository) } + + before do + project.add_developer(current_user) + end + + it 'allows the query' do + query = graphql_query_for('project', 'fullPath' => project.full_path) + + post_graphql(query, current_user: current_user) + + expect(graphql_data['project']).not_to be_nil + end + end +end diff --git a/spec/support/shared_examples/requests/api/labels_api_shared_examples.rb b/spec/support/shared_examples/requests/api/labels_api_shared_examples.rb new file mode 100644 index 00000000000..02e50b789cc --- /dev/null +++ b/spec/support/shared_examples/requests/api/labels_api_shared_examples.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'fetches labels' do + it 'returns correct labels' do + request + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response).to all(match_schema('public_api/v4/labels/label')) + expect(json_response.size).to eq(expected_labels.size) + expect(json_response.map {|r| r['name'] }).to match_array(expected_labels) + end +end diff --git a/spec/support/shared_examples/requests/api/multiple_and_scoped_issue_boards_shared_examples.rb b/spec/support/shared_examples/requests/api/multiple_and_scoped_issue_boards_shared_examples.rb new file mode 100644 index 00000000000..54aa9d47dd8 --- /dev/null +++ b/spec/support/shared_examples/requests/api/multiple_and_scoped_issue_boards_shared_examples.rb @@ -0,0 +1,94 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'multiple and scoped issue boards' do |route_definition| + let(:root_url) { route_definition.gsub(":id", board_parent.id.to_s) } + + context 'multiple issue boards' do + before do + board_parent.add_reporter(user) + stub_licensed_features(multiple_group_issue_boards: true) + end + + describe "POST #{route_definition}" do + it 'creates a board' do + post api(root_url, user), params: { name: "new board" } + + expect(response).to have_gitlab_http_status(:created) + + expect(response).to match_response_schema('public_api/v4/board', dir: "ee") + end + end + + describe "PUT #{route_definition}/:board_id" do + let(:url) { "#{root_url}/#{board.id}" } + + it 'updates a board' do + put api(url, user), params: { name: 'new name', weight: 4, labels: 'foo, bar' } + + expect(response).to have_gitlab_http_status(:ok) + + expect(response).to match_response_schema('public_api/v4/board', dir: "ee") + + board.reload + + expect(board.name).to eq('new name') + expect(board.weight).to eq(4) + expect(board.labels.map(&:title)).to contain_exactly('foo', 'bar') + end + + it 'does not remove missing attributes from the board' do + expect { put api(url, user), params: { name: 'new name' } } + .to not_change { board.reload.assignee } + .and not_change { board.reload.milestone } + .and not_change { board.reload.weight } + .and not_change { board.reload.labels.map(&:title).sort } + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('public_api/v4/board', dir: "ee") + end + + it 'allows removing optional attributes' do + put api(url, user), params: { name: 'new name', assignee_id: nil, milestone_id: nil, weight: nil, labels: nil } + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('public_api/v4/board', dir: "ee") + + board.reload + + expect(board.name).to eq('new name') + expect(board.assignee).to be_nil + expect(board.milestone).to be_nil + expect(board.weight).to be_nil + expect(board.labels).to be_empty + end + end + + describe "DELETE #{route_definition}/:board_id" do + let(:url) { "#{root_url}/#{board.id}" } + + it 'deletes a board' do + delete api(url, user) + + expect(response).to have_gitlab_http_status(:no_content) + end + end + end + + context 'with the scoped_issue_board-feature available' do + it 'returns the milestone when the `scoped_issue_board` feature is enabled' do + stub_licensed_features(scoped_issue_board: true) + + get api(root_url, user) + + expect(json_response.first["milestone"]).not_to be_nil + end + + it 'hides the milestone when the `scoped_issue_board` feature is disabled' do + stub_licensed_features(scoped_issue_board: false) + + get api(root_url, user) + + expect(json_response.first["milestone"]).to be_nil + end + end +end diff --git a/spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb new file mode 100644 index 00000000000..d3ad7aa0595 --- /dev/null +++ b/spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb @@ -0,0 +1,270 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'handling get metadata requests' do + let_it_be(:package_dependency_link1) { create(:packages_dependency_link, package: package, dependency_type: :dependencies) } + let_it_be(:package_dependency_link2) { create(:packages_dependency_link, package: package, dependency_type: :devDependencies) } + let_it_be(:package_dependency_link3) { create(:packages_dependency_link, package: package, dependency_type: :bundleDependencies) } + let_it_be(:package_dependency_link4) { create(:packages_dependency_link, package: package, dependency_type: :peerDependencies) } + + let(:params) { {} } + let(:headers) { {} } + + subject { get(url, params: params, headers: headers) } + + shared_examples 'returning the npm package info' do + it 'returns the package info' do + subject + + expect_a_valid_package_response + end + end + + shared_examples 'a package that requires auth' do + it 'denies request without oauth token' do + subject + + expect(response).to have_gitlab_http_status(:not_found) + end + + context 'with oauth token' do + let(:params) { { access_token: token.token } } + + it 'returns the package info with oauth token' do + subject + + expect_a_valid_package_response + end + end + + context 'with job token' do + let(:params) { { job_token: job.token } } + + it 'returns the package info with running job token' do + subject + + expect_a_valid_package_response + end + + it 'denies request without running job token' do + job.update!(status: :success) + + subject + + expect(response).to have_gitlab_http_status(:unauthorized) + end + end + + context 'with deploy token' do + let(:headers) { build_token_auth_header(deploy_token.token) } + + it 'returns the package info with deploy token' do + subject + + expect_a_valid_package_response + end + end + end + + context 'a public project' do + it_behaves_like 'returning the npm package info' + + context 'project path with a dot' do + before do + project.update!(path: 'foo.bar') + end + + it_behaves_like 'returning the npm package info' + end + + context 'with request forward disabled' do + before do + stub_application_setting(npm_package_requests_forwarding: false) + end + + it_behaves_like 'returning the npm package info' + + context 'with unknown package' do + let(:package_name) { 'unknown' } + + it 'returns the proper response' do + subject + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end + + context 'with request forward enabled' do + before do + stub_application_setting(npm_package_requests_forwarding: true) + end + + it_behaves_like 'returning the npm package info' + + context 'with unknown package' do + let(:package_name) { 'unknown' } + + it 'returns a redirect' do + subject + + expect(response).to have_gitlab_http_status(:found) + expect(response.headers['Location']).to eq('https://registry.npmjs.org/unknown') + end + + it_behaves_like 'a gitlab tracking event', described_class.name, 'npm_request_forward' + end + end + end + + context 'internal project' do + before do + project.update!(visibility_level: Gitlab::VisibilityLevel::INTERNAL) + end + + it_behaves_like 'a package that requires auth' + end + + context 'private project' do + before do + project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE) + end + + it_behaves_like 'a package that requires auth' + + context 'with guest' do + let(:params) { { access_token: token.token } } + + it 'denies request when not enough permissions' do + project.add_guest(user) + + subject + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + end + + def expect_a_valid_package_response + expect(response).to have_gitlab_http_status(:ok) + expect(response.media_type).to eq('application/json') + expect(response).to match_response_schema('public_api/v4/packages/npm_package') + expect(json_response['name']).to eq(package.name) + expect(json_response['versions'][package.version]).to match_schema('public_api/v4/packages/npm_package_version') + ::Packages::Npm::PackagePresenter::NPM_VALID_DEPENDENCY_TYPES.each do |dependency_type| + expect(json_response.dig('versions', package.version, dependency_type.to_s)).to be_any + end + expect(json_response['dist-tags']).to match_schema('public_api/v4/packages/npm_package_tags') + end +end + +RSpec.shared_examples 'handling get dist tags requests' do + let_it_be(:package_tag1) { create(:packages_tag, package: package) } + let_it_be(:package_tag2) { create(:packages_tag, package: package) } + + let(:params) { {} } + + subject { get(url, params: params) } + + context 'with public project' do + context 'with authenticated user' do + let(:params) { { private_token: personal_access_token.token } } + + it_behaves_like 'returns package tags', :maintainer + it_behaves_like 'returns package tags', :developer + it_behaves_like 'returns package tags', :reporter + it_behaves_like 'returns package tags', :guest + end + + context 'with unauthenticated user' do + it_behaves_like 'returns package tags', :no_type + end + end + + context 'with private project' do + before do + project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE) + end + + context 'with authenticated user' do + let(:params) { { private_token: personal_access_token.token } } + + it_behaves_like 'returns package tags', :maintainer + it_behaves_like 'returns package tags', :developer + it_behaves_like 'returns package tags', :reporter + it_behaves_like 'rejects package tags access', :guest, :forbidden + end + + context 'with unauthenticated user' do + it_behaves_like 'rejects package tags access', :no_type, :not_found + end + end +end + +RSpec.shared_examples 'handling create dist tag requests' do + let_it_be(:tag_name) { 'test' } + + let(:params) { {} } + let(:env) { {} } + let(:version) { package.version } + + subject { put(url, env: env, params: params) } + + context 'with public project' do + context 'with authenticated user' do + let(:params) { { private_token: personal_access_token.token } } + let(:env) { { 'api.request.body': version } } + + it_behaves_like 'create package tag', :maintainer + it_behaves_like 'create package tag', :developer + it_behaves_like 'rejects package tags access', :reporter, :forbidden + it_behaves_like 'rejects package tags access', :guest, :forbidden + end + + context 'with unauthenticated user' do + it_behaves_like 'rejects package tags access', :no_type, :unauthorized + end + end +end + +RSpec.shared_examples 'handling delete dist tag requests' do + let_it_be(:package_tag) { create(:packages_tag, package: package) } + + let(:params) { {} } + let(:tag_name) { package_tag.name } + + subject { delete(url, params: params) } + + context 'with public project' do + context 'with authenticated user' do + let(:params) { { private_token: personal_access_token.token } } + + it_behaves_like 'delete package tag', :maintainer + it_behaves_like 'rejects package tags access', :developer, :forbidden + it_behaves_like 'rejects package tags access', :reporter, :forbidden + it_behaves_like 'rejects package tags access', :guest, :forbidden + end + + context 'with unauthenticated user' do + it_behaves_like 'rejects package tags access', :no_type, :unauthorized + end + end + + context 'with private project' do + before do + project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE) + end + + context 'with authenticated user' do + let(:params) { { private_token: personal_access_token.token } } + + it_behaves_like 'delete package tag', :maintainer + it_behaves_like 'rejects package tags access', :developer, :forbidden + it_behaves_like 'rejects package tags access', :reporter, :forbidden + it_behaves_like 'rejects package tags access', :guest, :forbidden + end + + context 'with unauthenticated user' do + it_behaves_like 'rejects package tags access', :no_type, :unauthorized + end + end +end diff --git a/spec/support/shared_examples/requests/api/packages_shared_examples.rb b/spec/support/shared_examples/requests/api/packages_shared_examples.rb index d730ed53109..3833604e304 100644 --- a/spec/support/shared_examples/requests/api/packages_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/packages_shared_examples.rb @@ -128,9 +128,13 @@ RSpec.shared_examples 'job token for package uploads' do end RSpec.shared_examples 'a package tracking event' do |category, action| - it "creates a gitlab tracking event #{action}" do - expect(Gitlab::Tracking).to receive(:event).with(category, action, {}) + before do + stub_feature_flags(collect_package_events: true) + end + it "creates a gitlab tracking event #{action}", :snowplow do expect { subject }.to change { Packages::Event.count }.by(1) + + expect_snowplow_event(category: category, action: action) end end diff --git a/spec/support/shared_examples/requests/api/packages_tags_shared_examples.rb b/spec/support/shared_examples/requests/api/packages_tags_shared_examples.rb index a371d380f47..2c203dc096e 100644 --- a/spec/support/shared_examples/requests/api/packages_tags_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/packages_tags_shared_examples.rb @@ -40,7 +40,7 @@ RSpec.shared_examples 'returns package tags' do |user_type| context 'with invalid package name' do where(:package_name, :status) do '%20' | :bad_request - nil | :forbidden + nil | :not_found end with_them do @@ -95,7 +95,7 @@ RSpec.shared_examples 'create package tag' do |user_type| context 'with invalid package name' do where(:package_name, :status) do - 'unknown' | :forbidden + 'unknown' | :not_found '' | :not_found '%20' | :bad_request end @@ -160,7 +160,7 @@ RSpec.shared_examples 'delete package tag' do |user_type| context 'with invalid package name' do where(:package_name, :status) do - 'unknown' | :forbidden + 'unknown' | :not_found '' | :not_found '%20' | :bad_request end diff --git a/spec/support/shared_examples/requests/api/tracking_shared_examples.rb b/spec/support/shared_examples/requests/api/tracking_shared_examples.rb index 2e6feae3f98..826139635ed 100644 --- a/spec/support/shared_examples/requests/api/tracking_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/tracking_shared_examples.rb @@ -1,9 +1,9 @@ # frozen_string_literal: true RSpec.shared_examples 'a gitlab tracking event' do |category, action| - it "creates a gitlab tracking event #{action}" do - expect(Gitlab::Tracking).to receive(:event).with(category, action, {}) - + it "creates a gitlab tracking event #{action}", :snowplow do subject + + expect_snowplow_event(category: category, action: action) end end diff --git a/spec/support/shared_examples/requests/lfs_http_shared_examples.rb b/spec/support/shared_examples/requests/lfs_http_shared_examples.rb index 48c5a5933e6..4ae77179527 100644 --- a/spec/support/shared_examples/requests/lfs_http_shared_examples.rb +++ b/spec/support/shared_examples/requests/lfs_http_shared_examples.rb @@ -2,42 +2,252 @@ RSpec.shared_examples 'LFS http 200 response' do it_behaves_like 'LFS http expected response code and message' do - let(:response_code) { 200 } + let(:response_code) { :ok } + end +end + +RSpec.shared_examples 'LFS http 200 blob response' do + it_behaves_like 'LFS http expected response code and message' do + let(:response_code) { :ok } + let(:content_type) { Repositories::LfsApiController::LFS_TRANSFER_CONTENT_TYPE } + let(:response_headers) { { 'X-Sendfile' => lfs_object.file.path } } + end +end + +RSpec.shared_examples 'LFS http 200 workhorse response' do + it_behaves_like 'LFS http expected response code and message' do + let(:response_code) { :ok } + let(:content_type) { Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE } end end RSpec.shared_examples 'LFS http 401 response' do it_behaves_like 'LFS http expected response code and message' do - let(:response_code) { 401 } + let(:response_code) { :unauthorized } + let(:content_type) { 'text/plain' } end end RSpec.shared_examples 'LFS http 403 response' do it_behaves_like 'LFS http expected response code and message' do - let(:response_code) { 403 } + let(:response_code) { :forbidden } let(:message) { 'Access forbidden. Check your access level.' } end end RSpec.shared_examples 'LFS http 501 response' do it_behaves_like 'LFS http expected response code and message' do - let(:response_code) { 501 } + let(:response_code) { :not_implemented } let(:message) { 'Git LFS is not enabled on this GitLab server, contact your admin.' } end end RSpec.shared_examples 'LFS http 404 response' do it_behaves_like 'LFS http expected response code and message' do - let(:response_code) { 404 } + let(:response_code) { :not_found } end end RSpec.shared_examples 'LFS http expected response code and message' do let(:response_code) { } - let(:message) { } + let(:response_headers) { {} } + let(:content_type) { LfsRequest::CONTENT_TYPE } + let(:message) {} - it 'responds with the expected response code and message' do + specify do expect(response).to have_gitlab_http_status(response_code) + expect(response.headers.to_hash).to include(response_headers) + expect(response.media_type).to match(content_type) expect(json_response['message']).to eq(message) if message end end + +RSpec.shared_examples 'LFS http requests' do + include LfsHttpHelpers + + let(:authorize_guest) {} + let(:authorize_download) {} + let(:authorize_upload) {} + + let(:lfs_object) { create(:lfs_object, :with_file) } + let(:sample_oid) { lfs_object.oid } + + let(:authorization) { authorize_user } + let(:headers) do + { + 'Authorization' => authorization, + 'X-Sendfile-Type' => 'X-Sendfile' + } + end + + let(:request_download) do + get objects_url(container, sample_oid), params: {}, headers: headers + end + + let(:request_upload) do + post_lfs_json batch_url(container), upload_body(multiple_objects), headers + end + + before do + stub_lfs_setting(enabled: true) + end + + context 'when LFS is disabled globally' do + before do + stub_lfs_setting(enabled: false) + end + + describe 'download request' do + before do + request_download + end + + it_behaves_like 'LFS http 501 response' + end + + describe 'upload request' do + before do + request_upload + end + + it_behaves_like 'LFS http 501 response' + end + end + + context 'unauthenticated' do + let(:headers) { {} } + + describe 'download request' do + before do + request_download + end + + it_behaves_like 'LFS http 401 response' + end + + describe 'upload request' do + before do + request_upload + end + + it_behaves_like 'LFS http 401 response' + end + end + + context 'without access' do + describe 'download request' do + before do + request_download + end + + it_behaves_like 'LFS http 404 response' + end + + describe 'upload request' do + before do + request_upload + end + + it_behaves_like 'LFS http 404 response' + end + end + + context 'with guest access' do + before do + authorize_guest + end + + describe 'download request' do + before do + request_download + end + + it_behaves_like 'LFS http 404 response' + end + + describe 'upload request' do + before do + request_upload + end + + it_behaves_like 'LFS http 404 response' + end + end + + context 'with download permission' do + before do + authorize_download + end + + describe 'download request' do + before do + request_download + end + + it_behaves_like 'LFS http 200 blob response' + + context 'when container does not exist' do + def objects_url(*args) + super.sub(container.full_path, 'missing/path') + end + + it_behaves_like 'LFS http 404 response' + end + end + + describe 'upload request' do + before do + request_upload + end + + it_behaves_like 'LFS http 403 response' + end + end + + context 'with upload permission' do + before do + authorize_upload + end + + describe 'upload request' do + before do + request_upload + end + + it_behaves_like 'LFS http 200 response' + end + end + + describe 'deprecated API' do + shared_examples 'deprecated request' do + before do + request + end + + it_behaves_like 'LFS http expected response code and message' do + let(:response_code) { 501 } + let(:message) { 'Server supports batch API only, please update your Git LFS client to version 1.0.1 and up.' } + end + end + + context 'when fetching LFS object using deprecated API' do + subject(:request) do + get deprecated_objects_url(container, sample_oid), params: {}, headers: headers + end + + it_behaves_like 'deprecated request' + end + + context 'when handling LFS request using deprecated API' do + subject(:request) do + post_lfs_json deprecated_objects_url(container), nil, headers + end + + it_behaves_like 'deprecated request' + end + + def deprecated_objects_url(container, oid = nil) + File.join(["#{container.http_url_to_repo}/info/lfs/objects/", oid].compact) + end + end +end diff --git a/spec/support/shared_examples/requests/rack_attack_shared_examples.rb b/spec/support/shared_examples/requests/rack_attack_shared_examples.rb index 730df4dc5ab..d4ee68309ff 100644 --- a/spec/support/shared_examples/requests/rack_attack_shared_examples.rb +++ b/spec/support/shared_examples/requests/rack_attack_shared_examples.rb @@ -81,8 +81,15 @@ RSpec.shared_examples 'rate-limited token-authenticated requests' do end it 'logs RackAttack info into structured logs' do - requests_per_period.times do - make_request(request_args) + control_count = 0 + + requests_per_period.times do |i| + if i == 0 + control_count = ActiveRecord::QueryRecorder.new { make_request(request_args) }.count + else + make_request(request_args) + end + expect(response).not_to have_gitlab_http_status(:too_many_requests) end @@ -93,13 +100,15 @@ RSpec.shared_examples 'rate-limited token-authenticated requests' do request_method: request_method, path: request_args.first, user_id: user.id, - username: user.username, - throttle_type: throttle_types[throttle_setting_prefix] + 'meta.user' => user.username, + matched: throttle_types[throttle_setting_prefix] } expect(Gitlab::AuthLogger).to receive(:error).with(arguments).once - expect_rejection { make_request(request_args) } + expect_rejection do + expect { make_request(request_args) }.not_to exceed_query_limit(control_count) + end end end @@ -210,8 +219,15 @@ RSpec.shared_examples 'rate-limited web authenticated requests' do end it 'logs RackAttack info into structured logs' do - requests_per_period.times do - request_authenticated_web_url + control_count = 0 + + requests_per_period.times do |i| + if i == 0 + control_count = ActiveRecord::QueryRecorder.new { request_authenticated_web_url }.count + else + request_authenticated_web_url + end + expect(response).not_to have_gitlab_http_status(:too_many_requests) end @@ -222,13 +238,12 @@ RSpec.shared_examples 'rate-limited web authenticated requests' do request_method: request_method, path: url_that_requires_authentication, user_id: user.id, - username: user.username, - throttle_type: throttle_types[throttle_setting_prefix] + 'meta.user' => user.username, + matched: throttle_types[throttle_setting_prefix] } expect(Gitlab::AuthLogger).to receive(:error).with(arguments).once - - request_authenticated_web_url + expect { request_authenticated_web_url }.not_to exceed_query_limit(control_count) end end diff --git a/spec/support/shared_examples/serializers/note_entity_shared_examples.rb b/spec/support/shared_examples/serializers/note_entity_shared_examples.rb index a90a2dc3667..9af6ec45e49 100644 --- a/spec/support/shared_examples/serializers/note_entity_shared_examples.rb +++ b/spec/support/shared_examples/serializers/note_entity_shared_examples.rb @@ -5,8 +5,21 @@ RSpec.shared_examples 'note entity' do context 'basic note' do it 'exposes correct elements' do - expect(subject).to include(:type, :author, :note, :note_html, :current_user, :discussion_id, - :emoji_awardable, :award_emoji, :report_abuse_path, :attachment, :noteable_note_url, :resolvable) + expect(subject).to include( + :attachment, + :author, + :award_emoji, + :base_discussion, + :current_user, + :discussion_id, + :emoji_awardable, + :note, + :note_html, + :noteable_note_url, + :report_abuse_path, + :resolvable, + :type + ) end it 'does not expose elements for specific notes cases' do @@ -20,6 +33,39 @@ RSpec.shared_examples 'note entity' do it 'does not expose web_url for author' do expect(subject[:author]).not_to include(:web_url) end + + it 'exposes permission fields on current_user' do + expect(subject[:current_user]).to include(:can_edit, :can_award_emoji, :can_resolve, :can_resolve_discussion) + end + + describe ':can_resolve_discussion' do + context 'discussion is resolvable' do + before do + expect(note.discussion).to receive(:resolvable?).and_return(true) + end + + context 'user can resolve' do + it 'is true' do + expect(note.discussion).to receive(:can_resolve?).with(user).and_return(true) + expect(subject[:current_user][:can_resolve_discussion]).to be_truthy + end + end + + context 'user cannot resolve' do + it 'is false' do + expect(note.discussion).to receive(:can_resolve?).with(user).and_return(false) + expect(subject[:current_user][:can_resolve_discussion]).to be_falsey + end + end + end + + context 'discussion is not resolvable' do + it 'is false' do + expect(note.discussion).to receive(:resolvable?).and_return(false) + expect(subject[:current_user][:can_resolve_discussion]).to be_falsey + end + end + end end context 'when note was edited' do diff --git a/spec/support/shared_examples/services/alert_management_shared_examples.rb b/spec/support/shared_examples/services/alert_management_shared_examples.rb index 1ae74979b7a..003705ca21c 100644 --- a/spec/support/shared_examples/services/alert_management_shared_examples.rb +++ b/spec/support/shared_examples/services/alert_management_shared_examples.rb @@ -8,11 +8,11 @@ RSpec.shared_examples 'creates an alert management alert' do end it 'executes the alert service hooks' do - slack_service = create(:service, type: 'SlackService', project: project, alert_events: true, active: true) + expect_next_instance_of(AlertManagement::Alert) do |alert| + expect(alert).to receive(:execute_services) + end subject - - expect(ProjectServiceWorker).to have_received(:perform_async).with(slack_service.id, an_instance_of(Hash)) end end diff --git a/spec/support/shared_examples/services/common_system_notes_shared_examples.rb b/spec/support/shared_examples/services/common_system_notes_shared_examples.rb index 5b95a5753a1..7b277d4bede 100644 --- a/spec/support/shared_examples/services/common_system_notes_shared_examples.rb +++ b/spec/support/shared_examples/services/common_system_notes_shared_examples.rb @@ -17,13 +17,13 @@ RSpec.shared_examples 'system note creation' do |update_params, note_text| end end -RSpec.shared_examples 'draft notes creation' do |wip_action| +RSpec.shared_examples 'draft notes creation' do |action| subject { described_class.new(project, user).execute(issuable, old_labels: []) } it 'creates Draft toggle and title change notes' do expect { subject }.to change { Note.count }.from(0).to(2) - expect(Note.first.note).to match("#{wip_action} as a **Work In Progress**") + expect(Note.first.note).to match("marked this merge request as **#{action}**") expect(Note.second.note).to match('changed title') end end diff --git a/spec/support/shared_examples/services/jira_import/user_mapper_services_shared_examples.rb b/spec/support/shared_examples/services/jira_import/user_mapper_services_shared_examples.rb index 7fc7ff8a8de..cbe5c7d89db 100644 --- a/spec/support/shared_examples/services/jira_import/user_mapper_services_shared_examples.rb +++ b/spec/support/shared_examples/services/jira_import/user_mapper_services_shared_examples.rb @@ -3,7 +3,6 @@ RSpec.shared_examples 'mapping jira users' do let(:client) { double } - let_it_be(:project) { create(:project) } let_it_be(:jira_service) { create(:jira_service, project: project, active: true) } before do @@ -11,7 +10,7 @@ RSpec.shared_examples 'mapping jira users' do allow(client).to receive(:get).with(url).and_return(jira_users) end - subject { described_class.new(jira_service, start_at) } + subject { described_class.new(current_user, project, start_at) } context 'jira_users is nil' do let(:jira_users) { nil } @@ -22,18 +21,27 @@ RSpec.shared_examples 'mapping jira users' do end context 'when jira_users is present' do - # TODO: now we only create an array in a proper format - # mapping is tracked in https://gitlab.com/gitlab-org/gitlab/-/issues/219023 let(:mapped_users) do [ - { jira_account_id: 'abcd', jira_display_name: 'user1', jira_email: nil, gitlab_id: nil, gitlab_username: nil, gitlab_name: nil }, - { jira_account_id: 'efg', jira_display_name: nil, jira_email: nil, gitlab_id: nil, gitlab_username: nil, gitlab_name: nil }, - { jira_account_id: 'hij', jira_display_name: 'user3', jira_email: 'user3@example.com', gitlab_id: nil, gitlab_username: nil, gitlab_name: nil } + { jira_account_id: 'abcd', jira_display_name: 'User-Name1', jira_email: nil, gitlab_id: user_1.id }, + { jira_account_id: 'efg', jira_display_name: 'username-2', jira_email: nil, gitlab_id: user_2.id }, + { jira_account_id: 'hij', jira_display_name: nil, jira_email: nil, gitlab_id: nil }, + { jira_account_id: '123', jira_display_name: 'user-4', jira_email: 'user-4@example.com', gitlab_id: user_4.id }, + { jira_account_id: '456', jira_display_name: 'username5foo', jira_email: 'user-5@example.com', gitlab_id: nil }, + { jira_account_id: '789', jira_display_name: 'user-6', jira_email: 'user-6@example.com', gitlab_id: nil }, + { jira_account_id: 'xyz', jira_display_name: 'username-7', jira_email: 'user-7@example.com', gitlab_id: nil }, + { jira_account_id: 'vhk', jira_display_name: 'user-8', jira_email: 'user8_email@example.com', gitlab_id: user_8.id }, + { jira_account_id: 'uji', jira_display_name: 'user-9', jira_email: 'uji@example.com', gitlab_id: user_1.id } ] end it 'returns users mapped to Gitlab' do expect(subject.execute).to eq(mapped_users) end + + # 1 query for getting matched users, 3 queries for MembersFinder + it 'runs only 4 queries' do + expect { subject }.not_to exceed_query_limit(4) + end end end diff --git a/spec/support/shared_examples/services/packages_shared_examples.rb b/spec/support/shared_examples/services/packages_shared_examples.rb index 65f4b3b5513..7987f2c296b 100644 --- a/spec/support/shared_examples/services/packages_shared_examples.rb +++ b/spec/support/shared_examples/services/packages_shared_examples.rb @@ -8,8 +8,8 @@ RSpec.shared_examples 'assigns build to package' do it 'assigns the pipeline to the package' do package = subject - expect(package.build_info).to be_present - expect(package.build_info.pipeline).to eq job.pipeline + expect(package.original_build_info).to be_present + expect(package.original_build_info.pipeline).to eq job.pipeline end end end diff --git a/spec/support/shared_examples/services/pages_size_limit_shared_examples.rb b/spec/support/shared_examples/services/pages_size_limit_shared_examples.rb index 15bf0d3698a..d9e906ebb75 100644 --- a/spec/support/shared_examples/services/pages_size_limit_shared_examples.rb +++ b/spec/support/shared_examples/services/pages_size_limit_shared_examples.rb @@ -4,6 +4,7 @@ RSpec.shared_examples 'pages size limit is' do |size_limit| context "when size is below the limit" do before do allow(metadata).to receive(:total_size).and_return(size_limit - 1.megabyte) + allow(metadata).to receive(:entries).and_return([]) end it 'updates pages correctly' do @@ -17,6 +18,7 @@ RSpec.shared_examples 'pages size limit is' do |size_limit| context "when size is above the limit" do before do allow(metadata).to receive(:total_size).and_return(size_limit + 1.megabyte) + allow(metadata).to receive(:entries).and_return([]) end it 'limits the maximum size of gitlab pages' do diff --git a/spec/support/shared_examples/uploaders/workers/object_storage/migrate_uploads_shared_examples.rb b/spec/support/shared_examples/uploaders/workers/object_storage/migrate_uploads_shared_examples.rb deleted file mode 100644 index 5a9a3dfc2d2..00000000000 --- a/spec/support/shared_examples/uploaders/workers/object_storage/migrate_uploads_shared_examples.rb +++ /dev/null @@ -1,120 +0,0 @@ -# frozen_string_literal: true - -# Expects the calling spec to define: -# - model_class -# - mounted_as -# - to_store -RSpec.shared_examples 'uploads migration worker' do - def perform(uploads, store = nil) - described_class.new.perform(uploads.ids, model_class.to_s, mounted_as, store || to_store) - rescue ObjectStorage::MigrateUploadsWorker::Report::MigrationFailures - # swallow - end - - describe '.enqueue!' do - def enqueue! - described_class.enqueue!(uploads, model_class, mounted_as, to_store) - end - - it 'is guarded by .sanity_check!' do - expect(described_class).to receive(:perform_async) - expect(described_class).to receive(:sanity_check!) - - enqueue! - end - - context 'sanity_check! fails' do - include_context 'sanity_check! fails' - - it 'does not enqueue a job' do - expect(described_class).not_to receive(:perform_async) - - expect { enqueue! }.to raise_error(described_class::SanityCheckError) - end - end - end - - describe '.sanity_check!' do - shared_examples 'raises a SanityCheckError' do |expected_message| - let(:mount_point) { nil } - - it do - expect { described_class.sanity_check!(uploads, model_class, mount_point) } - .to raise_error(described_class::SanityCheckError).with_message(expected_message) - end - end - - context 'uploader types mismatch' do - let!(:outlier) { create(:upload, uploader: 'GitlabUploader') } - - include_examples 'raises a SanityCheckError', /Multiple uploaders found/ - end - - context 'mount point not found' do - include_examples 'raises a SanityCheckError', /Mount point [a-z:]+ not found in/ do - let(:mount_point) { :potato } - end - end - end - - describe '#perform' do - shared_examples 'outputs correctly' do |success: 0, failures: 0| - total = success + failures - - if success > 0 - it 'outputs the reports' do - expect(Gitlab::AppLogger).to receive(:info).with(%r{Migrated #{success}/#{total} files}) - - perform(uploads) - end - end - - if failures > 0 - it 'outputs upload failures' do - expect(Gitlab::AppLogger).to receive(:warn).with(/Error .* I am a teapot/) - - perform(uploads) - end - end - end - - it_behaves_like 'outputs correctly', success: 10 - - it 'migrates files to remote storage' do - perform(uploads) - - expect(Upload.where(store: ObjectStorage::Store::LOCAL).count).to eq(0) - end - - context 'reversed' do - let(:to_store) { ObjectStorage::Store::LOCAL } - - before do - perform(uploads, ObjectStorage::Store::REMOTE) - end - - it 'migrates files to local storage' do - expect(Upload.where(store: ObjectStorage::Store::REMOTE).count).to eq(10) - - perform(uploads) - - expect(Upload.where(store: ObjectStorage::Store::LOCAL).count).to eq(10) - end - end - - context 'migration is unsuccessful' do - before do - allow_any_instance_of(ObjectStorage::Concern) - .to receive(:migrate!).and_raise(CarrierWave::UploadError, 'I am a teapot.') - end - - it_behaves_like 'outputs correctly', failures: 10 - end - end -end - -RSpec.shared_context 'sanity_check! fails' do - before do - expect(described_class).to receive(:sanity_check!).and_raise(described_class::SanityCheckError) - end -end diff --git a/spec/support/shared_examples/workers/concerns/reenqueuer_shared_examples.rb b/spec/support/shared_examples/workers/concerns/reenqueuer_shared_examples.rb index 50879969e90..37f44f98cda 100644 --- a/spec/support/shared_examples/workers/concerns/reenqueuer_shared_examples.rb +++ b/spec/support/shared_examples/workers/concerns/reenqueuer_shared_examples.rb @@ -1,40 +1,32 @@ # frozen_string_literal: true -# Expects `worker_class` to be defined +# Expects `subject` to be a job/worker instance RSpec.shared_examples 'reenqueuer' do - subject(:job) { worker_class.new } - before do - allow(job).to receive(:sleep) # faster tests + allow(subject).to receive(:sleep) # faster tests end it 'implements lease_timeout' do - expect(job.lease_timeout).to be_a(ActiveSupport::Duration) + expect(subject.lease_timeout).to be_a(ActiveSupport::Duration) end describe '#perform' do it 'tries to obtain a lease' do - expect_to_obtain_exclusive_lease(job.lease_key) + expect_to_obtain_exclusive_lease(subject.lease_key) - job.perform + subject.perform end end end -# Example usage: -# -# it_behaves_like 'it is rate limited to 1 call per', 5.seconds do -# subject { described_class.new } -# let(:rate_limited_method) { subject.perform } -# end -# -RSpec.shared_examples 'it is rate limited to 1 call per' do |minimum_duration| +# Expects `subject` to be a job/worker instance +RSpec.shared_examples '#perform is rate limited to 1 call per' do |minimum_duration| before do # Allow Timecop freeze and travel without the block form Timecop.safe_mode = false Timecop.freeze - time_travel_during_rate_limited_method(actual_duration) + time_travel_during_perform(actual_duration) end after do @@ -48,7 +40,7 @@ RSpec.shared_examples 'it is rate limited to 1 call per' do |minimum_duration| it 'sleeps exactly the minimum duration' do expect(subject).to receive(:sleep).with(a_value_within(0.01).of(minimum_duration)) - rate_limited_method + subject.perform end end @@ -58,7 +50,7 @@ RSpec.shared_examples 'it is rate limited to 1 call per' do |minimum_duration| it 'sleeps 90% of minimum duration' do expect(subject).to receive(:sleep).with(a_value_within(0.01).of(0.9 * minimum_duration)) - rate_limited_method + subject.perform end end @@ -68,7 +60,7 @@ RSpec.shared_examples 'it is rate limited to 1 call per' do |minimum_duration| it 'sleeps 10% of minimum duration' do expect(subject).to receive(:sleep).with(a_value_within(0.01).of(0.1 * minimum_duration)) - rate_limited_method + subject.perform end end @@ -78,7 +70,7 @@ RSpec.shared_examples 'it is rate limited to 1 call per' do |minimum_duration| it 'does not sleep' do expect(subject).not_to receive(:sleep) - rate_limited_method + subject.perform end end @@ -88,7 +80,7 @@ RSpec.shared_examples 'it is rate limited to 1 call per' do |minimum_duration| it 'does not sleep' do expect(subject).not_to receive(:sleep) - rate_limited_method + subject.perform end end @@ -98,11 +90,11 @@ RSpec.shared_examples 'it is rate limited to 1 call per' do |minimum_duration| it 'does not sleep' do expect(subject).not_to receive(:sleep) - rate_limited_method + subject.perform end end - def time_travel_during_rate_limited_method(actual_duration) + def time_travel_during_perform(actual_duration) # Save the original implementation of ensure_minimum_duration original_ensure_minimum_duration = subject.method(:ensure_minimum_duration) diff --git a/spec/support/snowplow.rb b/spec/support/snowplow.rb index 58812b8f4e6..b67fa96fab8 100644 --- a/spec/support/snowplow.rb +++ b/spec/support/snowplow.rb @@ -1,22 +1,24 @@ # frozen_string_literal: true RSpec.configure do |config| + config.include SnowplowHelpers, :snowplow + config.before(:each, :snowplow) do # Using a high buffer size to not cause early flushes buffer_size = 100 # WebMock is set up to allow requests to `localhost` host = 'localhost' - allow(Gitlab::Tracking) + allow_any_instance_of(Gitlab::Tracking::Destinations::Snowplow) .to receive(:emitter) .and_return(SnowplowTracker::Emitter.new(host, buffer_size: buffer_size)) stub_application_setting(snowplow_enabled: true) - allow(Gitlab::Tracking).to receive(:event).and_call_original + allow(Gitlab::Tracking).to receive(:event).and_call_original # rubocop:disable RSpec/ExpectGitlabTracking end config.after(:each, :snowplow) do - Gitlab::Tracking.send(:snowplow).flush + Gitlab::Tracking.send(:snowplow).send(:tracker).flush end end |