diff options
Diffstat (limited to 'spec/support')
81 files changed, 2973 insertions, 801 deletions
diff --git a/spec/support/atlassian/jira_connect/schemata.rb b/spec/support/atlassian/jira_connect/schemata.rb index 91f8fe0bb41..d056c7cacf3 100644 --- a/spec/support/atlassian/jira_connect/schemata.rb +++ b/spec/support/atlassian/jira_connect/schemata.rb @@ -2,82 +2,291 @@ module Atlassian module Schemata - def self.build_info - { - 'type' => 'object', - 'required' => %w(schemaVersion pipelineId buildNumber updateSequenceNumber displayName url state issueKeys testInfo references), - 'properties' => { - 'schemaVersion' => { 'type' => 'string', 'pattern' => '1.0' }, - 'pipelineId' => { 'type' => 'string' }, - 'buildNumber' => { 'type' => 'integer' }, - 'updateSequenceNumber' => { 'type' => 'integer' }, - 'displayName' => { 'type' => 'string' }, - 'url' => { 'type' => 'string' }, - 'state' => { - 'type' => 'string', - 'pattern' => '(pending|in_progress|successful|failed|cancelled)' - }, - 'issueKeys' => { - 'type' => 'array', - 'items' => { 'type' => 'string' }, - 'minItems' => 1 - }, - 'testInfo' => { - 'type' => 'object', - 'required' => %w(totalNumber numberPassed numberFailed numberSkipped), - 'properties' => { - 'totalNumber' => { 'type' => 'integer' }, - 'numberFailed' => { 'type' => 'integer' }, - 'numberPassed' => { 'type' => 'integer' }, - 'numberSkipped' => { 'type' => 'integer' } - } - }, - 'references' => { - 'type' => 'array', - 'items' => { + class << self + def build_info + { + 'type' => 'object', + 'additionalProperties' => false, + 'required' => %w( + schemaVersion pipelineId buildNumber updateSequenceNumber + displayName url state issueKeys testInfo references + lastUpdated + ), + 'properties' => { + 'schemaVersion' => schema_version_type, + 'pipelineId' => { 'type' => 'string' }, + 'buildNumber' => { 'type' => 'integer' }, + 'updateSequenceNumber' => { 'type' => 'integer' }, + 'displayName' => { 'type' => 'string' }, + 'lastUpdated' => iso8601_type, + 'url' => { 'type' => 'string' }, + 'state' => state_type, + 'issueKeys' => issue_keys_type, + 'testInfo' => { 'type' => 'object', - 'required' => %w(commit ref), + 'required' => %w(totalNumber numberPassed numberFailed numberSkipped), 'properties' => { - 'commit' => { - 'type' => 'object', - 'required' => %w(id repositoryUri), - 'properties' => { - 'id' => { 'type' => 'string' }, - 'repositoryUri' => { 'type' => 'string' } + 'totalNumber' => { 'type' => 'integer' }, + 'numberFailed' => { 'type' => 'integer' }, + 'numberPassed' => { 'type' => 'integer' }, + 'numberSkipped' => { 'type' => 'integer' } + } + }, + 'references' => { + 'type' => 'array', + 'items' => { + 'type' => 'object', + 'required' => %w(commit ref), + 'properties' => { + 'commit' => { + 'type' => 'object', + 'required' => %w(id repositoryUri), + 'properties' => { + 'id' => { 'type' => 'string' }, + 'repositoryUri' => { 'type' => 'string' } + } + }, + 'ref' => { + 'type' => 'object', + 'required' => %w(name uri), + 'properties' => { + 'name' => { 'type' => 'string' }, + 'uri' => { 'type' => 'string' } + } } - }, - 'ref' => { - 'type' => 'object', - 'required' => %w(name uri), - 'properties' => { - 'name' => { 'type' => 'string' }, - 'uri' => { 'type' => 'string' } + } + } + } + } + } + end + + def deployment_info + { + 'type' => 'object', + 'additionalProperties' => false, + 'required' => %w( + deploymentSequenceNumber updateSequenceNumber + associations displayName url description lastUpdated + state pipeline environment + ), + 'properties' => { + 'deploymentSequenceNumber' => { 'type' => 'integer' }, + 'updateSequenceNumber' => { 'type' => 'integer' }, + 'associations' => { + 'type' => 'array', + 'items' => association_type, + 'minItems' => 1 + }, + 'displayName' => { 'type' => 'string' }, + 'description' => { 'type' => 'string' }, + 'label' => { 'type' => 'string' }, + 'url' => { 'type' => 'string' }, + 'lastUpdated' => iso8601_type, + 'state' => state_type, + 'pipeline' => pipeline_type, + 'environment' => environment_type, + 'schemaVersion' => schema_version_type + } + } + end + + def feature_flag_info + { + 'type' => 'object', + 'additionalProperties' => false, + 'required' => %w( + updateSequenceId id key issueKeys summary details + ), + 'properties' => { + 'id' => { 'type' => 'string' }, + 'key' => { 'type' => 'string' }, + 'displayName' => { 'type' => 'string' }, + 'issueKeys' => issue_keys_type, + 'summary' => summary_type, + 'details' => details_type, + 'updateSequenceId' => { 'type' => 'integer' }, + 'schemaVersion' => schema_version_type + } + } + end + + def details_type + { + 'type' => 'array', + 'items' => combine(summary_type, { + 'required' => ['environment'], + 'properties' => { + 'environment' => { + 'type' => 'object', + 'additionalProperties' => false, + 'required' => %w(name), + 'properties' => { + 'name' => { 'type' => 'string' }, + 'type' => { + 'type' => 'string', + 'pattern' => '^(development|testing|staging|production)$' } } } } + }) + } + end + + def combine(map_a, map_b) + map_a.merge(map_b) do |k, a, b| + a.respond_to?(:merge) ? a.merge(b) : a + b + end + end + + def summary_type + { + 'type' => 'object', + 'additionalProperties' => false, + 'required' => %w(url status lastUpdated), + 'properties' => { + 'lastUpdated' => iso8601_type, + 'url' => { 'type' => 'string' }, + 'status' => feature_status_type } } - } - end + end - def self.build_info_payload - { - 'type' => 'object', - 'required' => %w(providerMetadata builds), - 'properties' => { - 'providerMetadata' => provider_metadata, - 'builds' => { 'type' => 'array', 'items' => build_info } + def feature_status_type + { + 'type' => 'object', + 'additionalProperties' => false, + 'required' => %w(enabled), + 'properties' => { + 'enabled' => { 'type' => 'boolean' }, + 'defaultValue' => { 'type' => 'string' }, + 'rollout' => rollout_type + } } - } - end + end + + def rollout_type + { + 'type' => 'object', + 'additionalProperties' => false, + 'properties' => { + 'percentage' => { 'type' => 'number' }, + 'text' => { 'type' => 'string' }, + 'rules' => { 'type' => 'number' } + } + } + end - def self.provider_metadata - { - 'type' => 'object', - 'required' => %w(product), - 'properties' => { 'product' => { 'type' => 'string' } } - } + def environment_type + { + 'type' => 'object', + 'additionalProperties' => false, + 'required' => %w(id displayName type), + 'properties' => { + 'id' => { 'type' => 'string', 'maxLength' => 255 }, + 'displayName' => { 'type' => 'string', 'maxLength' => 255 }, + 'type' => { + 'type' => 'string', + 'pattern' => '(unmapped|development|testing|staging|production)' + } + } + } + end + + def pipeline_type + { + 'type' => 'object', + 'additionalProperties' => false, + 'required' => %w(id displayName url), + 'properties' => { + 'id' => { 'type' => 'string', 'maxLength' => 255 }, + 'displayName' => { 'type' => 'string', 'maxLength' => 255 }, + 'url' => { 'type' => 'string', 'maxLength' => 2000 } + } + } + end + + def schema_version_type + { 'type' => 'string', 'pattern' => '1.0' } + end + + def state_type + { + 'type' => 'string', + 'pattern' => '(pending|in_progress|successful|failed|cancelled)' + } + end + + def association_type + { + 'type' => 'object', + 'additionalProperties' => false, + 'required' => %w(associationType values), + 'properties' => { + 'associationType' => { + 'type' => 'string', + 'pattern' => '(issueKeys|issueIdOrKeys)' + }, + 'values' => issue_keys_type + } + } + end + + def issue_keys_type + { + 'type' => 'array', + 'items' => { 'type' => 'string' }, + 'minItems' => 1, + 'maxItems' => 100 + } + end + + def deploy_info_payload + payload('deployments', deployment_info) + end + + def build_info_payload + payload('builds', build_info) + end + + def ff_info_payload + pl = payload('flags', feature_flag_info) + pl['properties']['properties'] = { + 'type' => 'object', + 'additionalProperties' => { 'type' => 'string' }, + 'maxProperties' => 5, + 'propertyNames' => { 'pattern' => '^[^_][^:]+$' } + } + pl + end + + def payload(key, schema) + { + 'type' => 'object', + 'additionalProperties' => false, + 'required' => ['providerMetadata', key], + 'properties' => { + 'providerMetadata' => provider_metadata, + key => { 'type' => 'array', 'items' => schema } + } + } + end + + def provider_metadata + { + 'type' => 'object', + 'required' => %w(product), + 'properties' => { 'product' => { 'type' => 'string' } } + } + end + + def iso8601_type + { + 'type' => 'string', + 'pattern' => '^-?([1-9][0-9]*)?[0-9]{4}-(1[0-2]|0[1-9])-(3[01]|0[1-9]|[12][0-9])T(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])(\.[0-9]+)?(Z|[+-](?:2[0-3]|[01][0-9]):[0-5][0-9])?$' + } + end end end end diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb index ab55cf97ab4..db198ac9808 100644 --- a/spec/support/capybara.rb +++ b/spec/support/capybara.rb @@ -33,7 +33,7 @@ Capybara.register_server :puma_via_workhorse do |app, port, host, **options| socket_path = file.path file.close! # We just want the filename - TestEnv.with_workhorse(TestEnv.workhorse_dir, host, port, socket_path) do + TestEnv.with_workhorse(host, port, socket_path) do Capybara.servers[:puma].call(app, nil, socket_path, **options) end end diff --git a/spec/support/gitlab_stubs/gitlab_ci_for_sast.yml b/spec/support/gitlab_stubs/gitlab_ci_for_sast.yml index c4f3c3aace2..d20078c8904 100644 --- a/spec/support/gitlab_stubs/gitlab_ci_for_sast.yml +++ b/spec/support/gitlab_stubs/gitlab_ci_for_sast.yml @@ -4,7 +4,8 @@ include: variables: SECURE_ANALYZERS_PREFIX: "registry.gitlab.com/gitlab-org/security-products/analyzers2" SAST_EXCLUDED_PATHS: "spec, executables" - SAST_DEFAULT_ANALYZERS: "bandit, gosec" + SAST_DEFAULT_ANALYZERS: "bandit, brakeman" + SAST_EXCLUDED_ANALYZERS: "brakeman" stages: - our_custom_security_stage diff --git a/spec/support/gitlab_stubs/gitlab_ci_for_sast_default_analyzers.yml b/spec/support/gitlab_stubs/gitlab_ci_for_sast_default_analyzers.yml new file mode 100644 index 00000000000..c4f3c3aace2 --- /dev/null +++ b/spec/support/gitlab_stubs/gitlab_ci_for_sast_default_analyzers.yml @@ -0,0 +1,15 @@ +include: + - template: SAST.gitlab-ci.yml + +variables: + SECURE_ANALYZERS_PREFIX: "registry.gitlab.com/gitlab-org/security-products/analyzers2" + SAST_EXCLUDED_PATHS: "spec, executables" + SAST_DEFAULT_ANALYZERS: "bandit, gosec" + +stages: + - our_custom_security_stage +sast: + stage: our_custom_security_stage + variables: + SEARCH_MAX_DEPTH: 8 + SAST_BRAKEMAN_LEVEL: 2 diff --git a/spec/support/gitlab_stubs/gitlab_ci_for_sast_excluded_analyzers.yml b/spec/support/gitlab_stubs/gitlab_ci_for_sast_excluded_analyzers.yml new file mode 100644 index 00000000000..b665de5f982 --- /dev/null +++ b/spec/support/gitlab_stubs/gitlab_ci_for_sast_excluded_analyzers.yml @@ -0,0 +1,14 @@ +include: + - template: SAST.gitlab-ci.yml + +variables: + SECURE_ANALYZERS_PREFIX: "registry.gitlab.com/gitlab-org/security-products/analyzers2" + SAST_EXCLUDED_PATHS: "spec, executables" + SAST_EXCLUDED_ANALYZERS: "brakeman" + +stages: + - our_custom_security_stage +sast: + stage: our_custom_security_stage + variables: + SEARCH_MAX_DEPTH: 8 diff --git a/spec/support/gitlab_stubs/gitlab_ci_includes.yml b/spec/support/gitlab_stubs/gitlab_ci_includes.yml index e74773ce23e..1029fa1ea86 100644 --- a/spec/support/gitlab_stubs/gitlab_ci_includes.yml +++ b/spec/support/gitlab_stubs/gitlab_ci_includes.yml @@ -1,19 +1,45 @@ +before_script: + - bundle install + - bundle exec rake db:create + rspec 0 1: stage: build script: 'rake spec' needs: [] + tags: + - ruby + - postgres + only: + - branches + - master rspec 0 2: stage: build + allow_failure: true script: 'rake spec' + when: on_failure needs: [] spinach: stage: build script: 'rake spinach' needs: [] + except: + - tags +deploy_job: + stage: deploy + script: + - echo 'done' + environment: + name: production docker: stage: test script: 'curl http://dockerhub/URL' needs: [spinach, rspec 0 1] + when: manual + except: + - branches + +after_script: + - echo 'run this after' diff --git a/spec/support/helpers/database/database_helpers.rb b/spec/support/helpers/database/database_helpers.rb new file mode 100644 index 00000000000..b8d7ea3662f --- /dev/null +++ b/spec/support/helpers/database/database_helpers.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Database + module DatabaseHelpers + # In order to directly work with views using factories, + # we can swapout the view for a table of identical structure. + def swapout_view_for_table(view) + ActiveRecord::Base.connection.execute(<<~SQL) + CREATE TABLE #{view}_copy (LIKE #{view}); + DROP VIEW #{view}; + ALTER TABLE #{view}_copy RENAME TO #{view}; + SQL + end + end +end diff --git a/spec/support/helpers/database/partitioning_helpers.rb b/spec/support/helpers/database/partitioning_helpers.rb new file mode 100644 index 00000000000..80b31fe0603 --- /dev/null +++ b/spec/support/helpers/database/partitioning_helpers.rb @@ -0,0 +1,96 @@ +# frozen_string_literal: true + +module Database + module PartitioningHelpers + def expect_table_partitioned_by(table, columns, part_type: :range) + columns_with_part_type = columns.map { |c| [part_type.to_s, c] } + actual_columns = find_partitioned_columns(table) + + expect(columns_with_part_type).to match_array(actual_columns) + end + + def expect_range_partition_of(partition_name, table_name, min_value, max_value) + definition = find_partition_definition(partition_name, schema: Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA) + + expect(definition).not_to be_nil + expect(definition['base_table']).to eq(table_name.to_s) + expect(definition['condition']).to eq("FOR VALUES FROM (#{min_value}) TO (#{max_value})") + end + + def expect_total_partitions(table_name, count, schema: Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA) + partitions = find_partitions(table_name, schema: schema) + + expect(partitions.size).to eq(count) + end + + def expect_range_partitions_for(table_name, partitions) + partitions.each do |suffix, (min_value, max_value)| + partition_name = "#{table_name}_#{suffix}" + expect_range_partition_of(partition_name, table_name, min_value, max_value) + end + + expect_total_partitions(table_name, partitions.size, schema: Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA) + end + + def expect_hash_partition_of(partition_name, table_name, modulus, remainder) + definition = find_partition_definition(partition_name, schema: Gitlab::Database::STATIC_PARTITIONS_SCHEMA) + + expect(definition).not_to be_nil + expect(definition['base_table']).to eq(table_name.to_s) + expect(definition['condition']).to eq("FOR VALUES WITH (modulus #{modulus}, remainder #{remainder})") + end + + private + + def find_partitioned_columns(table) + connection.select_rows(<<~SQL) + select + case partstrat + when 'l' then 'list' + when 'r' then 'range' + when 'h' then 'hash' + end as partstrat, + cols.column_name + from ( + select partrelid, partstrat, unnest(partattrs) as col_pos + from pg_partitioned_table + ) pg_part + inner join pg_class + on pg_part.partrelid = pg_class.oid + inner join information_schema.columns cols + on cols.table_name = pg_class.relname + and cols.ordinal_position = pg_part.col_pos + where pg_class.relname = '#{table}'; + SQL + end + + def find_partition_definition(partition, schema: ) + connection.select_one(<<~SQL) + select + parent_class.relname as base_table, + pg_get_expr(pg_class.relpartbound, inhrelid) as condition + from pg_class + inner join pg_inherits i on pg_class.oid = inhrelid + inner join pg_class parent_class on parent_class.oid = inhparent + inner join pg_namespace ON pg_namespace.oid = pg_class.relnamespace + where pg_namespace.nspname = '#{schema}' + and pg_class.relname = '#{partition}' + and pg_class.relispartition + SQL + end + + def find_partitions(partition, schema: Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA) + connection.select_rows(<<~SQL) + select + pg_class.relname + from pg_class + inner join pg_inherits i on pg_class.oid = inhrelid + inner join pg_class parent_class on parent_class.oid = inhparent + inner join pg_namespace ON pg_namespace.oid = pg_class.relnamespace + where pg_namespace.nspname = '#{schema}' + and parent_class.relname = '#{partition}' + and pg_class.relispartition + SQL + end + end +end diff --git a/spec/support/helpers/database/table_schema_helpers.rb b/spec/support/helpers/database/table_schema_helpers.rb new file mode 100644 index 00000000000..48d33442110 --- /dev/null +++ b/spec/support/helpers/database/table_schema_helpers.rb @@ -0,0 +1,149 @@ +# frozen_string_literal: true + +module Database + 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_table_columns_to_match(expected_column_attributes, table_name) + expect(connection.table_exists?(table_name)).to eq(true) + + actual_columns = connection.columns(table_name) + expect(actual_columns.size).to eq(column_attributes.size) + + column_attributes.each_with_index do |attributes, i| + actual_column = actual_columns[i] + + attributes.each do |name, value| + actual_value = actual_column.public_send(name) + message = "expected #{actual_column.name}.#{name} to be #{value}, but got #{actual_value}" + + expect(actual_value).to eq(value), message + end + end + 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_check_constraint(table_name, name, definition, schema: nil) + expect(check_constraint_definition(table_name, name, schema: schema)).to eq("CHECK ((#{definition}))") + 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 + + def check_constraint_definition(table_name, constraint_name, schema: nil) + table_name = schema ? "#{schema}.#{table_name}" : table_name + + connection.select_value(<<~SQL) + SELECT + pg_get_constraintdef(oid) AS constraint_definition + FROM pg_catalog.pg_constraint + WHERE pg_constraint.conrelid = '#{table_name}'::regclass + AND pg_constraint.contype = 'c' + AND pg_constraint.conname = '#{constraint_name}' + SQL + end + end +end diff --git a/spec/support/helpers/database/trigger_helpers.rb b/spec/support/helpers/database/trigger_helpers.rb new file mode 100644 index 00000000000..9ec03e68413 --- /dev/null +++ b/spec/support/helpers/database/trigger_helpers.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +module Database + module TriggerHelpers + def expect_function_to_exist(name) + expect(find_function_def(name)).not_to be_nil + end + + def expect_function_not_to_exist(name) + expect(find_function_def(name)).to be_nil + end + + def expect_function_to_contain(name, *statements) + return_stmt, *body_stmts = parsed_function_statements(name).reverse + + expect(return_stmt).to eq('return old') + expect(body_stmts).to contain_exactly(*statements) + end + + def expect_trigger_not_to_exist(table_name, name) + expect(find_trigger_def(table_name, name)).to be_nil + end + + def expect_valid_function_trigger(table_name, name, fn_name, fires_on) + events, timing, definition = cleaned_trigger_def(table_name, name) + + events = events&.split(',') + expected_timing, expected_events = fires_on.first + expect(timing).to eq(expected_timing.to_s) + expect(events).to match_array(Array.wrap(expected_events)) + + expect(definition).to match(%r{execute (?:procedure|function) #{fn_name}()}) + end + + private + + def parsed_function_statements(name) + cleaned_definition = find_function_def(name)['body'].downcase.gsub(/\s+/, ' ') + statements = cleaned_definition.sub(/\A\s*begin\s*(.*)\s*end\s*\Z/, "\\1") + statements.split(';').map! { |stmt| stmt.strip.presence }.compact! + end + + def find_function_def(name) + connection.select_one(<<~SQL) + SELECT prosrc AS body + FROM pg_proc + WHERE proname = '#{name}' + SQL + end + + def cleaned_trigger_def(table_name, name) + find_trigger_def(table_name, name).values_at('event', 'action_timing', 'action_statement').map!(&:downcase) + end + + def find_trigger_def(table_name, name) + connection.select_one(<<~SQL) + SELECT + string_agg(event_manipulation, ',') AS event, + action_timing, + action_statement + FROM information_schema.triggers + WHERE event_object_table = '#{table_name}' + AND trigger_name = '#{name}' + GROUP BY 2, 3 + SQL + end + end +end diff --git a/spec/support/helpers/database_helpers.rb b/spec/support/helpers/database_helpers.rb deleted file mode 100644 index e9f0a74a8d1..00000000000 --- a/spec/support/helpers/database_helpers.rb +++ /dev/null @@ -1,13 +0,0 @@ -# frozen_string_literal: true - -module DatabaseHelpers - # In order to directly work with views using factories, - # we can swapout the view for a table of identical structure. - def swapout_view_for_table(view) - ActiveRecord::Base.connection.execute(<<~SQL) - CREATE TABLE #{view}_copy (LIKE #{view}); - DROP VIEW #{view}; - ALTER TABLE #{view}_copy RENAME TO #{view}; - SQL - end -end diff --git a/spec/support/helpers/graphql_helpers.rb b/spec/support/helpers/graphql_helpers.rb index b20801bd3c4..35c298a4d48 100644 --- a/spec/support/helpers/graphql_helpers.rb +++ b/spec/support/helpers/graphql_helpers.rb @@ -67,14 +67,16 @@ module GraphqlHelpers end end + def with_clean_batchloader_executor(&block) + BatchLoader::Executor.ensure_current + yield + ensure + BatchLoader::Executor.clear_current + end + # Runs a block inside a BatchLoader::Executor wrapper def batch(max_queries: nil, &blk) - wrapper = proc do - BatchLoader::Executor.ensure_current - yield - ensure - BatchLoader::Executor.clear_current - end + wrapper = -> { with_clean_batchloader_executor(&blk) } if max_queries result = nil @@ -85,6 +87,32 @@ module GraphqlHelpers end end + # Use this when writing N+1 tests. + # + # It does not use the controller, so it avoids confounding factors due to + # authentication (token set-up, license checks) + # It clears the request store, rails cache, and BatchLoader Executor between runs. + def run_with_clean_state(query, **args) + ::Gitlab::WithRequestStore.with_request_store do + with_clean_rails_cache do + with_clean_batchloader_executor do + ::GitlabSchema.execute(query, **args) + end + end + end + end + + # Basically a combination of use_sql_query_cache and use_clean_rails_memory_store_caching, + # but more fine-grained, suitable for comparing two runs in the same example. + def with_clean_rails_cache(&blk) + caching_store = Rails.cache + Rails.cache = ActiveSupport::Cache::MemoryStore.new + + ActiveRecord::Base.cache(&blk) + ensure + Rails.cache = caching_store + end + # BatchLoader::GraphQL returns a wrapper, so we need to :sync in order # to get the actual values def batch_sync(max_queries: nil, &blk) @@ -245,7 +273,7 @@ module GraphqlHelpers return if max_depth <= 0 allow_unlimited_graphql_complexity - allow_unlimited_graphql_depth + allow_unlimited_graphql_depth if max_depth > 1 allow_high_graphql_recursion allow_high_graphql_transaction_threshold diff --git a/spec/support/helpers/multipart_helpers.rb b/spec/support/helpers/multipart_helpers.rb index bcb184f84c5..8438a83aa8a 100644 --- a/spec/support/helpers/multipart_helpers.rb +++ b/spec/support/helpers/multipart_helpers.rb @@ -13,29 +13,23 @@ module MultipartHelpers ) end - # This function assumes a `mode` variable to be set - def upload_parameters_for(filepath: nil, key: nil, filename: 'filename', remote_id: 'remote_id') + def upload_parameters_for(filepath: nil, key: nil, mode: nil, filename: 'filename', remote_id: 'remote_id') result = { - "#{key}.name" => filename, - "#{key}.type" => "application/octet-stream", - "#{key}.sha256" => "1234567890" + "name" => filename, + "type" => "application/octet-stream", + "sha256" => "1234567890" } case mode when :local - result["#{key}.path"] = filepath + result["path"] = filepath when :remote - result["#{key}.remote_id"] = remote_id - result["#{key}.size"] = 3.megabytes + result["remote_id"] = remote_id + result["size"] = 3.megabytes else raise ArgumentError, "can't handle #{mode} mode" end - return result if ::Feature.disabled?(:upload_middleware_jwt_params_handler, default_enabled: true) - - # the HandlerForJWTParams expects a jwt token with the upload parameters - # *without* the "#{key}." prefix - result.deep_transform_keys! { |k| k.remove("#{key}.") } { "#{key}.gitlab-workhorse-upload" => jwt_token(data: { 'upload' => result }) } diff --git a/spec/support/helpers/partitioning_helpers.rb b/spec/support/helpers/partitioning_helpers.rb deleted file mode 100644 index 8981fea04d5..00000000000 --- a/spec/support/helpers/partitioning_helpers.rb +++ /dev/null @@ -1,94 +0,0 @@ -# frozen_string_literal: true - -module PartitioningHelpers - def expect_table_partitioned_by(table, columns, part_type: :range) - columns_with_part_type = columns.map { |c| [part_type.to_s, c] } - actual_columns = find_partitioned_columns(table) - - expect(columns_with_part_type).to match_array(actual_columns) - end - - def expect_range_partition_of(partition_name, table_name, min_value, max_value) - definition = find_partition_definition(partition_name, schema: Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA) - - expect(definition).not_to be_nil - expect(definition['base_table']).to eq(table_name.to_s) - expect(definition['condition']).to eq("FOR VALUES FROM (#{min_value}) TO (#{max_value})") - end - - def expect_total_partitions(table_name, count, schema: Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA) - partitions = find_partitions(table_name, schema: schema) - - expect(partitions.size).to eq(count) - end - - def expect_range_partitions_for(table_name, partitions) - partitions.each do |suffix, (min_value, max_value)| - partition_name = "#{table_name}_#{suffix}" - expect_range_partition_of(partition_name, table_name, min_value, max_value) - end - - expect_total_partitions(table_name, partitions.size, schema: Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA) - end - - def expect_hash_partition_of(partition_name, table_name, modulus, remainder) - definition = find_partition_definition(partition_name, schema: Gitlab::Database::STATIC_PARTITIONS_SCHEMA) - - expect(definition).not_to be_nil - expect(definition['base_table']).to eq(table_name.to_s) - expect(definition['condition']).to eq("FOR VALUES WITH (modulus #{modulus}, remainder #{remainder})") - end - - private - - def find_partitioned_columns(table) - connection.select_rows(<<~SQL) - select - case partstrat - when 'l' then 'list' - when 'r' then 'range' - when 'h' then 'hash' - end as partstrat, - cols.column_name - from ( - select partrelid, partstrat, unnest(partattrs) as col_pos - from pg_partitioned_table - ) pg_part - inner join pg_class - on pg_part.partrelid = pg_class.oid - inner join information_schema.columns cols - on cols.table_name = pg_class.relname - and cols.ordinal_position = pg_part.col_pos - where pg_class.relname = '#{table}'; - SQL - end - - def find_partition_definition(partition, schema: ) - connection.select_one(<<~SQL) - select - parent_class.relname as base_table, - pg_get_expr(pg_class.relpartbound, inhrelid) as condition - from pg_class - inner join pg_inherits i on pg_class.oid = inhrelid - inner join pg_class parent_class on parent_class.oid = inhparent - inner join pg_namespace ON pg_namespace.oid = pg_class.relnamespace - where pg_namespace.nspname = '#{schema}' - and pg_class.relname = '#{partition}' - and pg_class.relispartition - SQL - end - - def find_partitions(partition, schema: Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA) - connection.select_rows(<<~SQL) - select - pg_class.relname - from pg_class - inner join pg_inherits i on pg_class.oid = inhrelid - inner join pg_class parent_class on parent_class.oid = inhparent - inner join pg_namespace ON pg_namespace.oid = pg_class.relnamespace - where pg_namespace.nspname = '#{schema}' - and parent_class.relname = '#{partition}' - and pg_class.relispartition - SQL - end -end diff --git a/spec/support/helpers/rack_attack_spec_helpers.rb b/spec/support/helpers/rack_attack_spec_helpers.rb index a8ae69885d8..d50a6382a40 100644 --- a/spec/support/helpers/rack_attack_spec_helpers.rb +++ b/spec/support/helpers/rack_attack_spec_helpers.rb @@ -21,10 +21,31 @@ module RackAttackSpecHelpers { 'AUTHORIZATION' => "Bearer #{oauth_access_token.token}" } end + def basic_auth_headers(user, personal_access_token) + encoded_login = ["#{user.username}:#{personal_access_token.token}"].pack('m0') + { 'AUTHORIZATION' => "Basic #{encoded_login}" } + end + def expect_rejection(&block) yield expect(response).to have_gitlab_http_status(:too_many_requests) + + expect(response.headers.to_h).to include( + 'RateLimit-Limit' => a_string_matching(/^\d+$/), + 'RateLimit-Name' => a_string_matching(/^throttle_.*$/), + 'RateLimit-Observed' => a_string_matching(/^\d+$/), + 'RateLimit-Remaining' => a_string_matching(/^\d+$/), + 'Retry-After' => a_string_matching(/^\d+$/) + ) + expect(response).to have_header('RateLimit-Reset') + expect do + DateTime.strptime(response.headers['RateLimit-Reset'], '%s') + end.not_to raise_error + expect(response).to have_header('RateLimit-ResetTime') + expect do + Time.httpdate(response.headers['RateLimit-ResetTime']) + end.not_to raise_error end def expect_ok(&block) diff --git a/spec/support/helpers/stub_experiments.rb b/spec/support/helpers/stub_experiments.rb index 247692d83ee..408d16a7c08 100644 --- a/spec/support/helpers/stub_experiments.rb +++ b/spec/support/helpers/stub_experiments.rb @@ -11,6 +11,7 @@ module StubExperiments allow(Gitlab::Experimentation).to receive(:active?).and_call_original experiments.each do |experiment_key, enabled| + Feature.persist_used!("#{experiment_key}#{feature_flag_suffix}") allow(Gitlab::Experimentation).to receive(:active?).with(experiment_key) { enabled } end end @@ -25,7 +26,14 @@ module StubExperiments allow(Gitlab::Experimentation).to receive(:in_experiment_group?).and_call_original experiments.each do |experiment_key, enabled| + Feature.persist_used!("#{experiment_key}#{feature_flag_suffix}") allow(Gitlab::Experimentation).to receive(:in_experiment_group?).with(experiment_key, anything) { enabled } end end + + private + + def feature_flag_suffix + Gitlab::Experimentation::Experiment::FEATURE_FLAG_SUFFIX + end end diff --git a/spec/support/helpers/stub_feature_flags.rb b/spec/support/helpers/stub_feature_flags.rb index 7f30a2a70cd..77f31169ecb 100644 --- a/spec/support/helpers/stub_feature_flags.rb +++ b/spec/support/helpers/stub_feature_flags.rb @@ -66,4 +66,8 @@ module StubFeatureFlags def skip_feature_flags_yaml_validation allow(Feature::Definition).to receive(:valid_usage!) end + + def skip_default_enabled_yaml_check + allow(Feature::Definition).to receive(:default_enabled?).and_return(false) + end end diff --git a/spec/support/helpers/table_schema_helpers.rb b/spec/support/helpers/table_schema_helpers.rb deleted file mode 100644 index 28794211190..00000000000 --- a/spec/support/helpers/table_schema_helpers.rb +++ /dev/null @@ -1,112 +0,0 @@ -# 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 01571277a1d..cb25f5f9429 100644 --- a/spec/support/helpers/test_env.rb +++ b/spec/support/helpers/test_env.rb @@ -203,10 +203,13 @@ module TestEnv end gitaly_pid = Integer(File.read(TMP_TEST_PATH.join('gitaly.pid'))) + gitaly2_pid = Integer(File.read(TMP_TEST_PATH.join('gitaly2.pid'))) praefect_pid = Integer(File.read(TMP_TEST_PATH.join('praefect.pid'))) - Kernel.at_exit { stop(gitaly_pid) } - Kernel.at_exit { stop(praefect_pid) } + Kernel.at_exit do + pids = [gitaly_pid, gitaly2_pid, praefect_pid] + pids.each { |pid| stop(pid) } + end wait('gitaly') wait('praefect') @@ -284,7 +287,7 @@ module TestEnv @workhorse_path ||= File.join('tmp', 'tests', 'gitlab-workhorse') end - def with_workhorse(workhorse_dir, host, port, upstream, &blk) + def with_workhorse(host, port, upstream, &blk) host = "[#{host}]" if host.include?(':') listen_addr = [host, port].join(':') diff --git a/spec/support/helpers/trigger_helpers.rb b/spec/support/helpers/trigger_helpers.rb deleted file mode 100644 index dd6d8ff5bb5..00000000000 --- a/spec/support/helpers/trigger_helpers.rb +++ /dev/null @@ -1,66 +0,0 @@ -# frozen_string_literal: true - -module TriggerHelpers - def expect_function_to_exist(name) - expect(find_function_def(name)).not_to be_nil - end - - def expect_function_not_to_exist(name) - expect(find_function_def(name)).to be_nil - end - - def expect_function_to_contain(name, *statements) - return_stmt, *body_stmts = parsed_function_statements(name).reverse - - expect(return_stmt).to eq('return old') - expect(body_stmts).to contain_exactly(*statements) - end - - def expect_trigger_not_to_exist(table_name, name) - expect(find_trigger_def(table_name, name)).to be_nil - end - - def expect_valid_function_trigger(table_name, name, fn_name, fires_on) - events, timing, definition = cleaned_trigger_def(table_name, name) - - events = events&.split(',') - expected_timing, expected_events = fires_on.first - expect(timing).to eq(expected_timing.to_s) - expect(events).to match_array(Array.wrap(expected_events)) - - expect(definition).to match(%r{execute (?:procedure|function) #{fn_name}()}) - end - - private - - def parsed_function_statements(name) - cleaned_definition = find_function_def(name)['body'].downcase.gsub(/\s+/, ' ') - statements = cleaned_definition.sub(/\A\s*begin\s*(.*)\s*end\s*\Z/, "\\1") - statements.split(';').map! { |stmt| stmt.strip.presence }.compact! - end - - def find_function_def(name) - connection.select_one(<<~SQL) - SELECT prosrc AS body - FROM pg_proc - WHERE proname = '#{name}' - SQL - end - - def cleaned_trigger_def(table_name, name) - find_trigger_def(table_name, name).values_at('event', 'action_timing', 'action_statement').map!(&:downcase) - end - - def find_trigger_def(table_name, name) - connection.select_one(<<~SQL) - SELECT - string_agg(event_manipulation, ',') AS event, - action_timing, - action_statement - FROM information_schema.triggers - WHERE event_object_table = '#{table_name}' - AND trigger_name = '#{name}' - GROUP BY 2, 3 - SQL - end -end diff --git a/spec/support/matchers/be_sorted.rb b/spec/support/matchers/be_sorted.rb index 1455060fe71..b0ab93efbb2 100644 --- a/spec/support/matchers/be_sorted.rb +++ b/spec/support/matchers/be_sorted.rb @@ -4,18 +4,75 @@ # # By default, this checks that the collection is sorted ascending # but you can check order by specific field and order by passing -# them, eg: +# them, either as arguments, or using the fluent interface, eg: # # ``` +# # Usage examples: +# expect(collection).to be_sorted +# expect(collection).to be_sorted(:field) # expect(collection).to be_sorted(:field, :desc) +# expect(collection).to be_sorted.asc +# expect(collection).to be_sorted.desc.by(&:field) +# expect(collection).to be_sorted.by(&:field).desc +# expect(collection).to be_sorted.by { |x| [x.foo, x.bar] } # ``` -RSpec::Matchers.define :be_sorted do |by, order = :asc| +RSpec::Matchers.define :be_sorted do |on = :itself, order = :asc| + def by(&block) + @comparator = block + self + end + + def asc + @direction = :asc + self + end + + def desc + @direction = :desc + self + end + + def format_with(proc) + @format_with = proc + self + end + + define_method :comparator do + @comparator || on + end + + define_method :descending? do + (@direction || order.to_sym) == :desc + end + + def order(items) + descending? ? items.reverse : items + end + + def sort(items) + items.sort_by(&comparator) + end + match do |actual| - next true unless actual.present? # emtpy collection is sorted + next true unless actual.present? # empty collection is sorted + + actual = actual.to_a if actual.respond_to?(:to_a) && !actual.respond_to?(:sort_by) + + @got = actual + @expected = order(sort(actual)) + + @expected == actual + end + + def failure_message + "Expected #{show(@expected)}, got #{show(@got)}" + end - actual - .then { |collection| by ? collection.sort_by(&by) : collection.sort } - .then { |sorted_collection| order.to_sym == :desc ? sorted_collection.reverse : sorted_collection } - .then { |sorted_collection| sorted_collection == actual } + def show(things) + if @format_with + things.map(&@format_with) + else + things + end end end diff --git a/spec/support/matchers/be_valid_json.rb b/spec/support/matchers/be_valid_json.rb index f46c35c7198..228c1fc986e 100644 --- a/spec/support/matchers/be_valid_json.rb +++ b/spec/support/matchers/be_valid_json.rb @@ -1,20 +1,8 @@ # frozen_string_literal: true RSpec::Matchers.define :be_valid_json do - def according_to_schema(schema) - @schema = schema - self - end - match do |actual| - data = Gitlab::Json.parse(actual) - - if @schema.present? - @validation_errors = JSON::Validator.fully_validate(@schema, data) - @validation_errors.empty? - else - data.present? - end + Gitlab::Json.parse(actual).present? rescue JSON::ParserError => e @error = e false @@ -23,8 +11,6 @@ RSpec::Matchers.define :be_valid_json do def failure_message if @error "Parse failed with error: #{@error}" - elsif @validation_errors.present? - "Validation failed because #{@validation_errors.join(', and ')}" else "Parsing did not return any data" end diff --git a/spec/support/matchers/schema_matcher.rb b/spec/support/matchers/schema_matcher.rb index ebbd57c8115..f0e7a52c51e 100644 --- a/spec/support/matchers/schema_matcher.rb +++ b/spec/support/matchers/schema_matcher.rb @@ -2,6 +2,8 @@ module SchemaPath def self.expand(schema, dir = nil) + return schema unless schema.is_a?(String) + if Gitlab.ee? && dir.nil? ee_path = expand(schema, 'ee') @@ -35,7 +37,13 @@ RSpec::Matchers.define :match_schema do |schema, dir: nil, **options| end failure_message do |response| - "didn't match the schema defined by #{SchemaPath.expand(schema, dir)}" \ + "didn't match the schema defined by #{schema_name(schema, dir)}" \ " The validation errors were:\n#{@errors.join("\n")}" end + + def schema_name(schema, dir) + return 'provided schema' unless schema.is_a?(String) + + SchemaPath.expand(schema, dir) + end end diff --git a/spec/support/rspec.rb b/spec/support/rspec.rb index 32f738faa9b..00b9aac7bf4 100644 --- a/spec/support/rspec.rb +++ b/spec/support/rspec.rb @@ -25,4 +25,8 @@ RSpec.configure do |config| config.include FastRailsRoot config.include RuboCop::RSpec::ExpectOffense, type: :rubocop + + config.define_derived_metadata(file_path: %r{spec/rubocop}) do |metadata| + metadata[:type] = :rubocop + end end diff --git a/spec/support/shared_contexts/email_shared_context.rb b/spec/support/shared_contexts/email_shared_context.rb index 298e03162c4..9dffea7c94e 100644 --- a/spec/support/shared_contexts/email_shared_context.rb +++ b/spec/support/shared_contexts/email_shared_context.rb @@ -1,16 +1,16 @@ # frozen_string_literal: true RSpec.shared_context :email_shared_context do - let(:mail_key) { "59d8df8370b7e95c5a49fbf86aeb2c93" } + let(:mail_key) { '59d8df8370b7e95c5a49fbf86aeb2c93' } let(:receiver) { Gitlab::Email::Receiver.new(email_raw) } - let(:markdown) { "![image](uploads/image.png)" } + let(:markdown) { '![image](uploads/image.png)' } def setup_attachment allow_any_instance_of(Gitlab::Email::AttachmentUploader).to receive(:execute).and_return( [ { - url: "uploads/image.png", - alt: "image", + url: 'uploads/image.png', + alt: 'image', markdown: markdown } ] @@ -19,23 +19,252 @@ RSpec.shared_context :email_shared_context do end RSpec.shared_examples :reply_processing_shared_examples do - context "when the user could not be found" do + context 'when the user could not be found' do before do user.destroy! end - it "raises a UserNotFoundError" do + it 'raises a UserNotFoundError' do expect { receiver.execute }.to raise_error(Gitlab::Email::UserNotFoundError) end end - context "when the user is not authorized to the project" do + context 'when the user is not authorized to the project' do before do project.update_attribute(:visibility_level, Project::PRIVATE) end - it "raises a ProjectNotFound" do + it 'raises a ProjectNotFound' do expect { receiver.execute }.to raise_error(Gitlab::Email::ProjectNotFound) end end end + +RSpec.shared_examples :checks_permissions_on_noteable_examples do + context 'when user has access' do + before do + project.add_reporter(user) + end + + it 'creates a comment' do + expect { receiver.execute }.to change { noteable.notes.count }.by(1) + end + end + + context 'when user does not have access' do + it 'raises UserNotAuthorizedError' do + expect { receiver.execute }.to raise_error(Gitlab::Email::UserNotAuthorizedError) + end + end +end + +RSpec.shared_examples :note_handler_shared_examples do |forwardable| + context 'when the noteable could not be found' do + before do + noteable.destroy! + end + + it 'raises a NoteableNotFoundError' do + expect { receiver.execute }.to raise_error(Gitlab::Email::NoteableNotFoundError) + end + end + + context 'when the note could not be saved' do + before do + allow_any_instance_of(Note).to receive(:persisted?).and_return(false) + end + + it 'raises an InvalidNoteError' do + expect { receiver.execute }.to raise_error(Gitlab::Email::InvalidNoteError) + end + + context 'because the note was update commands only' do + let!(:email_raw) { update_commands_only } + + context 'and current user cannot update noteable' do + it 'raises a CommandsOnlyNoteError' do + expect { receiver.execute }.to raise_error(Gitlab::Email::InvalidNoteError) + end + end + + context 'and current user can update noteable' do + before do + project.add_developer(user) + end + + it 'does not raise an error', unless: forwardable do + expect { receiver.execute }.to change { noteable.resource_state_events.count }.by(1) + + expect(noteable.reload).to be_closed + end + + it 'raises an InvalidNoteError', if: forwardable do + expect { receiver.execute }.to raise_error(Gitlab::Email::InvalidNoteError) + end + end + end + end + + context 'when the note contains quick actions' do + let!(:email_raw) { commands_in_reply } + + context 'and current user cannot update the noteable' do + it 'only executes the commands that the user can perform' do + expect { receiver.execute } + .to change { noteable.notes.user.count }.by(1) + .and change { user.todos_pending_count }.from(0).to(1) + + expect(noteable.reload).to be_open + end + end + + context 'and current user can update noteable' do + before do + project.add_developer(user) + end + + it 'posts a note and updates the noteable' do + expect(TodoService.new.todo_exist?(noteable, user)).to be_falsy + + expect { receiver.execute } + .to change { noteable.notes.user.count }.by(1) + .and change { user.todos_pending_count }.from(0).to(1) + + expect(noteable.reload).to be_closed + end + end + end + + context 'when the reply is blank' do + let!(:email_raw) { no_content } + + it 'raises an EmptyEmailError', unless: forwardable do + expect { receiver.execute }.to raise_error(Gitlab::Email::EmptyEmailError) + end + + it 'allows email to only have quoted text', if: forwardable do + expect { receiver.execute }.not_to raise_error(Gitlab::Email::EmptyEmailError) + end + end + + context 'when discussion is locked' do + before do + noteable.update_attribute(:discussion_locked, true) + end + + it_behaves_like :checks_permissions_on_noteable_examples + end + + context 'when everything is fine' do + before do + setup_attachment + end + + it 'adds all attachments' do + expect_next_instance_of(Gitlab::Email::AttachmentUploader) do |uploader| + expect(uploader).to receive(:execute).with(upload_parent: project, uploader_class: FileUploader).and_return( + [ + { + url: 'uploads/image.png', + alt: 'image', + markdown: markdown + } + ] + ) + end + + receiver.execute + + note = noteable.notes.last + expect(note.note).to include(markdown) + expect(note.note).to include('Jake out') + end + end + + context 'when the service desk' do + let(:project) { create(:project, :public, service_desk_enabled: true) } + let(:support_bot) { User.support_bot } + let(:noteable) { create(:issue, project: project, author: support_bot, title: 'service desk issue') } + let!(:note) { create(:note, project: project, noteable: noteable) } + let(:email_raw) { with_quick_actions } + + let!(:sent_notification) do + SentNotification.record_note(note, support_bot.id, mail_key) + end + + context 'is enabled' do + before do + allow(Gitlab::ServiceDesk).to receive(:enabled?).with(project: project).and_return(true) + project.project_feature.update!(issues_access_level: issues_access_level) + end + + context 'when issues are enabled for everyone' do + let(:issues_access_level) { ProjectFeature::ENABLED } + + it 'creates a comment' do + expect { receiver.execute }.to change { noteable.notes.count }.by(1) + end + + context 'when quick actions are present' do + before do + receiver.execute + noteable.reload + end + + context 'when author is a support_bot', unless: forwardable do + it 'encloses quick actions with code span markdown' do + note = Note.last + expect(note.note).to include("Jake out\n\n`/close`\n`/title test`") + expect(noteable.title).to eq('service desk issue') + expect(noteable).to be_opened + end + end + + context 'when author is a normal user', if: forwardable do + it 'extracted the quick actions' do + note = Note.last + expect(note.note).to include('Jake out') + expect(note.note).not_to include("`/close`\n`/title test`") + end + end + end + end + + context 'when issues are protected members only' do + let(:issues_access_level) { ProjectFeature::PRIVATE } + + before do + if recipient.support_bot? + @changed_by = 1 + else + @changed_by = 2 + project.add_developer(recipient) + end + end + + it 'creates a comment' do + expect { receiver.execute }.to change { noteable.notes.count }.by(@changed_by) + end + end + + context 'when issues are disabled' do + let(:issues_access_level) { ProjectFeature::DISABLED } + + it 'does not create a comment' do + expect { receiver.execute }.to raise_error(Gitlab::Email::UserNotAuthorizedError) + end + end + end + + context 'is disabled', unless: forwardable do + before do + allow(Gitlab::ServiceDesk).to receive(:enabled?).and_return(false) + allow(Gitlab::ServiceDesk).to receive(:enabled?).with(project: project).and_return(false) + end + + it 'does not create a comment' do + expect { receiver.execute }.to raise_error(Gitlab::Email::ProjectNotFound) + end + end + end +end diff --git a/spec/support/shared_contexts/finders/issues_finder_shared_contexts.rb b/spec/support/shared_contexts/finders/issues_finder_shared_contexts.rb index 9ffde54c84a..d9cbea58406 100644 --- a/spec/support/shared_contexts/finders/issues_finder_shared_contexts.rb +++ b/spec/support/shared_contexts/finders/issues_finder_shared_contexts.rb @@ -8,13 +8,53 @@ RSpec.shared_context 'IssuesFinder context' do let_it_be(:project1, reload: true) { create(:project, group: group) } let_it_be(:project2, reload: true) { create(:project) } let_it_be(:project3, reload: true) { create(:project, group: subgroup) } - let_it_be(:milestone) { create(:milestone, project: project1) } + let_it_be(:release) { create(:release, project: project1, tag: 'v1.0.0') } + let_it_be(:milestone) { create(:milestone, project: project1, releases: [release]) } let_it_be(:label) { create(:label, project: project2) } let_it_be(:label2) { create(:label, project: project2) } - let_it_be(:issue1, reload: true) { create(:issue, author: user, assignees: [user], project: project1, milestone: milestone, title: 'gitlab', created_at: 1.week.ago, updated_at: 1.week.ago) } - let_it_be(:issue2, reload: true) { create(:issue, author: user, assignees: [user], project: project2, description: 'gitlab', created_at: 1.week.from_now, updated_at: 1.week.from_now) } - let_it_be(:issue3, reload: true) { create(:issue, author: user2, assignees: [user2], project: project2, title: 'tanuki', description: 'tanuki', created_at: 2.weeks.from_now, updated_at: 2.weeks.from_now) } + let_it_be(:issue1, reload: true) do + create(:issue, + author: user, + assignees: [user], + project: project1, + milestone: milestone, + title: 'gitlab', + created_at: 1.week.ago, + updated_at: 1.week.ago) + end + + let_it_be(:issue2, reload: true) do + create(:issue, + author: user, + assignees: [user], + project: project2, + description: 'gitlab', + created_at: 1.week.from_now, + updated_at: 1.week.from_now) + end + + let_it_be(:issue3, reload: true) do + create(:issue, + author: user2, + assignees: [user2], + project: project2, + title: 'tanuki', + description: 'tanuki', + created_at: 2.weeks.from_now, + updated_at: 2.weeks.from_now) + end + let_it_be(:issue4, reload: true) { create(:issue, project: project3) } + let_it_be(:issue5, reload: true) do + create(:issue, + author: user, + assignees: [user], + project: project1, + title: 'wotnot', + created_at: 3.days.ago, + updated_at: 3.days.ago) + end + let_it_be(:award_emoji1) { create(:award_emoji, name: 'thumbsup', user: user, awardable: issue1) } let_it_be(:award_emoji2) { create(:award_emoji, name: 'thumbsup', user: user2, awardable: issue2) } let_it_be(:award_emoji3) { create(:award_emoji, name: 'thumbsdown', user: user, awardable: issue3) } diff --git a/spec/support/shared_contexts/finders/users_finder_shared_contexts.rb b/spec/support/shared_contexts/finders/users_finder_shared_contexts.rb index 54022aeb494..6a09497a497 100644 --- a/spec/support/shared_contexts/finders/users_finder_shared_contexts.rb +++ b/spec/support/shared_contexts/finders/users_finder_shared_contexts.rb @@ -2,6 +2,7 @@ RSpec.shared_context 'UsersFinder#execute filter by project context' do let_it_be(:normal_user) { create(:user, username: 'johndoe') } + let_it_be(:admin_user) { create(:user, :admin, username: 'iamadmin') } let_it_be(:blocked_user) { create(:user, :blocked, username: 'notsorandom') } let_it_be(:external_user) { create(:user, :external) } let_it_be(:omniauth_user) { create(:omniauth_user, provider: 'twitter', extern_uid: '123456') } diff --git a/spec/support/shared_contexts/navbar_structure_context.rb b/spec/support/shared_contexts/navbar_structure_context.rb index 549dc1cff1d..57d8320b76a 100644 --- a/spec/support/shared_contexts/navbar_structure_context.rb +++ b/spec/support/shared_contexts/navbar_structure_context.rb @@ -137,6 +137,7 @@ RSpec.shared_context 'group navbar structure' do _('Projects'), _('Repository'), _('CI / CD'), + _('Packages & Registries'), _('Webhooks') ] } diff --git a/spec/support/shared_contexts/policies/group_policy_shared_context.rb b/spec/support/shared_contexts/policies/group_policy_shared_context.rb index e0e2a18cdd2..e7bc1450601 100644 --- a/spec/support/shared_contexts/policies/group_policy_shared_context.rb +++ b/spec/support/shared_contexts/policies/group_policy_shared_context.rb @@ -19,8 +19,29 @@ RSpec.shared_context 'GroupPolicy context' do end let(:read_group_permissions) { %i[read_label read_list read_milestone read_board] } - let(:reporter_permissions) { %i[admin_label read_container_image read_metrics_dashboard_annotation read_prometheus] } - let(:developer_permissions) { %i[admin_milestone create_metrics_dashboard_annotation delete_metrics_dashboard_annotation update_metrics_dashboard_annotation] } + + let(:reporter_permissions) do + %i[ + admin_label + admin_board + read_container_image + read_metrics_dashboard_annotation + read_prometheus + read_package_settings + ] + end + + let(:developer_permissions) do + %i[ + admin_milestone + create_metrics_dashboard_annotation + delete_metrics_dashboard_annotation + update_metrics_dashboard_annotation + create_custom_emoji + create_package_settings + ] + end + let(:maintainer_permissions) do %i[ create_projects diff --git a/spec/support/shared_contexts/read_ci_configuration_shared_context.rb b/spec/support/shared_contexts/read_ci_configuration_shared_context.rb index f8f33e2a745..04c50171766 100644 --- a/spec/support/shared_contexts/read_ci_configuration_shared_context.rb +++ b/spec/support/shared_contexts/read_ci_configuration_shared_context.rb @@ -5,5 +5,13 @@ RSpec.shared_context 'read ci configuration for sast enabled project' do File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci_for_sast.yml')) end + let_it_be(:gitlab_ci_yml_default_analyzers_content) do + File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci_for_sast_default_analyzers.yml')) + end + + let_it_be(:gitlab_ci_yml_excluded_analyzers_content) do + File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci_for_sast_excluded_analyzers.yml')) + end + let_it_be(:project) { create(:project, :repository) } end diff --git a/spec/support/shared_contexts/requests/api/nuget_packages_shared_context.rb b/spec/support/shared_contexts/requests/api/nuget_packages_shared_context.rb new file mode 100644 index 00000000000..f877d6299bd --- /dev/null +++ b/spec/support/shared_contexts/requests/api/nuget_packages_shared_context.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +RSpec.shared_context 'nuget api setup' do + include WorkhorseHelpers + include PackagesManagerApiSpecHelpers + include HttpBasicAuthHelpers + + let_it_be(:user) { create(:user) } + let_it_be(:personal_access_token) { create(:personal_access_token, user: user) } +end diff --git a/spec/support/shared_examples/controllers/repositories/git_http_controller_shared_examples.rb b/spec/support/shared_examples/controllers/repositories/git_http_controller_shared_examples.rb index 9b738a4b002..00a0fb7e4c5 100644 --- a/spec/support/shared_examples/controllers/repositories/git_http_controller_shared_examples.rb +++ b/spec/support/shared_examples/controllers/repositories/git_http_controller_shared_examples.rb @@ -77,30 +77,6 @@ RSpec.shared_examples Repositories::GitHttpController do end end end - - context 'with exceptions' do - before do - allow(controller).to receive(:authenticate_user).and_return(true) - allow(controller).to receive(:verify_workhorse_api!).and_return(true) - end - - it 'returns 503 with GRPC Unavailable' do - allow(controller).to receive(:access_check).and_raise(GRPC::Unavailable) - - get :info_refs, params: params - - expect(response).to have_gitlab_http_status(:service_unavailable) - end - - it 'returns 503 with timeout error' do - allow(controller).to receive(:access_check).and_raise(Gitlab::GitAccess::TimeoutError) - - get :info_refs, params: params - - expect(response).to have_gitlab_http_status(:service_unavailable) - expect(response.body).to eq 'Gitlab::GitAccess::TimeoutError' - end - end end describe 'POST #git_upload_pack' do diff --git a/spec/support/shared_examples/controllers/sessionless_auth_controller_shared_examples.rb b/spec/support/shared_examples/controllers/sessionless_auth_controller_shared_examples.rb index b67eb0d99fd..041695d8111 100644 --- a/spec/support/shared_examples/controllers/sessionless_auth_controller_shared_examples.rb +++ b/spec/support/shared_examples/controllers/sessionless_auth_controller_shared_examples.rb @@ -1,5 +1,9 @@ # frozen_string_literal: true +# This controller shared examples will be migrated to +# spec/support/shared_examples/requests/sessionless_auth_request_shared_examples.rb +# See also https://gitlab.com/groups/gitlab-org/-/epics/5076 + RSpec.shared_examples 'authenticates sessionless user' do |path, format, params| params ||= {} diff --git a/spec/support/shared_examples/controllers/unique_hll_events_examples.rb b/spec/support/shared_examples/controllers/unique_hll_events_examples.rb index cf7ee17ea13..c5d65743810 100644 --- a/spec/support/shared_examples/controllers/unique_hll_events_examples.rb +++ b/spec/support/shared_examples/controllers/unique_hll_events_examples.rb @@ -7,7 +7,7 @@ RSpec.shared_examples 'tracking unique hll events' do |feature_flag| it 'tracks unique event' do - expect(Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:track_event).with(expected_type, target_id) + expect(Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:track_event).with(target_id, values: expected_type) request end diff --git a/spec/support/shared_examples/features/file_uploads_shared_examples.rb b/spec/support/shared_examples/features/file_uploads_shared_examples.rb index ea8c8d44501..d586bf03b59 100644 --- a/spec/support/shared_examples/features/file_uploads_shared_examples.rb +++ b/spec/support/shared_examples/features/file_uploads_shared_examples.rb @@ -2,28 +2,6 @@ RSpec.shared_examples 'handling file uploads' do |shared_examples_name| context 'with object storage disabled' do - context 'with upload_middleware_jwt_params_handler disabled' do - before do - stub_feature_flags(upload_middleware_jwt_params_handler: false) - - expect_next_instance_of(Gitlab::Middleware::Multipart::Handler) do |handler| - expect(handler).to receive(:with_open_files).and_call_original - end - end - - it_behaves_like shared_examples_name - end - - context 'with upload_middleware_jwt_params_handler enabled' do - before do - stub_feature_flags(upload_middleware_jwt_params_handler: true) - - expect_next_instance_of(Gitlab::Middleware::Multipart::HandlerForJWTParams) do |handler| - expect(handler).to receive(:with_open_files).and_call_original - end - end - - it_behaves_like shared_examples_name - end + it_behaves_like shared_examples_name 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 1dbaace1c89..c2dc87b0fb0 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 @@ -12,9 +12,7 @@ RSpec.shared_examples 'Maintainer manages access requests' do sign_in(maintainer) visit members_page_path - if has_tabs - click_on 'Access requests' - end + click_on 'Access requests' end it 'maintainer can see access requests', :js do @@ -48,11 +46,7 @@ RSpec.shared_examples 'Maintainer manages access requests' do end def expect_visible_access_request(entity, user) - if has_tabs - expect(page).to have_content "Access requests 1" - else - expect(page).to have_content "Users requesting access to #{entity.name} 1" - end + expect(page).to have_content "Access requests 1" expect(page).to have_content user.name end diff --git a/spec/support/shared_examples/features/wiki/user_creates_wiki_page_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_creates_wiki_page_shared_examples.rb index 44d82d2e753..2f8ebd0d264 100644 --- a/spec/support/shared_examples/features/wiki/user_creates_wiki_page_shared_examples.rb +++ b/spec/support/shared_examples/features/wiki/user_creates_wiki_page_shared_examples.rb @@ -20,15 +20,25 @@ RSpec.shared_examples 'User creates wiki page' do click_link "Create your first page" end - it "shows validation error message" do + it "shows validation error message if the form is force submitted", :js do page.within(".wiki-form") do fill_in(:wiki_content, with: "") - click_on("Create page") + page.execute_script("window.onbeforeunload = null") + page.execute_script("document.querySelector('.wiki-form').submit()") end expect(page).to have_content("The form contains the following error:").and have_content("Content can't be blank") + end + + it "disables the submit button", :js do + page.within(".wiki-form") do + fill_in(:wiki_content, with: "") + expect(page).to have_button('Create page', disabled: true) + end + end + it "makes sure links to unknown pages work correctly", :js do page.within(".wiki-form") do fill_in(:wiki_content, with: "[link test](test)") @@ -42,7 +52,7 @@ RSpec.shared_examples 'User creates wiki page' do expect(page).to have_content("Create New Page") end - it "shows non-escaped link in the pages list" do + it "shows non-escaped link in the pages list", :js do fill_in(:wiki_title, with: "one/two/three-test") page.within(".wiki-form") do @@ -61,7 +71,7 @@ RSpec.shared_examples 'User creates wiki page' do expect(page).to have_field("wiki[message]", with: "Create home") end - it "creates a page from the home page" do + it "creates a page from the home page", :js do fill_in(:wiki_content, with: "[test](test)\n[GitLab API doc](api)\n[Rake tasks](raketasks)\n# Wiki header\n") fill_in(:wiki_message, with: "Adding links to wiki") @@ -79,7 +89,7 @@ RSpec.shared_examples 'User creates wiki page' do expect(current_path).to eq(wiki_page_path(wiki, "test")) - page.within(:css, ".nav-text") do + page.within(:css, ".wiki-page-header") do expect(page).to have_content("Create New Page") end @@ -91,7 +101,7 @@ RSpec.shared_examples 'User creates wiki page' do expect(current_path).to eq(wiki_page_path(wiki, "api")) - page.within(:css, ".nav-text") do + page.within(:css, ".wiki-page-header") do expect(page).to have_content("Create") end @@ -103,7 +113,7 @@ RSpec.shared_examples 'User creates wiki page' do expect(current_path).to eq(wiki_page_path(wiki, "raketasks")) - page.within(:css, ".nav-text") do + page.within(:css, ".wiki-page-header") do expect(page).to have_content("Create") end end @@ -142,7 +152,7 @@ RSpec.shared_examples 'User creates wiki page' do end end - it 'creates a wiki page with Org markup', :aggregate_failures do + it 'creates a wiki page with Org markup', :aggregate_failures, :js do org_content = <<~ORG * Heading ** Subheading @@ -170,7 +180,7 @@ RSpec.shared_examples 'User creates wiki page' do visit wiki_path(wiki) end - context "via the `new wiki page` page" do + context "via the `new wiki page` page", :js do it "creates a page with a single word" do click_link("New page") @@ -189,7 +199,7 @@ RSpec.shared_examples 'User creates wiki page' do .and have_content("My awesome wiki!") end - it "creates a page with spaces in the name" do + it "creates a page with spaces in the name", :js do click_link("New page") page.within(".wiki-form") do @@ -207,7 +217,7 @@ RSpec.shared_examples 'User creates wiki page' do .and have_content("My awesome wiki!") end - it "creates a page with hyphens in the name" do + it "creates a page with hyphens in the name", :js do click_link("New page") page.within(".wiki-form") do 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 3350e54a8a7..1e325535e81 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 @@ -90,9 +90,11 @@ RSpec.shared_examples 'User updates wiki page' do expect(page).to have_field('wiki[message]', with: 'Update Wiki title') end - it 'shows a validation error message' do + it 'shows a validation error message if the form is force submitted', :js do fill_in(:wiki_content, with: '') - click_button('Save changes') + + page.execute_script("window.onbeforeunload = null") + page.execute_script("document.querySelector('.wiki-form').submit()") expect(page).to have_selector('.wiki-form') expect(page).to have_content('Edit Page') @@ -101,6 +103,13 @@ RSpec.shared_examples 'User updates wiki page' do expect(find('textarea#wiki_content').value).to eq('') end + it "disables the submit button", :js do + page.within(".wiki-form") do + fill_in(:wiki_content, with: "") + expect(page).to have_button('Save changes', disabled: true) + end + end + it 'shows the emoji autocompletion dropdown', :js do find('#wiki_content').native.send_keys('') fill_in(:wiki_content, with: ':') @@ -108,7 +117,7 @@ RSpec.shared_examples 'User updates wiki page' do expect(page).to have_selector('.atwho-view') end - it 'shows the error message' do + it 'shows the error message', :js do wiki_page.update(content: 'Update') # rubocop:disable Rails/SaveBang click_button('Save changes') @@ -116,13 +125,17 @@ RSpec.shared_examples 'User updates wiki page' do expect(page).to have_content('Someone edited the page the same time you did.') end - it 'updates a page' do + it 'updates a page', :js do fill_in('Content', with: 'Updated Wiki Content') click_on('Save changes') expect(page).to have_content('Updated Wiki Content') end + it 'focuses on the content field', :js do + expect(page).to have_selector '.note-textarea:focus' + end + it 'cancels editing of a page' do page.within(:css, '.wiki-form .form-actions') do click_on('Cancel') @@ -143,7 +156,7 @@ RSpec.shared_examples 'User updates wiki page' do visit wiki_page_path(wiki, wiki_page, action: :edit) end - it 'moves the page to the root folder' do + it 'moves the page to the root folder', :js do fill_in(:wiki_title, with: "/#{page_name}") click_button('Save changes') @@ -151,7 +164,7 @@ RSpec.shared_examples 'User updates wiki page' do expect(current_path).to eq(wiki_page_path(wiki, page_name)) end - it 'moves the page to other dir' do + it 'moves the page to other dir', :js do new_page_dir = "foo1/bar1/#{page_name}" fill_in(:wiki_title, with: new_page_dir) @@ -161,7 +174,7 @@ RSpec.shared_examples 'User updates wiki page' do expect(current_path).to eq(wiki_page_path(wiki, new_page_dir)) end - it 'remains in the same place if title has not changed' do + it 'remains in the same place if title has not changed', :js do original_path = wiki_page_path(wiki, wiki_page) fill_in(:wiki_title, with: page_name) @@ -171,7 +184,7 @@ RSpec.shared_examples 'User updates wiki page' do expect(current_path).to eq(original_path) end - it 'can be moved to a different dir with a different name' do + it 'can be moved to a different dir with a different name', :js do new_page_dir = "foo1/bar1/new_page_name" fill_in(:wiki_title, with: new_page_dir) @@ -181,7 +194,7 @@ RSpec.shared_examples 'User updates wiki page' do expect(current_path).to eq(wiki_page_path(wiki, new_page_dir)) end - it 'can be renamed and moved to the root folder' do + it 'can be renamed and moved to the root folder', :js do new_name = 'new_page_name' fill_in(:wiki_title, with: "/#{new_name}") @@ -191,7 +204,7 @@ RSpec.shared_examples 'User updates wiki page' do expect(current_path).to eq(wiki_page_path(wiki, new_name)) end - it 'squishes the title before creating the page' do + it 'squishes the title before creating the page', :js do new_page_dir = " foo1 / bar1 / #{page_name} " fill_in(:wiki_title, with: new_page_dir) @@ -220,7 +233,7 @@ RSpec.shared_examples 'User updates wiki page' do expect(page).to have_content('Wiki page was successfully updated.') end - it 'shows a validation error when trying to change the content' do + it 'shows a validation error when trying to change the content', :js do fill_in 'Content', with: 'new content' click_on 'Save changes' diff --git a/spec/support/shared_examples/features/wiki/user_uses_wiki_shortcuts_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_uses_wiki_shortcuts_shared_examples.rb index 759cfaf6b1f..857d923785f 100644 --- a/spec/support/shared_examples/features/wiki/user_uses_wiki_shortcuts_shared_examples.rb +++ b/spec/support/shared_examples/features/wiki/user_uses_wiki_shortcuts_shared_examples.rb @@ -15,6 +15,6 @@ RSpec.shared_examples 'User uses wiki shortcuts' do it 'visit edit wiki page using "e" keyboard shortcut', :js do find('body').native.send_key('e') - expect(find('.wiki-page-title')).to have_content('Edit Page') + expect(find('.page-title')).to have_content('Edit Page') end end diff --git a/spec/support/shared_examples/features/wiki/user_views_wiki_empty_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_views_wiki_empty_shared_examples.rb index d7f5b485a82..14180d503df 100644 --- a/spec/support/shared_examples/features/wiki/user_views_wiki_empty_shared_examples.rb +++ b/spec/support/shared_examples/features/wiki/user_views_wiki_empty_shared_examples.rb @@ -53,7 +53,7 @@ RSpec.shared_examples 'User views empty wiki' do if writable element.click_link 'Create your first page' - expect(page).to have_button('Create page') + expect(page).to have_button('Create page', disabled: true) else expect(element).not_to have_link('Create your first page') end 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 af769be6d4b..61feeff57bb 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 @@ -44,7 +44,7 @@ RSpec.shared_examples 'User views a wiki page' do expect(current_path).to include('one/two/three-test') - page.within(:css, '.nav-text') do + page.within(:css, '.wiki-page-header') do expect(page).to have_content('History') end end @@ -69,7 +69,7 @@ RSpec.shared_examples 'User views a wiki page' do click_on('Page history') - within('.nav-text') do + within('.wiki-page-header') do expect(page).to have_content('History') end diff --git a/spec/support/shared_examples/features/wiki/user_views_wiki_sidebar_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_views_wiki_sidebar_shared_examples.rb index a7ba7a8ad07..639eb3f2b99 100644 --- a/spec/support/shared_examples/features/wiki/user_views_wiki_sidebar_shared_examples.rb +++ b/spec/support/shared_examples/features/wiki/user_views_wiki_sidebar_shared_examples.rb @@ -17,23 +17,55 @@ RSpec.shared_examples 'User views wiki sidebar' do create(:wiki_page, wiki: wiki, title: 'another', content: 'another') end - it 'renders a default sidebar when there is no customized sidebar' do - visit wiki_path(wiki) + context 'when there is no custom sidebar' do + before do + visit wiki_path(wiki) + end - expect(page).to have_content('another') - expect(page).not_to have_link('View All Pages') + it 'renders a default sidebar' do + within('.right-sidebar') do + expect(page).to have_content('another') + expect(page).not_to have_link('View All Pages') + end + end + + it 'can create a custom sidebar', :js do + click_on 'Edit sidebar' + fill_in :wiki_content, with: 'My custom sidebar' + click_on 'Create page' + + within('.right-sidebar') do + expect(page).to have_content('My custom sidebar') + expect(page).not_to have_content('another') + end + end end - context 'when there is a customized sidebar' do + context 'when there is a custom sidebar' do before do - create(:wiki_page, wiki: wiki, title: '_sidebar', content: 'My customized sidebar') - end + create(:wiki_page, wiki: wiki, title: '_sidebar', content: 'My custom sidebar') - it 'renders my customized sidebar instead of the default one' do visit wiki_path(wiki) + end + + it 'renders the custom sidebar instead of the default one' do + within('.right-sidebar') do + expect(page).to have_content('My custom sidebar') + expect(page).not_to have_content('another') + end + end + + it 'can edit the custom sidebar', :js do + click_on 'Edit sidebar' + + expect(page).to have_field(:wiki_content, with: 'My custom sidebar') + + fill_in :wiki_content, with: 'My other custom sidebar' + click_on 'Save changes' - expect(page).to have_content('My customized sidebar') - expect(page).not_to have_content('Another') + within('.right-sidebar') do + expect(page).to have_content('My other custom sidebar') + end end end end diff --git a/spec/support/shared_examples/finders/packages/debian/distributions_finder_shared_examples.rb b/spec/support/shared_examples/finders/packages/debian/distributions_finder_shared_examples.rb new file mode 100644 index 00000000000..2700d29bf0e --- /dev/null +++ b/spec/support/shared_examples/finders/packages/debian/distributions_finder_shared_examples.rb @@ -0,0 +1,79 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.shared_examples 'Debian Distributions Finder' do |factory, can_freeze| + let_it_be(:distribution_with_suite, freeze: can_freeze) { create(factory, suite: 'mysuite') } + let_it_be(:container) { distribution_with_suite.container } + let_it_be(:distribution_with_same_container, freeze: can_freeze) { create(factory, container: container ) } + let_it_be(:distribution_with_same_codename, freeze: can_freeze) { create(factory, codename: distribution_with_suite.codename ) } + let_it_be(:distribution_with_same_suite, freeze: can_freeze) { create(factory, suite: distribution_with_suite.suite ) } + let_it_be(:distribution_with_codename_and_suite_flipped, freeze: can_freeze) { create(factory, codename: distribution_with_suite.suite, suite: distribution_with_suite.codename) } + + let(:params) { {} } + let(:service) { described_class.new(container, params) } + + subject { service.execute.to_a } + + context 'by codename' do + context 'with existing codename' do + let(:params) { { codename: distribution_with_suite.codename } } + + it 'finds distributions by codename' do + is_expected.to contain_exactly(distribution_with_suite) + end + end + + context 'with non-existing codename' do + let(:params) { { codename: 'does_not_exists' } } + + it 'finds nothing' do + is_expected.to be_empty + end + end + end + + context 'by suite' do + context 'with existing suite' do + let(:params) { { suite: 'mysuite' } } + + it 'finds distribution by suite' do + is_expected.to contain_exactly(distribution_with_suite) + end + end + + context 'with non-existing suite' do + let(:params) { { suite: 'does_not_exists' } } + + it 'finds nothing' do + is_expected.to be_empty + end + end + end + + context 'by codename_or_suite' do + context 'with existing codename' do + let(:params) { { codename_or_suite: distribution_with_suite.codename } } + + it 'finds distribution by codename' do + is_expected.to contain_exactly(distribution_with_suite) + end + end + + context 'with existing suite' do + let(:params) { { codename_or_suite: 'mysuite' } } + + it 'finds distribution by suite' do + is_expected.to contain_exactly(distribution_with_suite) + end + end + + context 'with non-existing suite' do + let(:params) { { codename_or_suite: 'does_not_exists' } } + + it 'finds nothing' do + is_expected.to be_empty + end + end + end +end diff --git a/spec/support/shared_examples/finders/packages_shared_examples.rb b/spec/support/shared_examples/finders/packages_shared_examples.rb new file mode 100644 index 00000000000..52976565b21 --- /dev/null +++ b/spec/support/shared_examples/finders/packages_shared_examples.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'concerning versionless param' do + let_it_be(:versionless_package) { create(:maven_package, project: project, version: nil) } + + it { is_expected.not_to include(versionless_package) } + + context 'with valid include_versionless param' do + let(:params) { { include_versionless: true } } + + it { is_expected.to include(versionless_package) } + end + + context 'with empty include_versionless param' do + let(:params) { { include_versionless: '' } } + + it { is_expected.not_to include(versionless_package) } + end +end diff --git a/spec/support/shared_examples/graphql/mutations/http_integrations_shared_examples.rb b/spec/support/shared_examples/graphql/mutations/http_integrations_shared_examples.rb new file mode 100644 index 00000000000..0338eb43f8d --- /dev/null +++ b/spec/support/shared_examples/graphql/mutations/http_integrations_shared_examples.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'creating a new HTTP integration' do + it 'creates a new integration' do + post_graphql_mutation(mutation, current_user: current_user) + + new_integration = ::AlertManagement::HttpIntegration.last! + integration_response = mutation_response['integration'] + + expect(response).to have_gitlab_http_status(:success) + expect(integration_response['id']).to eq(GitlabSchema.id_from_object(new_integration).to_s) + expect(integration_response['type']).to eq('HTTP') + expect(integration_response['name']).to eq(new_integration.name) + expect(integration_response['active']).to eq(new_integration.active) + expect(integration_response['token']).to eq(new_integration.token) + expect(integration_response['url']).to eq(new_integration.url) + expect(integration_response['apiUrl']).to eq(nil) + end +end diff --git a/spec/support/shared_examples/graphql/projects/merge_request_n_plus_one_query_examples.rb b/spec/support/shared_examples/graphql/projects/merge_request_n_plus_one_query_examples.rb index 397e22ace28..738edd43c92 100644 --- a/spec/support/shared_examples/graphql/projects/merge_request_n_plus_one_query_examples.rb +++ b/spec/support/shared_examples/graphql/projects/merge_request_n_plus_one_query_examples.rb @@ -2,10 +2,12 @@ shared_examples 'N+1 query check' do it 'prevents N+1 queries' do execute_query # "warm up" to prevent undeterministic counts + expect(graphql_errors).to be_blank # Sanity check - ex falso quodlibet! - control_count = ActiveRecord::QueryRecorder.new { execute_query }.count + control = ActiveRecord::QueryRecorder.new { execute_query } + expect(control.count).to be > 0 search_params[:iids] << extra_iid_for_second_query - expect { execute_query }.not_to exceed_query_limit(control_count) + expect { execute_query }.not_to exceed_query_limit(control) end end diff --git a/spec/support/shared_examples/lib/banzai/filters/sanitization_filter_shared_examples.rb b/spec/support/shared_examples/lib/banzai/filters/sanitization_filter_shared_examples.rb index 134e38833cf..b5c07f45d59 100644 --- a/spec/support/shared_examples/lib/banzai/filters/sanitization_filter_shared_examples.rb +++ b/spec/support/shared_examples/lib/banzai/filters/sanitization_filter_shared_examples.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true -RSpec.shared_examples 'default whitelist' do - it 'sanitizes tags that are not whitelisted' do +RSpec.shared_examples 'default allowlist' do + it 'sanitizes tags that are not allowed' do act = %q{<textarea>no inputs</textarea> and <blink>no blinks</blink>} exp = 'no inputs and no blinks' expect(filter(act).to_html).to eq exp diff --git a/spec/support/shared_examples/lib/gitlab/cycle_analytics/base_stage_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/cycle_analytics/base_stage_shared_examples.rb deleted file mode 100644 index d76089d56dd..00000000000 --- a/spec/support/shared_examples/lib/gitlab/cycle_analytics/base_stage_shared_examples.rb +++ /dev/null @@ -1,74 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -ISSUES_MEDIAN = 30.minutes.to_i - -RSpec.shared_examples 'base stage' do - let(:stage) { described_class.new(options: { project: double }) } - - before do - allow(stage).to receive(:project_median).and_return(1.12) - allow_next_instance_of(Gitlab::CycleAnalytics::BaseEventFetcher) do |instance| - allow(instance).to receive(:event_result).and_return({}) - end - end - - it 'has the median data value' do - expect(stage.as_json[:value]).not_to be_nil - end - - it 'has the median data stage' do - expect(stage.as_json[:title]).not_to be_nil - end - - it 'has the median data description' do - expect(stage.as_json[:description]).not_to be_nil - end - - it 'has the title' do - expect(stage.title).to eq(stage_name.to_s.capitalize) - end - - it 'has the events' do - expect(stage.events).not_to be_nil - end -end - -RSpec.shared_examples 'calculate #median with date range' do - context 'when valid date range is given' do - before do - stage_options[:from] = 5.days.ago - stage_options[:to] = 5.days.from_now - end - - it { expect(stage.project_median).to eq(ISSUES_MEDIAN) } - end - - context 'when records are out of the date range' do - before do - stage_options[:from] = 2.years.ago - stage_options[:to] = 1.year.ago - end - - it { expect(stage.project_median).to eq(nil) } - end -end - -RSpec.shared_examples 'Gitlab::Analytics::CycleAnalytics::DataCollector backend examples' do - let(:stage_params) { Gitlab::Analytics::CycleAnalytics::DefaultStages.send("params_for_#{stage_name}_stage").merge(project: project) } - let(:stage) { Analytics::CycleAnalytics::ProjectStage.new(stage_params) } - let(:data_collector) { Gitlab::Analytics::CycleAnalytics::DataCollector.new(stage: stage, params: { from: stage_options[:from], current_user: project.creator }) } - let(:attribute_to_verify) { :title } - - context 'provides the same results as the old implementation' do - it 'for the median' do - expect(data_collector.median.seconds).to be_within(0.5).of(ISSUES_MEDIAN) - end - - it 'for the list of event records' do - records = data_collector.records_fetcher.serialized_records - expect(records.map { |event| event[attribute_to_verify] }).to eq(expected_ordered_attribute_values) - end - end -end diff --git a/spec/support/shared_examples/lib/gitlab/cycle_analytics/default_query_config_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/cycle_analytics/default_query_config_shared_examples.rb deleted file mode 100644 index 4f648b27ea2..00000000000 --- a/spec/support/shared_examples/lib/gitlab/cycle_analytics/default_query_config_shared_examples.rb +++ /dev/null @@ -1,16 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.shared_examples 'default query config' do - let(:project) { create(:project) } - let(:event) { described_class.new(stage: stage_name, options: { from: 1.day.ago, project: project }) } - - it 'has the stage attribute' do - expect(event.stage).not_to be_nil - end - - it 'has the projection attributes' do - expect(event.projections).not_to be_nil - end -end diff --git a/spec/support/shared_examples/lib/gitlab/middleware/multipart_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/middleware/multipart_shared_examples.rb index 6327367fcc2..40deaa27955 100644 --- a/spec/support/shared_examples/lib/gitlab/middleware/multipart_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/middleware/multipart_shared_examples.rb @@ -5,7 +5,7 @@ RSpec.shared_examples 'handling all upload parameters conditions' do include_context 'with one temporary file for multipart' let(:rewritten_fields) { rewritten_fields_hash('file' => uploaded_filepath) } - let(:params) { upload_parameters_for(filepath: uploaded_filepath, key: 'file', filename: filename, remote_id: remote_id) } + let(:params) { upload_parameters_for(filepath: uploaded_filepath, key: 'file', mode: mode, filename: filename, remote_id: remote_id) } it 'builds an UploadedFile' do expect_uploaded_files(filepath: uploaded_filepath, original_filename: filename, remote_id: remote_id, size: uploaded_file.size, params_path: %w(file)) @@ -19,8 +19,8 @@ RSpec.shared_examples 'handling all upload parameters conditions' do let(:rewritten_fields) { rewritten_fields_hash('file1' => uploaded_filepath, 'file2' => uploaded_filepath2) } let(:params) do - upload_parameters_for(filepath: uploaded_filepath, key: 'file1', filename: filename, remote_id: remote_id).merge( - upload_parameters_for(filepath: uploaded_filepath2, key: 'file2', filename: filename2, remote_id: remote_id2) + upload_parameters_for(filepath: uploaded_filepath, key: 'file1', mode: mode, filename: filename, remote_id: remote_id).merge( + upload_parameters_for(filepath: uploaded_filepath2, key: 'file2', mode: mode, filename: filename2, remote_id: remote_id2) ) end @@ -38,7 +38,7 @@ RSpec.shared_examples 'handling all upload parameters conditions' do include_context 'with one temporary file for multipart' let(:rewritten_fields) { rewritten_fields_hash('user[avatar]' => uploaded_filepath) } - let(:params) { { 'user' => { 'avatar' => upload_parameters_for(filepath: uploaded_filepath, filename: filename, remote_id: remote_id) } } } + let(:params) { { 'user' => { 'avatar' => upload_parameters_for(filepath: uploaded_filepath, mode: mode, filename: filename, remote_id: remote_id) } } } it 'builds an UploadedFile' do expect_uploaded_files(filepath: uploaded_filepath, original_filename: filename, remote_id: remote_id, size: uploaded_file.size, params_path: %w(user avatar)) @@ -54,8 +54,8 @@ RSpec.shared_examples 'handling all upload parameters conditions' do let(:params) do { 'user' => { - 'avatar' => upload_parameters_for(filepath: uploaded_filepath, filename: filename, remote_id: remote_id), - 'screenshot' => upload_parameters_for(filepath: uploaded_filepath2, filename: filename2, remote_id: remote_id2) + 'avatar' => upload_parameters_for(filepath: uploaded_filepath, mode: mode, filename: filename, remote_id: remote_id), + 'screenshot' => upload_parameters_for(filepath: uploaded_filepath2, mode: mode, filename: filename2, remote_id: remote_id2) } } end @@ -74,7 +74,7 @@ RSpec.shared_examples 'handling all upload parameters conditions' do include_context 'with one temporary file for multipart' let(:rewritten_fields) { rewritten_fields_hash('user[avatar][bananas]' => uploaded_filepath) } - let(:params) { { 'user' => { 'avatar' => { 'bananas' => upload_parameters_for(filepath: uploaded_filepath, filename: filename, remote_id: remote_id) } } } } + let(:params) { { 'user' => { 'avatar' => { 'bananas' => upload_parameters_for(filepath: uploaded_filepath, mode: mode, filename: filename, remote_id: remote_id) } } } } it 'builds an UploadedFile' do expect_uploaded_files(filepath: uploaded_file, original_filename: filename, remote_id: remote_id, size: uploaded_file.size, params_path: %w(user avatar bananas)) @@ -91,10 +91,10 @@ RSpec.shared_examples 'handling all upload parameters conditions' do { 'user' => { 'avatar' => { - 'bananas' => upload_parameters_for(filepath: uploaded_filepath, filename: filename, remote_id: remote_id) + 'bananas' => upload_parameters_for(filepath: uploaded_filepath, mode: mode, filename: filename, remote_id: remote_id) }, 'friend' => { - 'ananas' => upload_parameters_for(filepath: uploaded_filepath2, filename: filename2, remote_id: remote_id2) + 'ananas' => upload_parameters_for(filepath: uploaded_filepath2, mode: mode, filename: filename2, remote_id: remote_id2) } } } @@ -122,11 +122,11 @@ RSpec.shared_examples 'handling all upload parameters conditions' do end let(:params) do - upload_parameters_for(filepath: uploaded_filepath, filename: filename, key: 'file', remote_id: remote_id).merge( + upload_parameters_for(filepath: uploaded_filepath, filename: filename, key: 'file', mode: mode, remote_id: remote_id).merge( 'user' => { - 'avatar' => upload_parameters_for(filepath: uploaded_filepath2, filename: filename2, remote_id: remote_id2), + 'avatar' => upload_parameters_for(filepath: uploaded_filepath2, mode: mode, filename: filename2, remote_id: remote_id2), 'friend' => { - 'avatar' => upload_parameters_for(filepath: uploaded_filepath3, filename: filename3, remote_id: remote_id3) + 'avatar' => upload_parameters_for(filepath: uploaded_filepath3, mode: mode, filename: filename3, remote_id: remote_id3) } } ) diff --git a/spec/support/shared_examples/lib/gitlab/project_search_results_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/project_search_results_shared_examples.rb index 94ef41ce5a5..f83fecee4ea 100644 --- a/spec/support/shared_examples/lib/gitlab/project_search_results_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/project_search_results_shared_examples.rb @@ -54,7 +54,7 @@ RSpec.shared_examples 'access restricted confidential issues' do end end - context 'when the user is a developper' do + context 'when the user is a developer' do let(:user) do create(:user) { |user| project.add_developer(user) } end @@ -70,10 +70,19 @@ RSpec.shared_examples 'access restricted confidential issues' do context 'when the user is admin', :request_store do let(:user) { create(:user, admin: true) } - it 'lists all project issues' do - expect(objects).to contain_exactly(issue, - security_issue_1, - security_issue_2) + context 'when admin mode is enabled', :enable_admin_mode do + it 'lists all project issues' do + expect(objects).to contain_exactly(issue, + security_issue_1, + security_issue_2) + end + end + + context 'when admin mode is disabled' do + it 'does not list project confidential issues' do + expect(objects).to contain_exactly(issue) + expect(results.limited_issues_count).to eq 1 + end end end end diff --git a/spec/support/shared_examples/lib/gitlab/usage_data_counters/incident_management_activity_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/usage_data_counters/incident_management_activity_shared_examples.rb index 788c35dd5bf..88bc8e8d0c1 100644 --- a/spec/support/shared_examples/lib/gitlab/usage_data_counters/incident_management_activity_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/usage_data_counters/incident_management_activity_shared_examples.rb @@ -13,7 +13,7 @@ RSpec.shared_examples 'an incident management tracked event' do |event| expect(Gitlab::UsageDataCounters::HLLRedisCounter) .to receive(:track_event) - .with(current_user.id, event.to_s) + .with(event.to_s, values: current_user.id) .and_call_original expect { subject } diff --git a/spec/support/shared_examples/metrics/sampler_shared_examples.rb b/spec/support/shared_examples/metrics/sampler_shared_examples.rb new file mode 100644 index 00000000000..ebf199c3a8d --- /dev/null +++ b/spec/support/shared_examples/metrics/sampler_shared_examples.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'metrics sampler' do |env_prefix| + context 'when sampling interval is passed explicitly' do + subject { described_class.new(42) } + + specify { expect(subject.interval).to eq(42) } + end + + context 'when sampling interval is passed through the environment' do + subject { described_class.new } + + before do + stub_env("#{env_prefix}_INTERVAL_SECONDS", '42') + end + + specify { expect(subject.interval).to eq(42) } + end + + context 'when no sampling interval is passed anywhere' do + subject { described_class.new } + + it 'uses the hardcoded default' do + expect(subject.interval).to eq(described_class::DEFAULT_SAMPLING_INTERVAL_SECONDS) + end + end +end diff --git a/spec/support/shared_examples/models/boards/listable_shared_examples.rb b/spec/support/shared_examples/models/boards/listable_shared_examples.rb new file mode 100644 index 00000000000..e733a5488fb --- /dev/null +++ b/spec/support/shared_examples/models/boards/listable_shared_examples.rb @@ -0,0 +1,97 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'boards listable model' do |list_factory| + subject { build(list_factory) } + + describe 'associations' do + it { is_expected.to validate_presence_of(:position) } + it { is_expected.to validate_numericality_of(:position).only_integer.is_greater_than_or_equal_to(0) } + + context 'when list_type is set to closed' do + subject { build(list_factory, list_type: :closed) } + + it { is_expected.not_to validate_presence_of(:label) } + it { is_expected.not_to validate_presence_of(:position) } + end + end + + describe 'scopes' do + describe '.ordered' do + it 'returns lists ordered by type and position' do + # rubocop:disable Rails/SaveBang + lists = [ + create(list_factory, list_type: :backlog), + create(list_factory, list_type: :closed), + create(list_factory, position: 1), + create(list_factory, position: 2) + ] + # rubocop:enable Rails/SaveBang + + expect(described_class.where(id: lists).ordered).to eq([lists[0], lists[2], lists[3], lists[1]]) + end + end + end + + describe '#destroyable?' do + it 'returns true when list_type is set to label' do + subject.list_type = :label + + expect(subject).to be_destroyable + end + + it 'returns false when list_type is set to closed' do + subject.list_type = :closed + + expect(subject).not_to be_destroyable + end + end + + describe '#movable?' do + it 'returns true when list_type is set to label' do + subject.list_type = :label + + expect(subject).to be_movable + end + + it 'returns false when list_type is set to closed' do + subject.list_type = :closed + + expect(subject).not_to be_movable + end + end + + describe '#title' do + it 'returns label name when list_type is set to label' do + subject.list_type = :label + subject.label = Label.new(name: 'Development') + + expect(subject.title).to eq 'Development' + end + + it 'returns Open when list_type is set to backlog' do + subject.list_type = :backlog + + expect(subject.title).to eq 'Open' + end + + it 'returns Closed when list_type is set to closed' do + subject.list_type = :closed + + expect(subject.title).to eq 'Closed' + end + end + + describe '#destroy' do + it 'can be destroyed when list_type is set to label' do + subject = create(list_factory) # rubocop:disable Rails/SaveBang + + expect(subject.destroy).to be_truthy + end + + it 'can not be destroyed when list_type is set to closed' do + subject = create(list_factory, list_type: :closed) # rubocop:disable Rails/SaveBang + + expect(subject.destroy).to be_falsey + end + end +end diff --git a/spec/support/shared_examples/models/concerns/can_housekeep_repository_shared_examples.rb b/spec/support/shared_examples/models/concerns/can_housekeep_repository_shared_examples.rb new file mode 100644 index 00000000000..2f0b95427d2 --- /dev/null +++ b/spec/support/shared_examples/models/concerns/can_housekeep_repository_shared_examples.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'can housekeep repository' do + context 'with a clean redis state', :clean_gitlab_redis_shared_state do + describe '#pushes_since_gc' do + context 'without any pushes' do + it 'returns 0' do + expect(resource.pushes_since_gc).to eq(0) + end + end + + context 'with a number of pushes' do + it 'returns the number of pushes' do + 3.times { resource.increment_pushes_since_gc } + + expect(resource.pushes_since_gc).to eq(3) + end + end + end + + describe '#increment_pushes_since_gc' do + it 'increments the number of pushes since the last GC' do + 3.times { resource.increment_pushes_since_gc } + + expect(resource.pushes_since_gc).to eq(3) + end + end + + describe '#reset_pushes_since_gc' do + it 'resets the number of pushes since the last GC' do + 3.times { resource.increment_pushes_since_gc } + + resource.reset_pushes_since_gc + + expect(resource.pushes_since_gc).to eq(0) + end + end + + describe '#pushes_since_gc_redis_shared_state_key' do + it 'returns the proper redis key format' do + expect(resource.send(:pushes_since_gc_redis_shared_state_key)).to eq("#{resource_key}/#{resource.id}/pushes_since_gc") + end + end + end +end diff --git a/spec/support/shared_examples/models/concerns/repositories/can_housekeep_repository_shared_examples.rb b/spec/support/shared_examples/models/concerns/repositories/can_housekeep_repository_shared_examples.rb new file mode 100644 index 00000000000..2f0b95427d2 --- /dev/null +++ b/spec/support/shared_examples/models/concerns/repositories/can_housekeep_repository_shared_examples.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'can housekeep repository' do + context 'with a clean redis state', :clean_gitlab_redis_shared_state do + describe '#pushes_since_gc' do + context 'without any pushes' do + it 'returns 0' do + expect(resource.pushes_since_gc).to eq(0) + end + end + + context 'with a number of pushes' do + it 'returns the number of pushes' do + 3.times { resource.increment_pushes_since_gc } + + expect(resource.pushes_since_gc).to eq(3) + end + end + end + + describe '#increment_pushes_since_gc' do + it 'increments the number of pushes since the last GC' do + 3.times { resource.increment_pushes_since_gc } + + expect(resource.pushes_since_gc).to eq(3) + end + end + + describe '#reset_pushes_since_gc' do + it 'resets the number of pushes since the last GC' do + 3.times { resource.increment_pushes_since_gc } + + resource.reset_pushes_since_gc + + expect(resource.pushes_since_gc).to eq(0) + end + end + + describe '#pushes_since_gc_redis_shared_state_key' do + it 'returns the proper redis key format' do + expect(resource.send(:pushes_since_gc_redis_shared_state_key)).to eq("#{resource_key}/#{resource.id}/pushes_since_gc") + end + end + end +end diff --git a/spec/support/shared_examples/models/concerns/repository_storage_movable_shared_examples.rb b/spec/support/shared_examples/models/concerns/repository_storage_movable_shared_examples.rb index 5a8388d01df..4c617f3ba46 100644 --- a/spec/support/shared_examples/models/concerns/repository_storage_movable_shared_examples.rb +++ b/spec/support/shared_examples/models/concerns/repository_storage_movable_shared_examples.rb @@ -63,7 +63,6 @@ RSpec.shared_examples 'handles repository moves' do context 'and transits to scheduled' do it 'triggers the corresponding repository storage worker' do - skip unless repository_storage_worker # TODO remove after https://gitlab.com/gitlab-org/gitlab/-/issues/218991 is implemented expect(repository_storage_worker).to receive(:perform_async).with(container.id, 'test_second_storage', storage_move.id) storage_move.schedule! @@ -72,8 +71,7 @@ RSpec.shared_examples 'handles repository moves' do end context 'when the transition fails' do - it 'does not trigger ProjectUpdateRepositoryStorageWorker and adds an error' do - skip unless repository_storage_worker # TODO remove after https://gitlab.com/gitlab-org/gitlab/-/issues/218991 is implemented + it 'does not trigger the corresponding repository storage worker and adds an error' do allow(storage_move.container).to receive(:set_repository_read_only!).and_raise(StandardError, 'foobar') expect(repository_storage_worker).not_to receive(:perform_async) diff --git a/spec/support/shared_examples/models/packages/debian/architecture_shared_examples.rb b/spec/support/shared_examples/models/packages/debian/architecture_shared_examples.rb new file mode 100644 index 00000000000..38983f752f4 --- /dev/null +++ b/spec/support/shared_examples/models/packages/debian/architecture_shared_examples.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.shared_examples 'Debian Distribution Architecture' do |factory, container, can_freeze| + let_it_be_with_refind(:architecture) { create(factory) } # rubocop:disable Rails/SaveBang + let_it_be(:architecture_same_distribution, freeze: can_freeze) { create(factory, distribution: architecture.distribution) } + let_it_be(:architecture_same_name, freeze: can_freeze) { create(factory, name: architecture.name) } + + subject { architecture } + + describe 'relationships' do + it { is_expected.to belong_to(:distribution).class_name("Packages::Debian::#{container.capitalize}Distribution").inverse_of(:architectures) } + end + + describe 'validations' do + describe "#distribution" do + it { is_expected.to validate_presence_of(:distribution) } + end + + describe '#name' do + it { is_expected.to validate_presence_of(:name) } + + it { is_expected.to allow_value('amd64').for(:name) } + it { is_expected.to allow_value('kfreebsd-i386').for(:name) } + it { is_expected.not_to allow_value('-a').for(:name) } + it { is_expected.not_to allow_value('AMD64').for(:name) } + end + end + + describe 'scopes' do + describe '.with_distribution' do + subject { described_class.with_distribution(architecture.distribution) } + + it 'does not return other distributions' do + expect(subject.to_a).to eq([architecture, architecture_same_distribution]) + end + end + + describe '.with_name' do + subject { described_class.with_name(architecture.name) } + + it 'does not return other distributions' do + expect(subject.to_a).to eq([architecture, architecture_same_name]) + end + end + end +end diff --git a/spec/support/shared_examples/models/packages/debian/distribution_shared_examples.rb b/spec/support/shared_examples/models/packages/debian/distribution_shared_examples.rb new file mode 100644 index 00000000000..af87d30099f --- /dev/null +++ b/spec/support/shared_examples/models/packages/debian/distribution_shared_examples.rb @@ -0,0 +1,225 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.shared_examples 'Debian Distribution' do |factory, container, can_freeze| + let_it_be(:distribution_with_suite, freeze: can_freeze) { create(factory, suite: 'mysuite') } + let_it_be(:distribution_with_same_container, freeze: can_freeze) { create(factory, container: distribution_with_suite.container ) } + let_it_be(:distribution_with_same_codename, freeze: can_freeze) { create(factory, codename: distribution_with_suite.codename ) } + let_it_be(:distribution_with_same_suite, freeze: can_freeze) { create(factory, suite: distribution_with_suite.suite ) } + let_it_be(:distribution_with_codename_and_suite_flipped, freeze: can_freeze) { create(factory, codename: distribution_with_suite.suite, suite: distribution_with_suite.codename) } + + let_it_be_with_refind(:distribution) { create(factory, container: distribution_with_suite.container ) } + + subject { distribution } + + describe 'relationships' do + it { is_expected.to belong_to(container) } + it { is_expected.to belong_to(:creator).class_name('User') } + + it { is_expected.to have_many(:architectures).class_name("Packages::Debian::#{container.capitalize}Architecture").inverse_of(:distribution) } + end + + describe 'validations' do + describe "##{container}" do + it { is_expected.to validate_presence_of(container) } + end + + describe "#creator" do + it { is_expected.not_to validate_presence_of(:creator) } + end + + describe '#codename' do + it { is_expected.to validate_presence_of(:codename) } + + it { is_expected.to allow_value('buster').for(:codename) } + it { is_expected.to allow_value('buster-updates').for(:codename) } + it { is_expected.to allow_value('Debian10.5').for(:codename) } + it { is_expected.not_to allow_value('jessie/updates').for(:codename) } + it { is_expected.not_to allow_value('hé').for(:codename) } + end + + describe '#suite' do + it { is_expected.to allow_value(nil).for(:suite) } + it { is_expected.to allow_value('testing').for(:suite) } + it { is_expected.not_to allow_value('hé').for(:suite) } + end + + describe '#unique_debian_suite_and_codename' do + using RSpec::Parameterized::TableSyntax + + where(:with_existing_suite, :suite, :codename, :errors) do + false | nil | :keep | nil + false | 'testing' | :keep | nil + false | nil | :codename | ["Codename has already been taken"] + false | :codename | :keep | ["Suite has already been taken as Codename"] + false | :codename | :codename | ["Codename has already been taken", "Suite has already been taken as Codename"] + true | nil | :keep | nil + true | 'testing' | :keep | nil + true | nil | :codename | ["Codename has already been taken"] + true | :codename | :keep | ["Suite has already been taken as Codename"] + true | :codename | :codename | ["Codename has already been taken", "Suite has already been taken as Codename"] + true | nil | :suite | ["Codename has already been taken as Suite"] + true | :suite | :keep | ["Suite has already been taken"] + true | :suite | :suite | ["Suite has already been taken", "Codename has already been taken as Suite"] + end + + with_them do + context factory do + let(:new_distribution) { build(factory, container: distribution.container) } + + before do + distribution.update_column(:suite, 'suite-' + distribution.codename) if with_existing_suite + + if suite.is_a?(Symbol) + new_distribution.suite = distribution.send suite unless suite == :keep + else + new_distribution.suite = suite + end + + if codename.is_a?(Symbol) + new_distribution.codename = distribution.send codename unless codename == :keep + else + new_distribution.codename = codename + end + end + + it do + if errors + expect(new_distribution).not_to be_valid + expect(new_distribution.errors.to_a).to eq(errors) + else + expect(new_distribution).to be_valid + end + end + end + end + end + + describe '#origin' do + it { is_expected.to allow_value(nil).for(:origin) } + it { is_expected.to allow_value('Debian').for(:origin) } + it { is_expected.not_to allow_value('hé').for(:origin) } + end + + describe '#label' do + it { is_expected.to allow_value(nil).for(:label) } + it { is_expected.to allow_value('Debian').for(:label) } + it { is_expected.not_to allow_value('hé').for(:label) } + end + + describe '#version' do + it { is_expected.to allow_value(nil).for(:version) } + it { is_expected.to allow_value('10.6').for(:version) } + it { is_expected.not_to allow_value('hé').for(:version) } + end + + describe '#description' do + it { is_expected.to allow_value(nil).for(:description) } + it { is_expected.to allow_value('Debian 10.6 Released 26 September 2020').for(:description) } + it { is_expected.to allow_value('Hé !').for(:description) } + end + + describe '#valid_time_duration_seconds' do + it { is_expected.to allow_value(nil).for(:valid_time_duration_seconds) } + it { is_expected.to allow_value(24.hours.to_i).for(:valid_time_duration_seconds) } + it { is_expected.not_to allow_value(12.hours.to_i).for(:valid_time_duration_seconds) } + end + + describe '#signing_keys' do + it { is_expected.to validate_absence_of(:signing_keys) } + end + + describe '#file' do + it { is_expected.not_to validate_presence_of(:file) } + end + + describe '#file_store' do + it { is_expected.to validate_presence_of(:file_store) } + end + + describe '#file_signature' do + it { is_expected.to validate_absence_of(:file_signature) } + end + end + + describe 'scopes' do + describe '.with_container' do + subject { described_class.with_container(distribution_with_suite.container) } + + it 'does not return other distributions' do + expect(subject).to match_array([distribution_with_suite, distribution, distribution_with_same_container]) + end + end + + describe '.with_codename' do + subject { described_class.with_codename(distribution_with_suite.codename) } + + it 'does not return other distributions' do + expect(subject).to match_array([distribution_with_suite, distribution_with_same_codename]) + end + end + + describe '.with_suite' do + subject { described_class.with_suite(distribution_with_suite.suite) } + + it 'does not return other distributions' do + expect(subject).to match_array([distribution_with_suite, distribution_with_same_suite]) + end + end + + describe '.with_codename_or_suite' do + describe 'passing codename' do + subject { described_class.with_codename_or_suite(distribution_with_suite.codename) } + + it 'does not return other distributions' do + expect(subject.to_a).to eq([distribution_with_suite, distribution_with_same_codename, distribution_with_codename_and_suite_flipped]) + end + end + + describe 'passing suite' do + subject { described_class.with_codename_or_suite(distribution_with_suite.suite) } + + it 'does not return other distributions' do + expect(subject.to_a).to eq([distribution_with_suite, distribution_with_same_suite, distribution_with_codename_and_suite_flipped]) + end + end + end + end + + describe '#needs_update?' do + subject { distribution.needs_update? } + + context 'with new distribution' do + let(:distribution) { create(factory, container: distribution_with_suite.container) } + + it { is_expected.to be_truthy } + end + + context 'with file' do + context 'without valid_time_duration_seconds' do + let(:distribution) { create(factory, :with_file, container: distribution_with_suite.container) } + + it { is_expected.to be_falsey } + end + + context 'with valid_time_duration_seconds' do + let(:distribution) { create(factory, :with_file, container: distribution_with_suite.container, valid_time_duration_seconds: 2.days.to_i) } + + context 'when not yet expired' do + it { is_expected.to be_falsey } + end + + context 'when expired' do + it do + distribution + + travel_to(4.days.from_now) do + is_expected.to be_truthy + end + end + end + end + end + end +end diff --git a/spec/support/shared_examples/quick_actions/merge_request/rebase_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/merge_request/rebase_quick_action_shared_examples.rb new file mode 100644 index 00000000000..28decb4011d --- /dev/null +++ b/spec/support/shared_examples/quick_actions/merge_request/rebase_quick_action_shared_examples.rb @@ -0,0 +1,92 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'rebase quick action' do + context 'when updating the description' do + before do + sign_in(user) + visit edit_project_merge_request_path(project, merge_request) + end + + it 'rebases the MR', :sidekiq_inline do + fill_in('Description', with: '/rebase') + click_button('Save changes') + + expect(page).not_to have_content('commit behind the target branch') + expect(merge_request.reload).not_to be_merged + end + + it 'ignores /merge if /rebase is specified', :sidekiq_inline do + fill_in('Description', with: "/merge\n/rebase") + click_button('Save changes') + + expect(page).not_to have_content('commit behind the target branch') + expect(merge_request.reload).not_to be_merged + end + end + + context 'when creating a new note' do + context 'when the current user can rebase the MR' do + before do + sign_in(user) + visit project_merge_request_path(project, merge_request) + end + + it 'rebase the MR', :sidekiq_inline do + add_note("/rebase") + + expect(page).to have_content "Scheduled a rebase of branch #{merge_request.source_branch}." + end + + context 'when the merge request is closed' do + before do + merge_request.close! + end + + it 'does not rebase the MR', :sidekiq_inline do + add_note("/rebase") + + expect(page).not_to have_content 'Scheduled a rebase' + end + end + + context 'when a rebase is in progress', :sidekiq_inline, :clean_gitlab_redis_shared_state do + before do + jid = SecureRandom.hex + merge_request.update!(rebase_jid: jid) + Gitlab::SidekiqStatus.set(jid) + end + + it 'tells the user a rebase is in progress' do + add_note('/rebase') + + expect(page).to have_content 'A rebase is already in progress.' + expect(page).not_to have_content 'Scheduled a rebase' + end + end + + context 'when there are conflicts in the merge request' do + let!(:merge_request) { create(:merge_request, source_project: project, target_project: project, source_branch: 'conflict-missing-side', target_branch: 'conflict-start', merge_status: :cannot_be_merged) } + + it 'does not rebase the MR' do + add_note("/rebase") + + expect(page).to have_content 'This merge request cannot be rebased while there are conflicts.' + end + end + end + + context 'when the current user cannot rebase the MR' do + before do + project.add_guest(guest) + sign_in(guest) + visit project_merge_request_path(project, merge_request) + end + + it 'does not rebase the MR' do + add_note("/rebase") + + expect(page).not_to have_content 'Scheduled a rebase' + end + end + end +end diff --git a/spec/support/shared_examples/requests/api/boards_shared_examples.rb b/spec/support/shared_examples/requests/api/boards_shared_examples.rb index 0096aab55e3..8e8edd61ef9 100644 --- a/spec/support/shared_examples/requests/api/boards_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/boards_shared_examples.rb @@ -44,16 +44,35 @@ RSpec.shared_examples 'group and project boards' do |route_definition, ee = fals expect_schema_match_for(response, 'public_api/v4/boards', ee) end + end + end - describe "GET #{route_definition}/:board_id" do - let(:url) { "#{root_url}/#{board.id}" } + describe "GET #{route_definition}/:board_id" do + let(:url) { "#{root_url}/#{board.id}" } - it 'get a single board by id' do - get api(url, user) + it 'get a single board by id' do + get api(url, user) - expect_schema_match_for(response, 'public_api/v4/board', ee) - end - end + expect_schema_match_for(response, 'public_api/v4/board', ee) + end + end + + describe "PUT #{route_definition}/:board_id" do + let(:url) { "#{root_url}/#{board.id}" } + + it 'updates the board name' do + put api(url, user), params: { name: 'changed board name' } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['name']).to eq('changed board name') + end + + it 'updates the issue board booleans' do + put api(url, user), params: { hide_backlog_list: true, hide_closed_list: true } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['hide_backlog_list']).to eq(true) + expect(json_response['hide_closed_list']).to eq(true) 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 f55043fe64f..83ba72c12aa 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 @@ -37,9 +37,9 @@ RSpec.shared_context 'Debian repository shared context' do |object_type| let(:params) { workhorse_params } let(:auth_headers) { {} } + let(:workhorse_token) { JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256') } let(:workhorse_headers) do if method == :put - workhorse_token = JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256') { 'GitLab-Workhorse' => '1.0', Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER => workhorse_token } else {} @@ -117,12 +117,13 @@ RSpec.shared_examples 'Debian project repository PUT request' do |user_role, add and_body = body.nil? ? '' : ' and expected body' if status == :created - it 'creates package files' do + it 'creates package files', :aggregate_failures do pending "Debian package creation not implemented" expect { subject } .to change { project.packages.debian.count }.by(1) expect(response).to have_gitlab_http_status(status) + expect(response.media_type).to eq('text/plain') unless body.nil? expect(response.body).to eq(body) @@ -130,7 +131,59 @@ RSpec.shared_examples 'Debian project repository PUT request' do |user_role, add end it_behaves_like 'a package tracking event', described_class.name, 'push_package' else - it "returns #{status}#{and_body}" do + it "returns #{status}#{and_body}", :aggregate_failures do + subject + + expect(response).to have_gitlab_http_status(status) + + unless body.nil? + expect(response.body).to eq(body) + end + end + end + end +end + +RSpec.shared_examples 'Debian project repository PUT authorize request' do |user_role, add_member, status, body, is_authorize| + context "for user type #{user_role}" do + before do + project.send("add_#{user_role}", user) if add_member && user_role != :anonymous + end + + and_body = body.nil? ? '' : ' and expected body' + + if status == :created + it 'authorizes package file upload', :aggregate_failures do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(response.media_type).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE) + expect(json_response['TempPath']).to eq(Packages::PackageFileUploader.workhorse_local_upload_path) + expect(json_response['RemoteObject']).to be_nil + expect(json_response['MaximumSize']).to be_nil + end + + context 'without a valid token' do + let(:workhorse_token) { 'invalid' } + + it 'rejects request' do + subject + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + + context 'bypassing gitlab-workhorse' do + let(:workhorse_headers) { {} } + + it 'rejects request' do + subject + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + else + it "returns #{status}#{and_body}", :aggregate_failures do subject expect(response).to have_gitlab_http_status(status) @@ -194,7 +247,7 @@ RSpec.shared_examples 'Debian project repository GET endpoint' do |success_statu it_behaves_like 'rejects Debian access with unknown project id' end -RSpec.shared_examples 'Debian project repository PUT endpoint' do |success_status, success_body| +RSpec.shared_examples 'Debian project repository PUT endpoint' do |success_status, success_body, is_authorize = false| context 'with valid project' do using RSpec::Parameterized::TableSyntax @@ -221,7 +274,13 @@ RSpec.shared_examples 'Debian project repository PUT endpoint' do |success_statu with_them do include_context 'Debian repository project access', params[:project_visibility_level], params[:user_role], params[:user_token], :basic do - it_behaves_like 'Debian project repository PUT request', params[:user_role], params[:member], params[:expected_status], params[:expected_body] + desired_behavior = if is_authorize + 'Debian project repository PUT authorize request' + else + 'Debian project repository PUT request' + end + + it_behaves_like desired_behavior, params[:user_role], params[:member], params[:expected_status], params[:expected_body] end end end diff --git a/spec/support/shared_examples/requests/api/nuget_endpoints_shared_examples.rb b/spec/support/shared_examples/requests/api/nuget_endpoints_shared_examples.rb index f808d12baf4..7b7d2a33e8c 100644 --- a/spec/support/shared_examples/requests/api/nuget_endpoints_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/nuget_endpoints_shared_examples.rb @@ -1,31 +1,31 @@ # frozen_string_literal: true -RSpec.shared_examples 'handling nuget service requests' do +RSpec.shared_examples 'handling nuget service requests' do |anonymous_requests_example_name: 'process nuget service index request', anonymous_requests_status: :success| subject { get api(url) } - context 'with valid project' do + context 'with valid target' do using RSpec::Parameterized::TableSyntax context 'personal token' do - where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do - 'PUBLIC' | :developer | true | true | 'process nuget service index request' | :success - 'PUBLIC' | :guest | true | true | 'process nuget service index request' | :success - 'PUBLIC' | :developer | true | false | 'process nuget service index request' | :success - 'PUBLIC' | :guest | true | false | 'process nuget service index request' | :success - 'PUBLIC' | :developer | false | true | 'process nuget service index request' | :success - 'PUBLIC' | :guest | false | true | 'process nuget service index request' | :success - 'PUBLIC' | :developer | false | false | 'process nuget service index request' | :success - 'PUBLIC' | :guest | false | false | 'process nuget service index request' | :success - 'PUBLIC' | :anonymous | false | true | 'process nuget service index request' | :success - 'PRIVATE' | :developer | true | true | 'process nuget service index request' | :success - 'PRIVATE' | :guest | true | true | 'rejects nuget packages access' | :forbidden - 'PRIVATE' | :developer | true | false | 'rejects nuget packages access' | :unauthorized - 'PRIVATE' | :guest | true | false | 'rejects nuget packages access' | :unauthorized - 'PRIVATE' | :developer | false | true | 'rejects nuget packages access' | :not_found - 'PRIVATE' | :guest | false | true | 'rejects nuget packages access' | :not_found - 'PRIVATE' | :developer | false | false | 'rejects nuget packages access' | :unauthorized - 'PRIVATE' | :guest | false | false | 'rejects nuget packages access' | :unauthorized - 'PRIVATE' | :anonymous | false | true | 'rejects nuget packages access' | :unauthorized + where(:visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do + 'PUBLIC' | :developer | true | true | 'process nuget service index request' | :success + 'PUBLIC' | :guest | true | true | 'process nuget service index request' | :success + 'PUBLIC' | :developer | true | false | 'rejects nuget packages access' | :unauthorized + 'PUBLIC' | :guest | true | false | 'rejects nuget packages access' | :unauthorized + 'PUBLIC' | :developer | false | true | 'process nuget service index request' | :success + 'PUBLIC' | :guest | false | true | 'process nuget service index request' | :success + 'PUBLIC' | :developer | false | false | 'rejects nuget packages access' | :unauthorized + 'PUBLIC' | :guest | false | false | 'rejects nuget packages access' | :unauthorized + 'PUBLIC' | :anonymous | false | true | anonymous_requests_example_name | anonymous_requests_status + 'PRIVATE' | :developer | true | true | 'process nuget service index request' | :success + 'PRIVATE' | :guest | true | true | 'rejects nuget packages access' | :forbidden + 'PRIVATE' | :developer | true | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :guest | true | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :developer | false | true | 'rejects nuget packages access' | :not_found + 'PRIVATE' | :guest | false | true | 'rejects nuget packages access' | :not_found + 'PRIVATE' | :developer | false | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :guest | false | false | 'rejects nuget packages access' | :unauthorized + 'PRIVATE' | :anonymous | false | true | 'rejects nuget packages access' | :unauthorized end with_them do @@ -35,7 +35,7 @@ RSpec.shared_examples 'handling nuget service requests' do subject { get api(url), headers: headers } before do - project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility_level, false)) + update_visibility_to(Gitlab::VisibilityLevel.const_get(visibility_level, false)) end it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member] @@ -43,7 +43,7 @@ RSpec.shared_examples 'handling nuget service requests' do end context 'with job token' do - where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do + where(:visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do 'PUBLIC' | :developer | true | true | 'process nuget service index request' | :success 'PUBLIC' | :guest | true | true | 'process nuget service index request' | :success 'PUBLIC' | :developer | true | false | 'rejects nuget packages access' | :unauthorized @@ -52,7 +52,7 @@ RSpec.shared_examples 'handling nuget service requests' do 'PUBLIC' | :guest | false | true | 'process nuget service index request' | :success 'PUBLIC' | :developer | false | false | 'rejects nuget packages access' | :unauthorized 'PUBLIC' | :guest | false | false | 'rejects nuget packages access' | :unauthorized - 'PUBLIC' | :anonymous | false | true | 'process nuget service index request' | :success + 'PUBLIC' | :anonymous | false | true | anonymous_requests_example_name | anonymous_requests_status 'PRIVATE' | :developer | true | true | 'process nuget service index request' | :success 'PRIVATE' | :guest | true | true | 'rejects nuget packages access' | :forbidden 'PRIVATE' | :developer | true | false | 'rejects nuget packages access' | :unauthorized @@ -71,7 +71,7 @@ RSpec.shared_examples 'handling nuget service requests' do subject { get api(url), headers: headers } before do - project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility_level, false)) + update_visibility_to(Gitlab::VisibilityLevel.const_get(visibility_level, false)) end it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member] @@ -79,14 +79,18 @@ RSpec.shared_examples 'handling nuget service requests' do end end - it_behaves_like 'deploy token for package GET requests' + it_behaves_like 'deploy token for package GET requests' do + before do + update_visibility_to(Gitlab::VisibilityLevel::PRIVATE) + end + end - it_behaves_like 'rejects nuget access with unknown project id' + it_behaves_like 'rejects nuget access with unknown target id' - it_behaves_like 'rejects nuget access with invalid project id' + it_behaves_like 'rejects nuget access with invalid target id' end -RSpec.shared_examples 'handling nuget metadata requests with package name' do +RSpec.shared_examples 'handling nuget metadata requests with package name' do |anonymous_requests_example_name: 'process nuget metadata request at package name level', anonymous_requests_status: :success| include_context 'with expected presenters dependency groups' let_it_be(:package_name) { 'Dummy.Package' } @@ -99,19 +103,19 @@ RSpec.shared_examples 'handling nuget metadata requests with package name' do packages.each { |pkg| create_dependencies_for(pkg) } end - context 'with valid project' do + context 'with valid target' do using RSpec::Parameterized::TableSyntax - where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do + where(:visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do 'PUBLIC' | :developer | true | true | 'process nuget metadata request at package name level' | :success 'PUBLIC' | :guest | true | true | 'process nuget metadata request at package name level' | :success - 'PUBLIC' | :developer | true | false | 'process nuget metadata request at package name level' | :success - 'PUBLIC' | :guest | true | false | 'process nuget metadata request at package name level' | :success + 'PUBLIC' | :developer | true | false | 'rejects nuget packages access' | :unauthorized + 'PUBLIC' | :guest | true | false | 'rejects nuget packages access' | :unauthorized 'PUBLIC' | :developer | false | true | 'process nuget metadata request at package name level' | :success 'PUBLIC' | :guest | false | true | 'process nuget metadata request at package name level' | :success - 'PUBLIC' | :developer | false | false | 'process nuget metadata request at package name level' | :success - 'PUBLIC' | :guest | false | false | 'process nuget metadata request at package name level' | :success - 'PUBLIC' | :anonymous | false | true | 'process nuget metadata request at package name level' | :success + 'PUBLIC' | :developer | false | false | 'rejects nuget packages access' | :unauthorized + 'PUBLIC' | :guest | false | false | 'rejects nuget packages access' | :unauthorized + 'PUBLIC' | :anonymous | false | true | anonymous_requests_example_name | anonymous_requests_status 'PRIVATE' | :developer | true | true | 'process nuget metadata request at package name level' | :success 'PRIVATE' | :guest | true | true | 'rejects nuget packages access' | :forbidden 'PRIVATE' | :developer | true | false | 'rejects nuget packages access' | :unauthorized @@ -130,21 +134,25 @@ RSpec.shared_examples 'handling nuget metadata requests with package name' do subject { get api(url), headers: headers } before do - project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility_level, false)) + update_visibility_to(Gitlab::VisibilityLevel.const_get(visibility_level, false)) end it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member] end - it_behaves_like 'deploy token for package GET requests' + it_behaves_like 'deploy token for package GET requests' do + before do + update_visibility_to(Gitlab::VisibilityLevel::PRIVATE) + end + end - it_behaves_like 'rejects nuget access with unknown project id' + it_behaves_like 'rejects nuget access with unknown target id' - it_behaves_like 'rejects nuget access with invalid project id' + it_behaves_like 'rejects nuget access with invalid target id' end end -RSpec.shared_examples 'handling nuget metadata requests with package name and package version' do +RSpec.shared_examples 'handling nuget metadata requests with package name and package version' do |anonymous_requests_example_name: 'process nuget metadata request at package name and package version level', anonymous_requests_status: :success| include_context 'with expected presenters dependency groups' let_it_be(:package_name) { 'Dummy.Package' } @@ -157,19 +165,19 @@ RSpec.shared_examples 'handling nuget metadata requests with package name and pa create_dependencies_for(package) end - context 'with valid project' do + context 'with valid target' do using RSpec::Parameterized::TableSyntax - where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do + where(:visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do 'PUBLIC' | :developer | true | true | 'process nuget metadata request at package name and package version level' | :success 'PUBLIC' | :guest | true | true | 'process nuget metadata request at package name and package version level' | :success - 'PUBLIC' | :developer | true | false | 'process nuget metadata request at package name and package version level' | :success - 'PUBLIC' | :guest | true | false | 'process nuget metadata request at package name and package version level' | :success + 'PUBLIC' | :developer | true | false | 'rejects nuget packages access' | :unauthorized + 'PUBLIC' | :guest | true | false | 'rejects nuget packages access' | :unauthorized 'PUBLIC' | :developer | false | true | 'process nuget metadata request at package name and package version level' | :success 'PUBLIC' | :guest | false | true | 'process nuget metadata request at package name and package version level' | :success - 'PUBLIC' | :developer | false | false | 'process nuget metadata request at package name and package version level' | :success - 'PUBLIC' | :guest | false | false | 'process nuget metadata request at package name and package version level' | :success - 'PUBLIC' | :anonymous | false | true | 'process nuget metadata request at package name and package version level' | :success + 'PUBLIC' | :developer | false | false | 'rejects nuget packages access' | :unauthorized + 'PUBLIC' | :guest | false | false | 'rejects nuget packages access' | :unauthorized + 'PUBLIC' | :anonymous | false | true | anonymous_requests_example_name | anonymous_requests_status 'PRIVATE' | :developer | true | true | 'process nuget metadata request at package name and package version level' | :success 'PRIVATE' | :guest | true | true | 'rejects nuget packages access' | :forbidden 'PRIVATE' | :developer | true | false | 'rejects nuget packages access' | :unauthorized @@ -188,23 +196,25 @@ RSpec.shared_examples 'handling nuget metadata requests with package name and pa subject { get api(url), headers: headers } before do - project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility_level, false)) + update_visibility_to(Gitlab::VisibilityLevel.const_get(visibility_level, false)) end it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member] end end - it_behaves_like 'deploy token for package GET requests' + it_behaves_like 'deploy token for package GET requests' do + before do + update_visibility_to(Gitlab::VisibilityLevel::PRIVATE) + end + end - context 'with invalid package name' do - let_it_be(:package_name) { 'Unkown' } + it_behaves_like 'rejects nuget access with unknown target id' - it_behaves_like 'rejects nuget packages access', :developer, :not_found - end + it_behaves_like 'rejects nuget access with invalid target id' end -RSpec.shared_examples 'handling nuget search requests' do +RSpec.shared_examples 'handling nuget search requests' do |anonymous_requests_example_name: 'process nuget search request', anonymous_requests_status: :success| let_it_be(:package_a) { create(:nuget_package, :with_metadatum, name: 'Dummy.PackageA', project: project) } let_it_be(:tag) { create(:packages_tag, package: package_a, name: 'test') } let_it_be(:packages_b) { create_list(:nuget_package, 5, name: 'Dummy.PackageB', project: project) } @@ -219,19 +229,19 @@ RSpec.shared_examples 'handling nuget search requests' do subject { get api(url) } - context 'with valid project' do + context 'with valid target' do using RSpec::Parameterized::TableSyntax - where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do + where(:visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do 'PUBLIC' | :developer | true | true | 'process nuget search request' | :success 'PUBLIC' | :guest | true | true | 'process nuget search request' | :success - 'PUBLIC' | :developer | true | false | 'process nuget search request' | :success - 'PUBLIC' | :guest | true | false | 'process nuget search request' | :success + 'PUBLIC' | :developer | true | false | 'rejects nuget packages access' | :unauthorized + 'PUBLIC' | :guest | true | false | 'rejects nuget packages access' | :unauthorized 'PUBLIC' | :developer | false | true | 'process nuget search request' | :success 'PUBLIC' | :guest | false | true | 'process nuget search request' | :success - 'PUBLIC' | :developer | false | false | 'process nuget search request' | :success - 'PUBLIC' | :guest | false | false | 'process nuget search request' | :success - 'PUBLIC' | :anonymous | false | true | 'process nuget search request' | :success + 'PUBLIC' | :developer | false | false | 'rejects nuget packages access' | :unauthorized + 'PUBLIC' | :guest | false | false | 'rejects nuget packages access' | :unauthorized + 'PUBLIC' | :anonymous | false | true | anonymous_requests_example_name | anonymous_requests_status 'PRIVATE' | :developer | true | true | 'process nuget search request' | :success 'PRIVATE' | :guest | true | true | 'rejects nuget packages access' | :forbidden 'PRIVATE' | :developer | true | false | 'rejects nuget packages access' | :unauthorized @@ -250,16 +260,20 @@ RSpec.shared_examples 'handling nuget search requests' do subject { get api(url), headers: headers } before do - project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility_level, false)) + update_visibility_to(Gitlab::VisibilityLevel.const_get(visibility_level, false)) end it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member] end end - it_behaves_like 'deploy token for package GET requests' + it_behaves_like 'deploy token for package GET requests' do + before do + update_visibility_to(Gitlab::VisibilityLevel::PRIVATE) + end + end - it_behaves_like 'rejects nuget access with unknown project id' + it_behaves_like 'rejects nuget access with unknown target id' - it_behaves_like 'rejects nuget access with invalid project id' + it_behaves_like 'rejects nuget access with invalid target id' end diff --git a/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb index dc6ac5f0371..8b60857cdaf 100644 --- a/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb @@ -3,7 +3,7 @@ RSpec.shared_examples 'rejects nuget packages access' do |user_type, status, add_member = true| context "for user type #{user_type}" do before do - project.send("add_#{user_type}", user) if add_member && user_type != :anonymous + target.send("add_#{user_type}", user) if add_member && user_type != :anonymous end it_behaves_like 'returning response status', status @@ -21,7 +21,7 @@ end RSpec.shared_examples 'process nuget service index request' do |user_type, status, add_member = true| context "for user type #{user_type}" do before do - project.send("add_#{user_type}", user) if add_member && user_type != :anonymous + target.send("add_#{user_type}", user) if add_member && user_type != :anonymous end it_behaves_like 'returning response status', status @@ -37,7 +37,7 @@ RSpec.shared_examples 'process nuget service index request' do |user_type, statu end context 'with invalid format' do - let(:url) { "/projects/#{project.id}/packages/nuget/index.xls" } + let(:url) { "/#{target_type}/#{target.id}/packages/nuget/index.xls" } it_behaves_like 'rejects nuget packages access', :anonymous, :not_found end @@ -57,7 +57,7 @@ end RSpec.shared_examples 'process nuget metadata request at package name level' do |user_type, status, add_member = true| context "for user type #{user_type}" do before do - project.send("add_#{user_type}", user) if add_member && user_type != :anonymous + target.send("add_#{user_type}", user) if add_member && user_type != :anonymous end it_behaves_like 'returning response status', status @@ -65,7 +65,7 @@ RSpec.shared_examples 'process nuget metadata request at package name level' do it_behaves_like 'returning nuget metadata json response with json schema', 'public_api/v4/packages/nuget/packages_metadata' context 'with invalid format' do - let(:url) { "/projects/#{project.id}/packages/nuget/metadata/#{package_name}/index.xls" } + let(:url) { "/#{target_type}/#{target.id}/packages/nuget/metadata/#{package_name}/index.xls" } it_behaves_like 'rejects nuget packages access', :anonymous, :not_found end @@ -83,7 +83,7 @@ end RSpec.shared_examples 'process nuget metadata request at package name and package version level' do |user_type, status, add_member = true| context "for user type #{user_type}" do before do - project.send("add_#{user_type}", user) if add_member && user_type != :anonymous + target.send("add_#{user_type}", user) if add_member && user_type != :anonymous end it_behaves_like 'returning response status', status @@ -91,7 +91,7 @@ RSpec.shared_examples 'process nuget metadata request at package name and packag it_behaves_like 'returning nuget metadata json response with json schema', 'public_api/v4/packages/nuget/package_metadata' context 'with invalid format' do - let(:url) { "/projects/#{project.id}/packages/nuget/metadata/#{package_name}/#{package.version}.xls" } + let(:url) { "/#{target_type}/#{target.id}/packages/nuget/metadata/#{package_name}/#{package.version}.xls" } it_behaves_like 'rejects nuget packages access', :anonymous, :not_found end @@ -109,7 +109,7 @@ end RSpec.shared_examples 'process nuget workhorse authorization' do |user_type, status, add_member = true| context "for user type #{user_type}" do before do - project.send("add_#{user_type}", user) if add_member && user_type != :anonymous + target.send("add_#{user_type}", user) if add_member && user_type != :anonymous end it_behaves_like 'returning response status', status @@ -128,7 +128,7 @@ RSpec.shared_examples 'process nuget workhorse authorization' do |user_type, sta end before do - project.add_maintainer(user) + target.add_maintainer(user) end it_behaves_like 'returning response status', :forbidden @@ -141,18 +141,18 @@ RSpec.shared_examples 'process nuget upload' do |user_type, status, add_member = it 'creates package files' do expect(::Packages::Nuget::ExtractionWorker).to receive(:perform_async).once expect { subject } - .to change { project.packages.count }.by(1) + .to change { target.packages.count }.by(1) .and change { Packages::PackageFile.count }.by(1) expect(response).to have_gitlab_http_status(status) - package_file = project.packages.last.package_files.reload.last + package_file = target.packages.last.package_files.reload.last expect(package_file.file_name).to eq('package.nupkg') end end context "for user type #{user_type}" do before do - project.send("add_#{user_type}", user) if add_member && user_type != :anonymous + target.send("add_#{user_type}", user) if add_member && user_type != :anonymous end context 'with object storage disabled' do @@ -206,7 +206,7 @@ RSpec.shared_examples 'process nuget upload' do |user_type, status, add_member = context 'with crafted package.path param' do let(:crafted_file) { Tempfile.new('nuget.crafted.package.path') } - let(:url) { "/projects/#{project.id}/packages/nuget?package.path=#{crafted_file.path}" } + let(:url) { "/#{target_type}/#{target.id}/packages/nuget?package.path=#{crafted_file.path}" } let(:params) { { file: temp_file(file_name) } } let(:file_key) { :file } @@ -255,7 +255,7 @@ RSpec.shared_examples 'process nuget download versions request' do |user_type, s context "for user type #{user_type}" do before do - project.send("add_#{user_type}", user) if add_member && user_type != :anonymous + target.send("add_#{user_type}", user) if add_member && user_type != :anonymous end it_behaves_like 'returning response status', status @@ -263,7 +263,7 @@ RSpec.shared_examples 'process nuget download versions request' do |user_type, s it_behaves_like 'returns a valid nuget download versions json response' context 'with invalid format' do - let(:url) { "/projects/#{project.id}/packages/nuget/download/#{package_name}/index.xls" } + let(:url) { "/#{target_type}/#{target.id}/packages/nuget/download/#{package_name}/index.xls" } it_behaves_like 'rejects nuget packages access', :anonymous, :not_found end @@ -281,7 +281,7 @@ end RSpec.shared_examples 'process nuget download content request' do |user_type, status, add_member = true| context "for user type #{user_type}" do before do - project.send("add_#{user_type}", user) if add_member && user_type != :anonymous + target.send("add_#{user_type}", user) if add_member && user_type != :anonymous end it_behaves_like 'returning response status', status @@ -295,7 +295,7 @@ RSpec.shared_examples 'process nuget download content request' do |user_type, st end context 'with invalid format' do - let(:url) { "/projects/#{project.id}/packages/nuget/download/#{package.name}/#{package.version}/#{package.name}.#{package.version}.xls" } + let(:url) { "/#{target_type}/#{target.id}/packages/nuget/download/#{package.name}/#{package.version}/#{package.name}.#{package.version}.xls" } it_behaves_like 'rejects nuget packages access', :anonymous, :not_found end @@ -331,7 +331,7 @@ RSpec.shared_examples 'process nuget search request' do |user_type, status, add_ context "for user type #{user_type}" do before do - project.send("add_#{user_type}", user) if add_member && user_type != :anonymous + target.send("add_#{user_type}", user) if add_member && user_type != :anonymous end it_behaves_like 'returns a valid json search response', status, 4, [1, 5, 5, 1] @@ -370,20 +370,20 @@ RSpec.shared_examples 'process nuget search request' do |user_type, status, add_ end end -RSpec.shared_examples 'rejects nuget access with invalid project id' do - context 'with a project id with invalid integers' do +RSpec.shared_examples 'rejects nuget access with invalid target id' do + context 'with a target id with invalid integers' do using RSpec::Parameterized::TableSyntax - let(:project) { OpenStruct.new(id: id) } + let(:target) { OpenStruct.new(id: id) } where(:id, :status) do - '/../' | :unauthorized + '/../' | :bad_request '' | :not_found - '%20' | :unauthorized - '%2e%2e%2f' | :unauthorized - 'NaN' | :unauthorized + '%20' | :bad_request + '%2e%2e%2f' | :bad_request + 'NaN' | :bad_request 00002345 | :unauthorized - 'anything25' | :unauthorized + 'anything25' | :bad_request end with_them do @@ -392,9 +392,9 @@ RSpec.shared_examples 'rejects nuget access with invalid project id' do end end -RSpec.shared_examples 'rejects nuget access with unknown project id' do - context 'with an unknown project' do - let(:project) { OpenStruct.new(id: 1234567890) } +RSpec.shared_examples 'rejects nuget access with unknown target id' do + context 'with an unknown target' do + let(:target) { OpenStruct.new(id: 1234567890) } context 'as anonymous' do it_behaves_like 'rejects nuget packages access', :anonymous, :unauthorized diff --git a/spec/support/shared_examples/requests/api/repository_storage_moves_shared_examples.rb b/spec/support/shared_examples/requests/api/repository_storage_moves_shared_examples.rb new file mode 100644 index 00000000000..b2970fd265d --- /dev/null +++ b/spec/support/shared_examples/requests/api/repository_storage_moves_shared_examples.rb @@ -0,0 +1,219 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'repository_storage_moves API' do |container_type| + include AccessMatchersForRequest + + let_it_be(:user) { create(:admin) } + + shared_examples 'get single container repository storage move' do + let(:repository_storage_move_id) { storage_move.id } + + def get_container_repository_storage_move + get api(url, user) + end + + it 'returns a container repository storage move', :aggregate_failures do + get_container_repository_storage_move + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema("public_api/v4/#{container_type.singularize}_repository_storage_move") + expect(json_response['id']).to eq(storage_move.id) + expect(json_response['state']).to eq(storage_move.human_state_name) + end + + context 'non-existent container repository storage move' do + let(:repository_storage_move_id) { non_existing_record_id } + + it 'returns not found' do + get_container_repository_storage_move + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + describe 'permissions' do + it { expect { get_container_repository_storage_move }.to be_allowed_for(:admin) } + it { expect { get_container_repository_storage_move }.to be_denied_for(:user) } + end + end + + shared_examples 'get container repository storage move list' do + def get_container_repository_storage_moves + get api(url, user) + end + + it 'returns container repository storage moves', :aggregate_failures do + get_container_repository_storage_moves + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to include_pagination_headers + expect(response).to match_response_schema("public_api/v4/#{container_type.singularize}_repository_storage_moves") + expect(json_response.size).to eq(1) + expect(json_response.first['id']).to eq(storage_move.id) + expect(json_response.first['state']).to eq(storage_move.human_state_name) + end + + it 'avoids N+1 queries', :request_store do + # prevent `let` from polluting the control + get_container_repository_storage_moves + + control = ActiveRecord::QueryRecorder.new { get_container_repository_storage_moves } + + create(repository_storage_move_factory, :scheduled, container: container) + + expect { get_container_repository_storage_moves }.not_to exceed_query_limit(control) + end + + it 'returns the most recently created first' do + storage_move_oldest = create(repository_storage_move_factory, :scheduled, container: container, created_at: 2.days.ago) + storage_move_middle = create(repository_storage_move_factory, :scheduled, container: container, created_at: 1.day.ago) + + get_container_repository_storage_moves + + json_ids = json_response.map {|storage_move| storage_move['id'] } + expect(json_ids).to eq([ + storage_move.id, + storage_move_middle.id, + storage_move_oldest.id + ]) + end + + describe 'permissions' do + it { expect { get_container_repository_storage_moves }.to be_allowed_for(:admin) } + it { expect { get_container_repository_storage_moves }.to be_denied_for(:user) } + end + end + + describe "GET /#{container_type}/:id/repository_storage_moves" do + it_behaves_like 'get container repository storage move list' do + let(:url) { "/#{container_type}/#{container.id}/repository_storage_moves" } + end + end + + describe "GET /#{container_type}/:id/repository_storage_moves/:repository_storage_move_id" do + it_behaves_like 'get single container repository storage move' do + let(:url) { "/#{container_type}/#{container.id}/repository_storage_moves/#{repository_storage_move_id}" } + end + end + + describe "GET /#{container_type.singularize}_repository_storage_moves" do + it_behaves_like 'get container repository storage move list' do + let(:url) { "/#{container_type.singularize}_repository_storage_moves" } + end + end + + describe "GET /#{container_type.singularize}_repository_storage_moves/:repository_storage_move_id" do + it_behaves_like 'get single container repository storage move' do + let(:url) { "/#{container_type.singularize}_repository_storage_moves/#{repository_storage_move_id}" } + end + end + + describe "POST /#{container_type}/:id/repository_storage_moves" do + let(:url) { "/#{container_type}/#{container.id}/repository_storage_moves" } + let(:destination_storage_name) { 'test_second_storage' } + + def create_container_repository_storage_move + post api(url, user), params: { destination_storage_name: destination_storage_name } + end + + before do + stub_storage_settings('test_second_storage' => { 'path' => 'tmp/tests/extra_storage' }) + end + + it 'schedules a container repository storage move', :aggregate_failures do + create_container_repository_storage_move + + storage_move = container.repository_storage_moves.last + + expect(response).to have_gitlab_http_status(:created) + expect(response).to match_response_schema("public_api/v4/#{container_type.singularize}_repository_storage_move") + expect(json_response['id']).to eq(storage_move.id) + expect(json_response['state']).to eq('scheduled') + expect(json_response['source_storage_name']).to eq('default') + expect(json_response['destination_storage_name']).to eq(destination_storage_name) + end + + describe 'permissions' do + it { expect { create_container_repository_storage_move }.to be_allowed_for(:admin) } + it { expect { create_container_repository_storage_move }.to be_denied_for(:user) } + end + + context 'destination_storage_name is missing', :aggregate_failures do + let(:destination_storage_name) { nil } + + it 'schedules a container repository storage move' do + create_container_repository_storage_move + + storage_move = container.repository_storage_moves.last + + expect(response).to have_gitlab_http_status(:created) + expect(response).to match_response_schema("public_api/v4/#{container_type.singularize}_repository_storage_move") + expect(json_response['id']).to eq(storage_move.id) + expect(json_response['state']).to eq('scheduled') + expect(json_response['source_storage_name']).to eq('default') + expect(json_response['destination_storage_name']).to be_present + end + end + end + + describe "POST /#{container_type.singularize}_repository_storage_moves" do + let(:url) { "/#{container_type.singularize}_repository_storage_moves" } + let(:source_storage_name) { 'default' } + let(:destination_storage_name) { 'test_second_storage' } + + def create_container_repository_storage_moves + post api(url, user), params: { + source_storage_name: source_storage_name, + destination_storage_name: destination_storage_name + } + end + + before do + stub_storage_settings('test_second_storage' => { 'path' => 'tmp/tests/extra_storage' }) + end + + it 'schedules the worker' do + expect(bulk_worker_klass).to receive(:perform_async).with(source_storage_name, destination_storage_name) + + create_container_repository_storage_moves + + expect(response).to have_gitlab_http_status(:accepted) + end + + context 'source_storage_name is invalid' do + let(:destination_storage_name) { 'not-a-real-storage' } + + it 'gives an error' do + create_container_repository_storage_moves + + expect(response).to have_gitlab_http_status(:bad_request) + end + end + + context 'destination_storage_name is missing' do + let(:destination_storage_name) { nil } + + it 'schedules the worker' do + expect(bulk_worker_klass).to receive(:perform_async).with(source_storage_name, destination_storage_name) + + create_container_repository_storage_moves + + expect(response).to have_gitlab_http_status(:accepted) + end + end + + context 'destination_storage_name is invalid' do + let(:destination_storage_name) { 'not-a-real-storage' } + + it 'gives an error' do + create_container_repository_storage_moves + + expect(response).to have_gitlab_http_status(:bad_request) + end + end + + describe 'normal user' do + it { expect { create_container_repository_storage_moves }.to be_denied_for(:user) } + end + end +end diff --git a/spec/support/shared_examples/requests/api/resolvable_discussions_shared_examples.rb b/spec/support/shared_examples/requests/api/resolvable_discussions_shared_examples.rb index 5748e873fd4..460e8d57a2b 100644 --- a/spec/support/shared_examples/requests/api/resolvable_discussions_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/resolvable_discussions_shared_examples.rb @@ -9,6 +9,7 @@ RSpec.shared_examples 'resolvable discussions API' do |parent_type, noteable_typ expect(response).to have_gitlab_http_status(:ok) expect(json_response['notes'].size).to eq(1) expect(json_response['notes'][0]['resolved']).to eq(true) + expect(Time.parse(json_response['notes'][0]['resolved_at'])).to be_like_time(note.reload.resolved_at) end it "unresolves discussion if resolved is false" do @@ -18,6 +19,7 @@ RSpec.shared_examples 'resolvable discussions API' do |parent_type, noteable_typ expect(response).to have_gitlab_http_status(:ok) expect(json_response['notes'].size).to eq(1) expect(json_response['notes'][0]['resolved']).to eq(false) + expect(json_response['notes'][0]['resolved_at']).to be_nil end it "returns a 400 bad request error if resolved parameter is not passed" do diff --git a/spec/support/shared_examples/requests/rack_attack_shared_examples.rb b/spec/support/shared_examples/requests/rack_attack_shared_examples.rb index 5d300d38e4a..3b039049ca9 100644 --- a/spec/support/shared_examples/requests/rack_attack_shared_examples.rb +++ b/spec/support/shared_examples/requests/rack_attack_shared_examples.rb @@ -154,10 +154,11 @@ RSpec.shared_examples 'rate-limited token-authenticated requests' do end def make_request(args) + path, options = args if request_method == 'POST' - post(*args) + post(path, **options) else - get(*args) + get(path, **options) end end end diff --git a/spec/support/shared_examples/requests/releases_shared_examples.rb b/spec/support/shared_examples/requests/releases_shared_examples.rb new file mode 100644 index 00000000000..b835947e497 --- /dev/null +++ b/spec/support/shared_examples/requests/releases_shared_examples.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'correct release milestone order' do + let_it_be_with_reload(:milestone_1) { create(:milestone, project: project) } + let_it_be_with_reload(:milestone_2) { create(:milestone, project: project) } + + shared_examples 'correct sort order' do + it 'sorts milestonee_1 before milestone_2' do + freeze_time do + expect(actual_milestone_title_order).to eq([milestone_1.title, milestone_2.title]) + end + end + end + + context 'due_date' do + before do + milestone_1.update!(due_date: Time.zone.now, start_date: 1.day.ago, title: 'z') + milestone_2.update!(due_date: 1.day.from_now, start_date: 2.days.ago, title: 'a') + end + + context 'when both milestones have a due_date' do + it_behaves_like 'correct sort order' + end + + context 'when one milestone does not have a due_date' do + before do + milestone_2.update!(due_date: nil) + end + + it_behaves_like 'correct sort order' + end + end + + context 'start_date' do + before do + milestone_1.update!(due_date: 1.day.from_now, start_date: 1.day.ago, title: 'z' ) + milestone_2.update!(due_date: 1.day.from_now, start_date: milestone_2_start_date, title: 'a' ) + end + + context 'when both milestones have a start_date' do + let(:milestone_2_start_date) { Time.zone.now } + + it_behaves_like 'correct sort order' + end + + context 'when one milestone does not have a start_date' do + let(:milestone_2_start_date) { nil } + + it_behaves_like 'correct sort order' + end + end + + context 'title' do + before do + milestone_1.update!(due_date: 1.day.from_now, start_date: Time.zone.now, title: 'a' ) + milestone_2.update!(due_date: 1.day.from_now, start_date: Time.zone.now, title: 'z' ) + end + + it_behaves_like 'correct sort order' + end +end diff --git a/spec/support/shared_examples/requests/sessionless_auth_request_shared_examples.rb b/spec/support/shared_examples/requests/sessionless_auth_request_shared_examples.rb new file mode 100644 index 00000000000..d82da1b01e1 --- /dev/null +++ b/spec/support/shared_examples/requests/sessionless_auth_request_shared_examples.rb @@ -0,0 +1,84 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'authenticates sessionless user for the request spec' do |params| + params ||= {} + + before do + stub_authentication_activity_metrics(debug: false) + end + + let(:user) { create(:user) } + let(:personal_access_token) { create(:personal_access_token, user: user) } + let(:default_params) { params.except(:public) || {} } + + context "when the 'personal_access_token' param is populated with the personal access token" do + it 'logs the user in' do + expect(authentication_metrics) + .to increment(:user_authenticated_counter) + .and increment(:user_session_override_counter) + .and increment(:user_sessionless_authentication_counter) + + get url, params: default_params.merge(private_token: personal_access_token.token) + + expect(response).to have_gitlab_http_status(:ok) + expect(controller.current_user).to eq(user) + end + + it 'does not log the user in if page is public', if: params[:public] do + get url, params: default_params + + expect(response).to have_gitlab_http_status(:ok) + expect(controller.current_user).to be_nil + end + end + + context 'when the personal access token has no api scope', unless: params[:public] do + it 'does not log the user in' do + # Several instances of where these specs are shared route the request + # through ApplicationController#route_not_found which does not involve + # the usual auth code from Devise, so does not increment the + # :user_unauthenticated_counter + # + unless params[:ignore_incrementing] + expect(authentication_metrics) + .to increment(:user_unauthenticated_counter) + end + + personal_access_token.update!(scopes: [:read_user]) + + get url, params: default_params.merge(private_token: personal_access_token.token) + + expect(response).not_to have_gitlab_http_status(:ok) + end + end + + context "when the 'PERSONAL_ACCESS_TOKEN' header is populated with the personal access token" do + it 'logs the user in' do + expect(authentication_metrics) + .to increment(:user_authenticated_counter) + .and increment(:user_session_override_counter) + .and increment(:user_sessionless_authentication_counter) + + headers = { 'PRIVATE-TOKEN': personal_access_token.token } + get url, params: default_params, headers: headers + + expect(response).to have_gitlab_http_status(:ok) + end + end + + it "doesn't log the user in otherwise", unless: params[:public] do + # Several instances of where these specs are shared route the request + # through ApplicationController#route_not_found which does not involve + # the usual auth code from Devise, so does not increment the + # :user_unauthenticated_counter + # + unless params[:ignore_incrementing] + expect(authentication_metrics) + .to increment(:user_unauthenticated_counter) + end + + get url, params: default_params.merge(private_token: 'token') + + expect(response).not_to have_gitlab_http_status(:ok) + end +end diff --git a/spec/support/shared_examples/routing/legacy_path_redirect_shared_examples.rb b/spec/support/shared_examples/routing/legacy_path_redirect_shared_examples.rb index 808336db7b1..ae3f6425b5e 100644 --- a/spec/support/shared_examples/routing/legacy_path_redirect_shared_examples.rb +++ b/spec/support/shared_examples/routing/legacy_path_redirect_shared_examples.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -RSpec.shared_examples 'redirecting a legacy project path' do |source, target| +RSpec.shared_examples 'redirecting a legacy path' do |source, target| include RSpec::Rails::RequestExampleGroup it "redirects #{source} to #{target}" do diff --git a/spec/support/shared_examples/serializers/pipeline_artifacts_shared_example.rb b/spec/support/shared_examples/serializers/pipeline_artifacts_shared_example.rb new file mode 100644 index 00000000000..d5ffd5e7510 --- /dev/null +++ b/spec/support/shared_examples/serializers/pipeline_artifacts_shared_example.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true +RSpec.shared_examples 'public artifacts' do + let_it_be(:project) { create(:project, :public) } + let(:pipeline) { create(:ci_empty_pipeline, status: :success, project: project) } + + context 'that has artifacts' do + let!(:build) { create(:ci_build, :success, :artifacts, pipeline: pipeline) } + + it 'contains information about artifacts' do + expect(subject[:details][:artifacts].length).to eq(1) + end + end + + context 'that has non public artifacts' do + let!(:build) { create(:ci_build, :success, :artifacts, :non_public_artifacts, pipeline: pipeline) } + + it 'does not contain information about artifacts' do + expect(subject[:details][:artifacts].length).to eq(0) + end + end +end diff --git a/spec/support/shared_examples/services/boards/issues_list_service_shared_examples.rb b/spec/support/shared_examples/services/boards/issues_list_service_shared_examples.rb index 06e2b715e6d..197b0694741 100644 --- a/spec/support/shared_examples/services/boards/issues_list_service_shared_examples.rb +++ b/spec/support/shared_examples/services/boards/issues_list_service_shared_examples.rb @@ -19,78 +19,12 @@ RSpec.shared_examples 'issues list service' do end end - it 'avoids N+1' do - params = { board_id: board.id } - control = ActiveRecord::QueryRecorder.new { described_class.new(parent, user, params).execute } - - create(:list, board: board) - - expect { described_class.new(parent, user, params).execute }.not_to exceed_query_limit(control) - end - - context 'issues are ordered by priority' do - it 'returns opened issues when list_id is missing' do - params = { board_id: board.id } - - issues = described_class.new(parent, user, params).execute - - expect(issues).to eq [opened_issue2, reopened_issue1, opened_issue1] - end - - it 'returns opened issues when listing issues from Backlog' do - params = { board_id: board.id, id: backlog.id } - - issues = described_class.new(parent, user, params).execute - - expect(issues).to eq [opened_issue2, reopened_issue1, opened_issue1] - end - - it 'returns opened issues that have label list applied when listing issues from a label list' do - params = { board_id: board.id, id: list1.id } - - issues = described_class.new(parent, user, params).execute - - expect(issues).to eq [list1_issue3, list1_issue1, list1_issue2] - end - end - - context 'issues are ordered by date of closing' do - it 'returns closed issues when listing issues from Closed' do - params = { board_id: board.id, id: closed.id } - - issues = described_class.new(parent, user, params).execute - - expect(issues).to eq [closed_issue1, closed_issue2, closed_issue3, closed_issue4, closed_issue5] - end - end - - context 'with list that does not belong to the board' do - it 'raises an error' do - list = create(:list) - service = described_class.new(parent, user, board_id: board.id, id: list.id) - - expect { service.execute }.to raise_error(ActiveRecord::RecordNotFound) - end - end - - context 'with invalid list id' do - it 'raises an error' do - service = described_class.new(parent, user, board_id: board.id, id: nil) - - expect { service.execute }.to raise_error(ActiveRecord::RecordNotFound) - end - end - - context 'when :all_lists is used' do - it 'returns issues from all lists' do - params = { board_id: board.id, all_lists: true } - - issues = described_class.new(parent, user, params).execute - - expected = [opened_issue2, reopened_issue1, opened_issue1, list1_issue1, - list1_issue2, list1_issue3, list2_issue1, closed_issue1, - closed_issue2, closed_issue3, closed_issue4, closed_issue5] - expect(issues).to match_array(expected) - end + it_behaves_like 'items list service' do + let(:backlog_items) { [opened_issue2, reopened_issue1, opened_issue1] } + let(:list1_items) { [list1_issue3, list1_issue1, list1_issue2] } + let(:closed_items) { [closed_issue1, closed_issue2, closed_issue3, closed_issue4, closed_issue5] } + let(:all_items) { backlog_items + list1_items + closed_items + [list2_issue1] } + let(:list_factory) { :list } + let(:new_list) { create(:list, board: board) } end end diff --git a/spec/support/shared_examples/services/boards/items_list_service_shared_examples.rb b/spec/support/shared_examples/services/boards/items_list_service_shared_examples.rb new file mode 100644 index 00000000000..9a3a0cc9cc8 --- /dev/null +++ b/spec/support/shared_examples/services/boards/items_list_service_shared_examples.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'items list service' do + it 'avoids N+1' do + params = { board_id: board.id } + control = ActiveRecord::QueryRecorder.new { described_class.new(parent, user, params).execute } + + new_list + + expect { described_class.new(parent, user, params).execute }.not_to exceed_query_limit(control) + end + + it 'returns opened items when list_id is missing' do + params = { board_id: board.id } + + items = described_class.new(parent, user, params).execute + + expect(items).to match_array(backlog_items) + end + + it 'returns opened items when listing items from Backlog' do + params = { board_id: board.id, id: backlog.id } + + items = described_class.new(parent, user, params).execute + + expect(items).to match_array(backlog_items) + end + + it 'returns opened items that have label list applied when listing items from a label list' do + params = { board_id: board.id, id: list1.id } + + items = described_class.new(parent, user, params).execute + + expect(items).to match_array(list1_items) + end + + it 'returns closed items when listing items from Closed sorted by closed_at in descending order' do + params = { board_id: board.id, id: closed.id } + + items = described_class.new(parent, user, params).execute + + expect(items).to eq(closed_items) + end + + it 'raises an error if the list does not belong to the board' do + list = create(list_factory) # rubocop:disable Rails/SaveBang + service = described_class.new(parent, user, board_id: board.id, id: list.id) + + expect { service.execute }.to raise_error(ActiveRecord::RecordNotFound) + end + + it 'raises an error if list id is invalid' do + service = described_class.new(parent, user, board_id: board.id, id: nil) + + expect { service.execute }.to raise_error(ActiveRecord::RecordNotFound) + end + + it 'returns items from all lists if :all_list is used' do + params = { board_id: board.id, all_lists: true } + + items = described_class.new(parent, user, params).execute + + expect(items).to match_array(all_items) + end +end diff --git a/spec/support/shared_examples/services/merge_request_shared_examples.rb b/spec/support/shared_examples/services/merge_request_shared_examples.rb index 2bd06ac3e9c..56179b6cd00 100644 --- a/spec/support/shared_examples/services/merge_request_shared_examples.rb +++ b/spec/support/shared_examples/services/merge_request_shared_examples.rb @@ -58,3 +58,18 @@ RSpec.shared_examples 'reviewer_ids filter' do end end end + +RSpec.shared_examples 'merge request reviewers cache counters invalidator' do + let(:reviewer_1) { create(:user) } + let(:reviewer_2) { create(:user) } + + before do + merge_request.update!(reviewers: [reviewer_1, reviewer_2]) + end + + it 'invalidates counter cache for reviewers' do + expect(merge_request.reviewers).to all(receive(:invalidate_merge_request_cache_counts)) + + described_class.new(project, user, {}).execute(merge_request) + end +end diff --git a/spec/support/shared_examples/services/namespace_package_settings_shared_examples.rb b/spec/support/shared_examples/services/namespace_package_settings_shared_examples.rb new file mode 100644 index 00000000000..8398dd3c453 --- /dev/null +++ b/spec/support/shared_examples/services/namespace_package_settings_shared_examples.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'updating the namespace package setting attributes' do |from: {}, to:| + it_behaves_like 'not creating the namespace package setting' + + it 'updates the namespace package setting' do + expect { subject } + .to change { namespace.package_settings.reload.maven_duplicates_allowed }.from(from[:maven_duplicates_allowed]).to(to[:maven_duplicates_allowed]) + .and change { namespace.package_settings.reload.maven_duplicate_exception_regex }.from(from[:maven_duplicate_exception_regex]).to(to[:maven_duplicate_exception_regex]) + end +end + +RSpec.shared_examples 'not creating the namespace package setting' do + it "doesn't create the namespace package setting" do + expect { subject }.not_to change { Namespace::PackageSetting.count } + end +end + +RSpec.shared_examples 'creating the namespace package setting' do + it 'creates a new package setting' do + expect { subject }.to change { Namespace::PackageSetting.count }.by(1) + end + + it 'saves the settings', :aggregate_failures do + subject + + expect(namespace.package_setting_relation.maven_duplicates_allowed).to eq(package_settings[:maven_duplicates_allowed]) + expect(namespace.package_setting_relation.maven_duplicate_exception_regex).to eq(package_settings[:maven_duplicate_exception_regex]) + end + + it_behaves_like 'returning a success' +end diff --git a/spec/support/shared_examples/services/onboarding_progress_shared_examples.rb b/spec/support/shared_examples/services/onboarding_progress_shared_examples.rb new file mode 100644 index 00000000000..8c6c2271af3 --- /dev/null +++ b/spec/support/shared_examples/services/onboarding_progress_shared_examples.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'records an onboarding progress action' do |action| + include AfterNextHelpers + + it do + expect_next(OnboardingProgressService, namespace) + .to receive(:execute).with(action: action).and_call_original + + subject + end +end + +RSpec.shared_examples 'does not record an onboarding progress action' do + it do + expect(OnboardingProgressService).not_to receive(:new) + + subject + 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 70d29b4bc99..fa307d2a9a6 100644 --- a/spec/support/shared_examples/services/packages_shared_examples.rb +++ b/spec/support/shared_examples/services/packages_shared_examples.rb @@ -220,3 +220,45 @@ RSpec.shared_examples 'package workhorse uploads' do end end end + +RSpec.shared_examples 'with versionless packages' do + context 'with versionless package' do + let!(:versionless_package) { create(:maven_package, project: project, version: nil) } + + shared_examples 'not including the package' do + it 'does not return the package' do + subject + + expect(json_response.map { |package| package['id'] }).not_to include(versionless_package.id) + end + end + + it_behaves_like 'not including the package' + + context 'with include_versionless param' do + context 'with true include_versionless param' do + [true, 'true', 1, '1'].each do |param| + context "for param #{param}" do + let(:params) { super().merge(include_versionless: param) } + + it 'returns the package' do + subject + + expect(json_response.map { |package| package['id'] }).to include(versionless_package.id) + end + end + end + end + + context 'with falsy include_versionless param' do + [false, '', nil, 'false', 0, '0'].each do |param| + context "for param #{param}" do + let(:params) { super().merge(include_versionless: param) } + + it_behaves_like 'not including the package' + end + end + end + end + end +end diff --git a/spec/support/shared_examples/services/repositories/housekeeping_shared_examples.rb b/spec/support/shared_examples/services/repositories/housekeeping_shared_examples.rb new file mode 100644 index 00000000000..a174ae94b75 --- /dev/null +++ b/spec/support/shared_examples/services/repositories/housekeeping_shared_examples.rb @@ -0,0 +1,118 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'housekeeps repository' do + subject { described_class.new(resource) } + + context 'with a clean redis state', :clean_gitlab_redis_shared_state do + describe '#execute' do + it 'enqueues a sidekiq job' do + expect(subject).to receive(:try_obtain_lease).and_return(:the_uuid) + expect(subject).to receive(:lease_key).and_return(:the_lease_key) + expect(subject).to receive(:task).and_return(:incremental_repack) + expect(GitGarbageCollectWorker).to receive(:perform_async).with(resource.id, :incremental_repack, :the_lease_key, :the_uuid).and_call_original + + Sidekiq::Testing.fake! do + expect { subject.execute }.to change(GitGarbageCollectWorker.jobs, :size).by(1) + end + end + + it 'yields the block if given' do + expect do |block| + subject.execute(&block) + end.to yield_with_no_args + end + + it 'resets counter after execution' do + expect(subject).to receive(:try_obtain_lease).and_return(:the_uuid) + allow(subject).to receive(:gc_period).and_return(1) + resource.increment_pushes_since_gc + + perform_enqueued_jobs do + expect { subject.execute }.to change { resource.pushes_since_gc }.to(0) + end + end + + context 'when no lease can be obtained' do + before do + expect(subject).to receive(:try_obtain_lease).and_return(false) + end + + it 'does not enqueue a job' do + expect(GitGarbageCollectWorker).not_to receive(:perform_async) + + expect { subject.execute }.to raise_error(Repositories::HousekeepingService::LeaseTaken) + end + + it 'does not reset pushes_since_gc' do + expect do + expect { subject.execute }.to raise_error(Repositories::HousekeepingService::LeaseTaken) + end.not_to change { resource.pushes_since_gc } + end + + it 'does not yield' do + expect do |block| + expect { subject.execute(&block) } + .to raise_error(Repositories::HousekeepingService::LeaseTaken) + end.not_to yield_with_no_args + end + end + + context 'task type' do + it 'goes through all three housekeeping tasks, executing only the highest task when there is overlap' do + allow(subject).to receive(:try_obtain_lease).and_return(:the_uuid) + allow(subject).to receive(:lease_key).and_return(:the_lease_key) + + # At push 200 + expect(GitGarbageCollectWorker).to receive(:perform_async).with(resource.id, :gc, :the_lease_key, :the_uuid) + .once + # At push 50, 100, 150 + expect(GitGarbageCollectWorker).to receive(:perform_async).with(resource.id, :full_repack, :the_lease_key, :the_uuid) + .exactly(3).times + # At push 10, 20, ... (except those above) + expect(GitGarbageCollectWorker).to receive(:perform_async).with(resource.id, :incremental_repack, :the_lease_key, :the_uuid) + .exactly(16).times + # At push 6, 12, 18, ... (except those above) + expect(GitGarbageCollectWorker).to receive(:perform_async).with(resource.id, :pack_refs, :the_lease_key, :the_uuid) + .exactly(27).times + + 201.times do + subject.increment! + subject.execute if subject.needed? + end + + expect(resource.pushes_since_gc).to eq(1) + end + end + + it 'runs the task specifically requested' do + housekeeping = described_class.new(resource, :gc) + + allow(housekeeping).to receive(:try_obtain_lease).and_return(:gc_uuid) + allow(housekeeping).to receive(:lease_key).and_return(:gc_lease_key) + + expect(GitGarbageCollectWorker).to receive(:perform_async).with(resource.id, :gc, :gc_lease_key, :gc_uuid).twice + + 2.times do + housekeeping.execute + end + end + end + + describe '#needed?' do + it 'when the count is low enough' do + expect(subject.needed?).to eq(false) + end + + it 'when the count is high enough' do + allow(resource).to receive(:pushes_since_gc).and_return(10) + expect(subject.needed?).to eq(true) + end + end + + describe '#increment!' do + it 'increments the pushes_since_gc counter' do + expect { subject.increment! }.to change { resource.pushes_since_gc }.by(1) + end + end + end +end diff --git a/spec/support/shared_examples/services/schedule_bulk_repository_shard_moves_shared_examples.rb b/spec/support/shared_examples/services/schedule_bulk_repository_shard_moves_shared_examples.rb new file mode 100644 index 00000000000..e67fc4ab04a --- /dev/null +++ b/spec/support/shared_examples/services/schedule_bulk_repository_shard_moves_shared_examples.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'moves repository shard in bulk' do + let(:source_storage_name) { 'default' } + let(:destination_storage_name) { 'test_second_storage' } + + before do + stub_storage_settings(destination_storage_name => { 'path' => 'tmp/tests/extra_storage' }) + end + + describe '#execute' do + it 'schedules container repository storage moves' do + expect { subject.execute(source_storage_name, destination_storage_name) } + .to change(move_service_klass, :count).by(1) + + storage_move = container.repository_storage_moves.last! + + expect(storage_move).to have_attributes( + source_storage_name: source_storage_name, + destination_storage_name: destination_storage_name, + state_name: :scheduled + ) + end + + context 'read-only repository' do + it 'does not get scheduled' do + container.set_repository_read_only! + + expect(subject).to receive(:log_info) + .with(/Container #{container.full_path} \(#{container.id}\) was skipped: #{container.class} is read only/) + expect { subject.execute(source_storage_name, destination_storage_name) } + .to change(move_service_klass, :count).by(0) + end + end + end + + describe '.enqueue' do + it 'defers to the worker' do + expect(bulk_worker_klass).to receive(:perform_async).with(source_storage_name, destination_storage_name) + + described_class.enqueue(source_storage_name, destination_storage_name) + end + end +end diff --git a/spec/support/shared_examples/workers/schedule_bulk_repository_shard_moves_shared_examples.rb b/spec/support/shared_examples/workers/schedule_bulk_repository_shard_moves_shared_examples.rb new file mode 100644 index 00000000000..465aca63148 --- /dev/null +++ b/spec/support/shared_examples/workers/schedule_bulk_repository_shard_moves_shared_examples.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'schedules bulk repository shard moves' do + let(:source_storage_name) { 'default' } + let(:destination_storage_name) { 'test_second_storage' } + + describe "#perform" do + before do + stub_storage_settings(destination_storage_name => { 'path' => 'tmp/tests/extra_storage' }) + + allow(worker_klass).to receive(:perform_async) + end + + include_examples 'an idempotent worker' do + let(:job_args) { [source_storage_name, destination_storage_name] } + + it 'schedules container repository storage moves' do + expect { subject }.to change(move_service_klass, :count).by(1) + + storage_move = container.repository_storage_moves.last! + + expect(storage_move).to have_attributes( + source_storage_name: source_storage_name, + destination_storage_name: destination_storage_name, + state_name: :scheduled + ) + end + end + end +end diff --git a/spec/support/shared_examples/workers/update_repository_move_shared_examples.rb b/spec/support/shared_examples/workers/update_repository_move_shared_examples.rb new file mode 100644 index 00000000000..babd7cfbbeb --- /dev/null +++ b/spec/support/shared_examples/workers/update_repository_move_shared_examples.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'an update storage move worker' do + describe '#perform' do + let(:service) { double(:update_repository_storage_service) } + + before do + allow(Gitlab.config.repositories.storages).to receive(:keys).and_return(%w[default test_second_storage]) + end + + context 'without repository storage move' do + it 'calls the update repository storage service' do + expect(service_klass).to receive(:new).and_return(service) + expect(service).to receive(:execute) + + expect do + subject.perform(container.id, 'test_second_storage') + end.to change(repository_storage_move_klass, :count).by(1) + + storage_move = container.repository_storage_moves.last + expect(storage_move).to have_attributes( + source_storage_name: 'default', + destination_storage_name: 'test_second_storage' + ) + end + end + + context 'with repository storage move' do + it 'calls the update repository storage service' do + expect(service_klass).to receive(:new).and_return(service) + expect(service).to receive(:execute) + + expect do + subject.perform(nil, nil, repository_storage_move.id) + end.not_to change(repository_storage_move_klass, :count) + end + end + end +end |