summaryrefslogtreecommitdiff
path: root/spec/support
diff options
context:
space:
mode:
Diffstat (limited to 'spec/support')
-rw-r--r--spec/support/atlassian/jira_connect/schemata.rb337
-rw-r--r--spec/support/capybara.rb2
-rw-r--r--spec/support/gitlab_stubs/gitlab_ci_for_sast.yml3
-rw-r--r--spec/support/gitlab_stubs/gitlab_ci_for_sast_default_analyzers.yml15
-rw-r--r--spec/support/gitlab_stubs/gitlab_ci_for_sast_excluded_analyzers.yml14
-rw-r--r--spec/support/gitlab_stubs/gitlab_ci_includes.yml26
-rw-r--r--spec/support/helpers/database/database_helpers.rb15
-rw-r--r--spec/support/helpers/database/partitioning_helpers.rb96
-rw-r--r--spec/support/helpers/database/table_schema_helpers.rb149
-rw-r--r--spec/support/helpers/database/trigger_helpers.rb68
-rw-r--r--spec/support/helpers/database_helpers.rb13
-rw-r--r--spec/support/helpers/graphql_helpers.rb42
-rw-r--r--spec/support/helpers/multipart_helpers.rb20
-rw-r--r--spec/support/helpers/partitioning_helpers.rb94
-rw-r--r--spec/support/helpers/rack_attack_spec_helpers.rb21
-rw-r--r--spec/support/helpers/stub_experiments.rb8
-rw-r--r--spec/support/helpers/stub_feature_flags.rb4
-rw-r--r--spec/support/helpers/table_schema_helpers.rb112
-rw-r--r--spec/support/helpers/test_env.rb9
-rw-r--r--spec/support/helpers/trigger_helpers.rb66
-rw-r--r--spec/support/matchers/be_sorted.rb71
-rw-r--r--spec/support/matchers/be_valid_json.rb16
-rw-r--r--spec/support/matchers/schema_matcher.rb10
-rw-r--r--spec/support/rspec.rb4
-rw-r--r--spec/support/shared_contexts/email_shared_context.rb245
-rw-r--r--spec/support/shared_contexts/finders/issues_finder_shared_contexts.rb48
-rw-r--r--spec/support/shared_contexts/finders/users_finder_shared_contexts.rb1
-rw-r--r--spec/support/shared_contexts/navbar_structure_context.rb1
-rw-r--r--spec/support/shared_contexts/policies/group_policy_shared_context.rb25
-rw-r--r--spec/support/shared_contexts/read_ci_configuration_shared_context.rb8
-rw-r--r--spec/support/shared_contexts/requests/api/nuget_packages_shared_context.rb10
-rw-r--r--spec/support/shared_examples/controllers/repositories/git_http_controller_shared_examples.rb24
-rw-r--r--spec/support/shared_examples/controllers/sessionless_auth_controller_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/controllers/unique_hll_events_examples.rb2
-rw-r--r--spec/support/shared_examples/features/file_uploads_shared_examples.rb24
-rw-r--r--spec/support/shared_examples/features/master_manages_access_requests_shared_example.rb10
-rw-r--r--spec/support/shared_examples/features/wiki/user_creates_wiki_page_shared_examples.rb32
-rw-r--r--spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb35
-rw-r--r--spec/support/shared_examples/features/wiki/user_uses_wiki_shortcuts_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/features/wiki/user_views_wiki_empty_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/features/wiki/user_views_wiki_page_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/features/wiki/user_views_wiki_sidebar_shared_examples.rb52
-rw-r--r--spec/support/shared_examples/finders/packages/debian/distributions_finder_shared_examples.rb79
-rw-r--r--spec/support/shared_examples/finders/packages_shared_examples.rb19
-rw-r--r--spec/support/shared_examples/graphql/mutations/http_integrations_shared_examples.rb19
-rw-r--r--spec/support/shared_examples/graphql/projects/merge_request_n_plus_one_query_examples.rb6
-rw-r--r--spec/support/shared_examples/lib/banzai/filters/sanitization_filter_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/lib/gitlab/cycle_analytics/base_stage_shared_examples.rb74
-rw-r--r--spec/support/shared_examples/lib/gitlab/cycle_analytics/default_query_config_shared_examples.rb16
-rw-r--r--spec/support/shared_examples/lib/gitlab/middleware/multipart_shared_examples.rb24
-rw-r--r--spec/support/shared_examples/lib/gitlab/project_search_results_shared_examples.rb19
-rw-r--r--spec/support/shared_examples/lib/gitlab/usage_data_counters/incident_management_activity_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/metrics/sampler_shared_examples.rb27
-rw-r--r--spec/support/shared_examples/models/boards/listable_shared_examples.rb97
-rw-r--r--spec/support/shared_examples/models/concerns/can_housekeep_repository_shared_examples.rb45
-rw-r--r--spec/support/shared_examples/models/concerns/repositories/can_housekeep_repository_shared_examples.rb45
-rw-r--r--spec/support/shared_examples/models/concerns/repository_storage_movable_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/models/packages/debian/architecture_shared_examples.rb48
-rw-r--r--spec/support/shared_examples/models/packages/debian/distribution_shared_examples.rb225
-rw-r--r--spec/support/shared_examples/quick_actions/merge_request/rebase_quick_action_shared_examples.rb92
-rw-r--r--spec/support/shared_examples/requests/api/boards_shared_examples.rb33
-rw-r--r--spec/support/shared_examples/requests/api/debian_packages_shared_examples.rb69
-rw-r--r--spec/support/shared_examples/requests/api/nuget_endpoints_shared_examples.rb146
-rw-r--r--spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb58
-rw-r--r--spec/support/shared_examples/requests/api/repository_storage_moves_shared_examples.rb219
-rw-r--r--spec/support/shared_examples/requests/api/resolvable_discussions_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/requests/rack_attack_shared_examples.rb5
-rw-r--r--spec/support/shared_examples/requests/releases_shared_examples.rb61
-rw-r--r--spec/support/shared_examples/requests/sessionless_auth_request_shared_examples.rb84
-rw-r--r--spec/support/shared_examples/routing/legacy_path_redirect_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/serializers/pipeline_artifacts_shared_example.rb21
-rw-r--r--spec/support/shared_examples/services/boards/issues_list_service_shared_examples.rb80
-rw-r--r--spec/support/shared_examples/services/boards/items_list_service_shared_examples.rb65
-rw-r--r--spec/support/shared_examples/services/merge_request_shared_examples.rb15
-rw-r--r--spec/support/shared_examples/services/namespace_package_settings_shared_examples.rb32
-rw-r--r--spec/support/shared_examples/services/onboarding_progress_shared_examples.rb20
-rw-r--r--spec/support/shared_examples/services/packages_shared_examples.rb42
-rw-r--r--spec/support/shared_examples/services/repositories/housekeeping_shared_examples.rb118
-rw-r--r--spec/support/shared_examples/services/schedule_bulk_repository_shard_moves_shared_examples.rb44
-rw-r--r--spec/support/shared_examples/workers/schedule_bulk_repository_shard_moves_shared_examples.rb30
-rw-r--r--spec/support/shared_examples/workers/update_repository_move_shared_examples.rb39
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