summaryrefslogtreecommitdiff
path: root/spec/support
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-06-18 11:18:50 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2020-06-18 11:18:50 +0000
commit8c7f4e9d5f36cff46365a7f8c4b9c21578c1e781 (patch)
treea77e7fe7a93de11213032ed4ab1f33a3db51b738 /spec/support
parent00b35af3db1abfe813a778f643dad221aad51fca (diff)
downloadgitlab-ce-8c7f4e9d5f36cff46365a7f8c4b9c21578c1e781.tar.gz
Add latest changes from gitlab-org/gitlab@13-1-stable-ee
Diffstat (limited to 'spec/support')
-rw-r--r--spec/support/action_cable.rb7
-rw-r--r--spec/support/helpers/api_helpers.rb11
-rw-r--r--spec/support/helpers/design_management_test_helpers.rb3
-rw-r--r--spec/support/helpers/filter_spec_helper.rb16
-rw-r--r--spec/support/helpers/graphql_helpers.rb65
-rw-r--r--spec/support/helpers/http_basic_auth_helpers.rb26
-rw-r--r--spec/support/helpers/login_helpers.rb6
-rw-r--r--spec/support/helpers/markdown_feature.rb8
-rw-r--r--spec/support/helpers/partitioning_helpers.rb54
-rw-r--r--spec/support/helpers/prometheus_helpers.rb47
-rw-r--r--spec/support/helpers/stub_action_cable_connection.rb7
-rw-r--r--spec/support/helpers/stub_feature_flags.rb58
-rw-r--r--spec/support/helpers/stub_gitlab_calls.rb6
-rw-r--r--spec/support/helpers/trigger_helpers.rb65
-rw-r--r--spec/support/helpers/usage_data_helpers.rb24
-rw-r--r--spec/support/helpers/wiki_helpers.rb4
-rw-r--r--spec/support/import_export/common_util.rb2
-rw-r--r--spec/support/let_it_be.rb9
-rw-r--r--spec/support/matchers/exceed_query_limit.rb101
-rw-r--r--spec/support/matchers/graphql_matchers.rb22
-rw-r--r--spec/support/rspec.rb3
-rw-r--r--spec/support/shared_contexts/finders/merge_requests_finder_shared_contexts.rb31
-rw-r--r--spec/support/shared_contexts/navbar_structure_context.rb5
-rw-r--r--spec/support/shared_contexts/project_service_shared_context.rb1
-rw-r--r--spec/support/shared_contexts/requests/api/graphql/jira_import/jira_projects_context.rb104
-rw-r--r--spec/support/shared_contexts/spam_constants.rb9
-rw-r--r--spec/support/shared_examples/controllers/import_controller_new_import_ui_shared_examples.rb36
-rw-r--r--spec/support/shared_examples/controllers/issuables_list_metadata_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/controllers/milestone_tabs_shared_examples.rb10
-rw-r--r--spec/support/shared_examples/controllers/namespace_storage_limit_alert_shared_examples.rb53
-rw-r--r--spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb302
-rw-r--r--spec/support/shared_examples/features/comments_on_merge_request_files_shared_examples.rb3
-rw-r--r--spec/support/shared_examples/features/discussion_comments_shared_example.rb6
-rw-r--r--spec/support/shared_examples/graphql/container_expiration_policy_shared_examples.rb12
-rw-r--r--spec/support/shared_examples/graphql/label_fields.rb124
-rw-r--r--spec/support/shared_examples/graphql/members_shared_examples.rb22
-rw-r--r--spec/support/shared_examples/graphql/resolves_issuable_shared_examples.rb14
-rw-r--r--spec/support/shared_examples/graphql/sorted_paginated_query_shared_examples.rb18
-rw-r--r--spec/support/shared_examples/integrations/test_examples.rb11
-rw-r--r--spec/support/shared_examples/lib/gitlab/gl_repository_shared_examples.rb12
-rw-r--r--spec/support/shared_examples/lib/gitlab/import/stuck_import_job_workers_shared_examples.rb44
-rw-r--r--spec/support/shared_examples/lib/gitlab/position_formatters_shared_examples.rb16
-rw-r--r--spec/support/shared_examples/lib/gitlab/repo_type_shared_examples.rb20
-rw-r--r--spec/support/shared_examples/models/application_setting_shared_examples.rb34
-rw-r--r--spec/support/shared_examples/models/cluster_application_helm_cert_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/models/cluster_application_initial_status_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/models/cluster_application_status_shared_examples.rb63
-rw-r--r--spec/support/shared_examples/models/concerns/blob_replicator_strategy_shared_examples.rb117
-rw-r--r--spec/support/shared_examples/models/concerns/has_repository_shared_examples.rb8
-rw-r--r--spec/support/shared_examples/models/concerns/limitable_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/models/wiki_shared_examples.rb7
-rw-r--r--spec/support/shared_examples/path_extraction_shared_examples.rb118
-rw-r--r--spec/support/shared_examples/quick_actions/issuable/time_tracking_quick_action_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/requests/api/container_repositories_shared_examples.rb23
-rw-r--r--spec/support/shared_examples/requests/api/diff_discussions_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/requests/api/graphql/group_and_project_boards_query_shared_examples.rb55
-rw-r--r--spec/support/shared_examples/requests/api/issuable_update_shared_examples.rb38
-rw-r--r--spec/support/shared_examples/requests/api/resource_label_events_api_shared_examples.rb14
-rw-r--r--spec/support/shared_examples/requests/api/resource_milestone_events_api_shared_examples.rb66
-rw-r--r--spec/support/shared_examples/serializers/diff_file_entity_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/serializers/import/import_entity_shared_examples.rb21
-rw-r--r--spec/support/shared_examples/services/container_expiration_policy_shared_examples.rb40
-rw-r--r--spec/support/shared_examples/services/wiki_pages/create_service_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/services/wiki_pages/destroy_service_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/services/wiki_pages/update_service_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/uncached_response_shared_examples.rb12
66 files changed, 1746 insertions, 289 deletions
diff --git a/spec/support/action_cable.rb b/spec/support/action_cable.rb
new file mode 100644
index 00000000000..64cfc435875
--- /dev/null
+++ b/spec/support/action_cable.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+RSpec.configure do |config|
+ config.before(:each, type: :channel) do
+ stub_action_cable_connection
+ end
+end
diff --git a/spec/support/helpers/api_helpers.rb b/spec/support/helpers/api_helpers.rb
index eb9594a4fb6..b1e6078c4f2 100644
--- a/spec/support/helpers/api_helpers.rb
+++ b/spec/support/helpers/api_helpers.rb
@@ -40,17 +40,6 @@ module ApiHelpers
end
end
- def basic_auth_header(user = nil)
- return { 'HTTP_AUTHORIZATION' => user } unless user.respond_to?(:username)
-
- {
- 'HTTP_AUTHORIZATION' => ActionController::HttpAuthentication::Basic.encode_credentials(
- user.username,
- create(:personal_access_token, user: user).token
- )
- }
- end
-
def expect_empty_array_response
expect_successful_response_with_paginated_array
expect(json_response.length).to eq(0)
diff --git a/spec/support/helpers/design_management_test_helpers.rb b/spec/support/helpers/design_management_test_helpers.rb
index bf41e2f5079..1daa92e8ad4 100644
--- a/spec/support/helpers/design_management_test_helpers.rb
+++ b/spec/support/helpers/design_management_test_helpers.rb
@@ -1,9 +1,8 @@
# frozen_string_literal: true
module DesignManagementTestHelpers
- def enable_design_management(enabled = true, ref_filter = true)
+ def enable_design_management(enabled = true)
stub_lfs_setting(enabled: enabled)
- stub_feature_flags(design_management_reference_filter_gfm_pipeline: ref_filter)
end
def delete_designs(*designs)
diff --git a/spec/support/helpers/filter_spec_helper.rb b/spec/support/helpers/filter_spec_helper.rb
index c165128040f..ca844b33ba8 100644
--- a/spec/support/helpers/filter_spec_helper.rb
+++ b/spec/support/helpers/filter_spec_helper.rb
@@ -56,14 +56,11 @@ module FilterSpecHelper
pipeline.call(body)
end
- def reference_pipeline(context = {})
+ def reference_pipeline(filter: described_class, **context)
context.reverse_merge!(project: project) if defined?(project)
context.reverse_merge!(current_user: current_user) if defined?(current_user)
- filters = [
- Banzai::Filter::AutolinkFilter,
- described_class
- ]
+ filters = [Banzai::Filter::AutolinkFilter, filter].compact
redact = context.delete(:redact)
filters.push(Banzai::Filter::ReferenceRedactorFilter) if redact
@@ -75,8 +72,13 @@ module FilterSpecHelper
reference_pipeline(context).call(body)
end
- def reference_filter(html, context = {})
- reference_pipeline(context).to_document(html)
+ def reference_filter(text, context = {})
+ reference_pipeline(**context).to_document(text)
+ end
+
+ # Use to test no-ops
+ def null_filter(text, context = {})
+ reference_pipeline(filter: nil, **context).to_document(text)
end
# Modify a String reference to make it invalid
diff --git a/spec/support/helpers/graphql_helpers.rb b/spec/support/helpers/graphql_helpers.rb
index b3d7f7bcece..87525734490 100644
--- a/spec/support/helpers/graphql_helpers.rb
+++ b/spec/support/helpers/graphql_helpers.rb
@@ -11,9 +11,19 @@ module GraphqlHelpers
underscored_field_name.to_s.camelize(:lower)
end
- # Run a loader's named resolver
+ # Run a loader's named resolver in a way that closely mimics the framework.
+ #
+ # First the `ready?` method is called. If it turns out that the resolver is not
+ # ready, then the early return is returned instead.
+ #
+ # Then the resolve method is called.
def resolve(resolver_class, obj: nil, args: {}, ctx: {}, field: nil)
- resolver_class.new(object: obj, context: ctx, field: field).resolve(args)
+ resolver = resolver_class.new(object: obj, context: ctx, field: field)
+ ready, early_return = sync_all { resolver.ready?(**args) }
+
+ return early_return unless ready
+
+ resolver.resolve(args)
end
# Eagerly run a loader's named resolver
@@ -51,12 +61,12 @@ module GraphqlHelpers
# BatchLoader::GraphQL returns a wrapper, so we need to :sync in order
# to get the actual values
def batch_sync(max_queries: nil, &blk)
- wrapper = proc do
- lazy_vals = yield
- lazy_vals.is_a?(Array) ? lazy_vals.map { |val| sync(val) } : sync(lazy_vals)
- end
+ batch(max_queries: max_queries) { sync_all(&blk) }
+ end
- batch(max_queries: max_queries, &wrapper)
+ def sync_all(&blk)
+ lazy_vals = yield
+ lazy_vals.is_a?(Array) ? lazy_vals.map { |val| sync(val) } : sync(lazy_vals)
end
def graphql_query_for(name, attributes = {}, fields = nil)
@@ -67,10 +77,14 @@ module GraphqlHelpers
QUERY
end
- def graphql_mutation(name, input, fields = nil)
+ def graphql_mutation(name, input, fields = nil, &block)
+ raise ArgumentError, 'Please pass either `fields` parameter or a block to `#graphql_mutation`, but not both.' if fields.present? && block_given?
+
mutation_name = GraphqlHelpers.fieldnamerize(name)
input_variable_name = "$#{input_variable_name_for_mutation(name)}"
mutation_field = GitlabSchema.mutation.fields[mutation_name]
+
+ fields = yield if block_given?
fields ||= all_graphql_fields_for(mutation_field.type.to_graphql)
query = <<~MUTATION
@@ -139,7 +153,15 @@ module GraphqlHelpers
end
def wrap_fields(fields)
- fields = Array.wrap(fields).join("\n")
+ fields = Array.wrap(fields).map do |field|
+ case field
+ when Symbol
+ GraphqlHelpers.fieldnamerize(field)
+ else
+ field
+ end
+ end.join("\n")
+
return unless fields.present?
<<~FIELDS
@@ -257,8 +279,13 @@ module GraphqlHelpers
end
def graphql_dig_at(data, *path)
- keys = path.map { |segment| GraphqlHelpers.fieldnamerize(segment) }
- data.dig(*keys)
+ keys = path.map { |segment| segment.is_a?(Integer) ? segment : GraphqlHelpers.fieldnamerize(segment) }
+
+ # Allows for array indexing, like this
+ # ['project', 'boards', 'edges', 0, 'node', 'lists']
+ keys.reduce(data) do |memo, key|
+ memo.is_a?(Array) ? memo[key] : memo&.dig(key)
+ end
end
def graphql_errors
@@ -294,6 +321,22 @@ module GraphqlHelpers
graphql_data.fetch(GraphqlHelpers.fieldnamerize(mutation_name))
end
+ def scalar_fields_of(type_name)
+ GitlabSchema.types[type_name].fields.map do |name, field|
+ next if nested_fields?(field) || required_arguments?(field)
+
+ name
+ end.compact
+ end
+
+ def nested_fields_of(type_name)
+ GitlabSchema.types[type_name].fields.map do |name, field|
+ next if !nested_fields?(field) || required_arguments?(field)
+
+ [name, field]
+ end.compact
+ end
+
def nested_fields?(field)
!scalar?(field) && !enum?(field)
end
diff --git a/spec/support/helpers/http_basic_auth_helpers.rb b/spec/support/helpers/http_basic_auth_helpers.rb
new file mode 100644
index 00000000000..c0b24b3dfa4
--- /dev/null
+++ b/spec/support/helpers/http_basic_auth_helpers.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+module HttpBasicAuthHelpers
+ def user_basic_auth_header(user)
+ access_token = create(:personal_access_token, user: user)
+
+ basic_auth_header(user.username, access_token.token)
+ end
+
+ def job_basic_auth_header(job)
+ basic_auth_header(Ci::Build::CI_REGISTRY_USER, job.token)
+ end
+
+ def client_basic_auth_header(client)
+ basic_auth_header(client.uid, client.secret)
+ end
+
+ def basic_auth_header(username, password)
+ {
+ 'HTTP_AUTHORIZATION' => ActionController::HttpAuthentication::Basic.encode_credentials(
+ username,
+ password
+ )
+ }
+ end
+end
diff --git a/spec/support/helpers/login_helpers.rb b/spec/support/helpers/login_helpers.rb
index cb880939b1c..92f6d673255 100644
--- a/spec/support/helpers/login_helpers.rb
+++ b/spec/support/helpers/login_helpers.rb
@@ -57,13 +57,13 @@ module LoginHelpers
def gitlab_sign_in_via(provider, user, uid, saml_response = nil)
mock_auth_hash_with_saml_xml(provider, uid, user.email, saml_response)
visit new_user_session_path
- click_link provider
+ click_button provider
end
def gitlab_enable_admin_mode_sign_in_via(provider, user, uid, saml_response = nil)
mock_auth_hash_with_saml_xml(provider, uid, user.email, saml_response)
visit new_admin_session_path
- click_link provider
+ click_button provider
end
# Requires Javascript driver.
@@ -103,7 +103,7 @@ module LoginHelpers
check 'remember_me' if remember_me
- click_link "oauth-login-#{provider}"
+ click_button "oauth-login-#{provider}"
end
def fake_successful_u2f_authentication
diff --git a/spec/support/helpers/markdown_feature.rb b/spec/support/helpers/markdown_feature.rb
index eea03fb9325..40e0d4413e2 100644
--- a/spec/support/helpers/markdown_feature.rb
+++ b/spec/support/helpers/markdown_feature.rb
@@ -36,12 +36,12 @@ class MarkdownFeature
end
end
- def project_wiki
- @project_wiki ||= ProjectWiki.new(project, user)
+ def wiki
+ @wiki ||= ProjectWiki.new(project, user)
end
- def project_wiki_page
- @project_wiki_page ||= build(:wiki_page, wiki: project_wiki)
+ def wiki_page
+ @wiki_page ||= build(:wiki_page, wiki: wiki)
end
def issue
diff --git a/spec/support/helpers/partitioning_helpers.rb b/spec/support/helpers/partitioning_helpers.rb
new file mode 100644
index 00000000000..98a13915d76
--- /dev/null
+++ b/spec/support/helpers/partitioning_helpers.rb
@@ -0,0 +1,54 @@
+# 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)
+
+ 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
+
+ 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)
+ 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
+ where pg_class.relname = '#{partition}' and pg_class.relispartition;
+ SQL
+ end
+end
diff --git a/spec/support/helpers/prometheus_helpers.rb b/spec/support/helpers/prometheus_helpers.rb
index fdce00e7dec..d49abbf3f19 100644
--- a/spec/support/helpers/prometheus_helpers.rb
+++ b/spec/support/helpers/prometheus_helpers.rb
@@ -236,4 +236,51 @@ module PrometheusHelpers
]
}
end
+
+ def prometheus_alert_payload(firing: [], resolved: [])
+ status = firing.any? ? 'firing' : 'resolved'
+ alerts = firing + resolved
+ alert_name = alerts.first&.title || ''
+ prometheus_metric_id = alerts.first&.prometheus_metric_id&.to_s
+
+ alerts_map = \
+ firing.map { |alert| prometheus_map_alert_payload('firing', alert) } +
+ resolved.map { |alert| prometheus_map_alert_payload('resolved', alert) }
+
+ # See https://prometheus.io/docs/alerting/configuration/#%3Cwebhook_config%3E
+ {
+ 'version' => '4',
+ 'receiver' => 'gitlab',
+ 'status' => status,
+ 'alerts' => alerts_map,
+ 'groupLabels' => {
+ 'alertname' => alert_name
+ },
+ 'commonLabels' => {
+ 'alertname' => alert_name,
+ 'gitlab' => 'hook',
+ 'gitlab_alert_id' => prometheus_metric_id
+ },
+ 'commonAnnotations' => {},
+ 'externalURL' => '',
+ 'groupKey' => "{}:{alertname=\'#{alert_name}\'}"
+ }
+ end
+
+ private
+
+ def prometheus_map_alert_payload(status, alert)
+ {
+ 'status' => status,
+ 'labels' => {
+ 'alertname' => alert.title,
+ 'gitlab' => 'hook',
+ 'gitlab_alert_id' => alert.prometheus_metric_id.to_s
+ },
+ 'annotations' => {},
+ 'startsAt' => '2018-09-24T08:57:31.095725221Z',
+ 'endsAt' => '0001-01-01T00:00:00Z',
+ 'generatorURL' => 'http://prometheus-prometheus-server-URL'
+ }
+ end
end
diff --git a/spec/support/helpers/stub_action_cable_connection.rb b/spec/support/helpers/stub_action_cable_connection.rb
new file mode 100644
index 00000000000..b4e9c2ae48c
--- /dev/null
+++ b/spec/support/helpers/stub_action_cable_connection.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+module StubActionCableConnection
+ def stub_action_cable_connection(current_user: nil, request: ActionDispatch::TestRequest.create)
+ stub_connection(current_user: current_user, request: request)
+ end
+end
diff --git a/spec/support/helpers/stub_feature_flags.rb b/spec/support/helpers/stub_feature_flags.rb
index 5b8a85b206f..696148cacaf 100644
--- a/spec/support/helpers/stub_feature_flags.rb
+++ b/spec/support/helpers/stub_feature_flags.rb
@@ -1,6 +1,38 @@
# frozen_string_literal: true
module StubFeatureFlags
+ class StubFeatureGate
+ attr_reader :flipper_id
+
+ def initialize(flipper_id)
+ @flipper_id = flipper_id
+ end
+ end
+
+ def stub_all_feature_flags
+ adapter = Flipper::Adapters::Memory.new
+ flipper = Flipper.new(adapter)
+
+ allow(Feature).to receive(:flipper).and_return(flipper)
+
+ # All new requested flags are enabled by default
+ allow(Feature).to receive(:enabled?).and_wrap_original do |m, *args|
+ feature_flag = m.call(*args)
+
+ # If feature flag is not persisted we mark the feature flag as enabled
+ # We do `m.call` as we want to validate the execution of method arguments
+ # and a feature flag state if it is not persisted
+ unless Feature.persisted_name?(args.first)
+ # TODO: this is hack to support `promo_feature_available?`
+ # We enable all feature flags by default unless they are `promo_`
+ # Issue: https://gitlab.com/gitlab-org/gitlab/-/issues/218667
+ feature_flag = true unless args.first.to_s.start_with?('promo_')
+ end
+
+ feature_flag
+ end
+ end
+
# Stub Feature flags with `flag_name: true/false`
#
# @param [Hash] features where key is feature name and value is boolean whether enabled or not.
@@ -14,23 +46,29 @@ module StubFeatureFlags
# Enable `ci_live_trace` feature flag only on the specified projects.
def stub_feature_flags(features)
features.each do |feature_name, actors|
- allow(Feature).to receive(:enabled?).with(feature_name, any_args).and_return(false)
- allow(Feature).to receive(:enabled?).with(feature_name.to_s, any_args).and_return(false)
+ # Remove feature flag overwrite
+ feature = Feature.get(feature_name) # rubocop:disable Gitlab/AvoidFeatureGet
+ feature.remove
Array(actors).each do |actor|
raise ArgumentError, "actor cannot be Hash" if actor.is_a?(Hash)
- case actor
- when false, true
- allow(Feature).to receive(:enabled?).with(feature_name, any_args).and_return(actor)
- allow(Feature).to receive(:enabled?).with(feature_name.to_s, any_args).and_return(actor)
- when nil, ActiveRecord::Base, Symbol, RSpec::Mocks::Double
- allow(Feature).to receive(:enabled?).with(feature_name, actor, any_args).and_return(true)
- allow(Feature).to receive(:enabled?).with(feature_name.to_s, actor, any_args).and_return(true)
+ # Control a state of feature flag
+ if actor == true || actor.nil? || actor.respond_to?(:flipper_id)
+ feature.enable(actor)
+ elsif actor == false
+ feature.disable
else
- raise ArgumentError, "#stub_feature_flags accepts only `nil`, `true`, `false`, `ActiveRecord::Base` or `Symbol` as actors"
+ raise ArgumentError, "#stub_feature_flags accepts only `nil`, `bool`, an object responding to `#flipper_id` or including `FeatureGate`."
end
end
end
end
+
+ def stub_feature_flag_gate(object)
+ return if object.nil?
+ return object if object.is_a?(FeatureGate)
+
+ StubFeatureGate.new(object)
+ end
end
diff --git a/spec/support/helpers/stub_gitlab_calls.rb b/spec/support/helpers/stub_gitlab_calls.rb
index 120d432655b..4da8f760056 100644
--- a/spec/support/helpers/stub_gitlab_calls.rb
+++ b/spec/support/helpers/stub_gitlab_calls.rb
@@ -141,6 +141,12 @@ module StubGitlabCalls
.to_return(status: 200, body: "", headers: {})
end
+ def stub_webide_config_file(content, sha: anything)
+ allow_any_instance_of(Repository)
+ .to receive(:blob_data_at).with(sha, '.gitlab/.gitlab-webide.yml')
+ .and_return(content)
+ end
+
def project_hash_array
f = File.read(Rails.root.join('spec/support/gitlab_stubs/projects.json'))
Gitlab::Json.parse(f)
diff --git a/spec/support/helpers/trigger_helpers.rb b/spec/support/helpers/trigger_helpers.rb
new file mode 100644
index 00000000000..fa4f499b900
--- /dev/null
+++ b/spec/support/helpers/trigger_helpers.rb
@@ -0,0 +1,65 @@
+# 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 eq("execute procedure #{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/helpers/usage_data_helpers.rb b/spec/support/helpers/usage_data_helpers.rb
index 382e4f6a1a4..f6c415a75bc 100644
--- a/spec/support/helpers/usage_data_helpers.rb
+++ b/spec/support/helpers/usage_data_helpers.rb
@@ -78,6 +78,7 @@ module UsageDataHelpers
labels
lfs_objects
merge_requests
+ merge_requests_users
milestone_lists
milestones
notes
@@ -117,12 +118,18 @@ module UsageDataHelpers
projects_with_expiration_policy_enabled_with_cadence_set_to_14d
projects_with_expiration_policy_enabled_with_cadence_set_to_1month
projects_with_expiration_policy_enabled_with_cadence_set_to_3month
+ projects_with_terraform_reports
+ projects_with_terraform_states
pages_domains
protected_branches
releases
remote_mirrors
snippets
+ personal_snippets
+ project_snippets
suggestions
+ terraform_reports
+ terraform_states
todos
uploads
web_hooks
@@ -157,6 +164,11 @@ module UsageDataHelpers
object_store
).freeze
+ def stub_usage_data_connections
+ allow(ActiveRecord::Base.connection).to receive(:transaction_open?).and_return(false)
+ allow(Gitlab::Prometheus::Internal).to receive(:prometheus_enabled?).and_return(false)
+ end
+
def stub_object_store_settings
allow(Settings).to receive(:[]).with('artifacts')
.and_return(
@@ -209,4 +221,16 @@ module UsageDataHelpers
'proxy_download' => false } }
)
end
+
+ def expect_prometheus_api_to(*receive_matchers)
+ expect_next_instance_of(Gitlab::PrometheusClient) do |client|
+ receive_matchers.each { |m| expect(client).to m }
+ end
+ end
+
+ def allow_prometheus_queries
+ allow_next_instance_of(Gitlab::PrometheusClient) do |client|
+ allow(client).to receive(:aggregate).and_return({})
+ end
+ end
end
diff --git a/spec/support/helpers/wiki_helpers.rb b/spec/support/helpers/wiki_helpers.rb
index e6818ff8f0c..ae0d53d1297 100644
--- a/spec/support/helpers/wiki_helpers.rb
+++ b/spec/support/helpers/wiki_helpers.rb
@@ -8,14 +8,14 @@ module WikiHelpers
find('.svg-content .js-lazy-loaded') if example.nil? || example.metadata.key?(:js)
end
- def upload_file_to_wiki(project, user, file_name)
+ def upload_file_to_wiki(container, user, file_name)
opts = {
file_name: file_name,
file_content: File.read(expand_fixture_path(file_name))
}
::Wikis::CreateAttachmentService.new(
- container: project,
+ container: container,
current_user: user,
params: opts
).execute[:result][:file_path]
diff --git a/spec/support/import_export/common_util.rb b/spec/support/import_export/common_util.rb
index 0069ae81b76..c0c3559cca0 100644
--- a/spec/support/import_export/common_util.rb
+++ b/spec/support/import_export/common_util.rb
@@ -19,7 +19,7 @@ module ImportExport
end
def setup_reader(reader)
- if reader == :ndjson_reader && Feature.enabled?(:project_import_ndjson)
+ if reader == :ndjson_reader && Feature.enabled?(:project_import_ndjson, default_enabled: true)
allow_any_instance_of(Gitlab::ImportExport::JSON::LegacyReader::File).to receive(:exist?).and_return(false)
allow_any_instance_of(Gitlab::ImportExport::JSON::NdjsonReader).to receive(:exist?).and_return(true)
else
diff --git a/spec/support/let_it_be.rb b/spec/support/let_it_be.rb
new file mode 100644
index 00000000000..ade585faaec
--- /dev/null
+++ b/spec/support/let_it_be.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+TestProf::LetItBe.configure do |config|
+ config.alias_to :let_it_be_with_refind, refind: true
+end
+
+TestProf::LetItBe.configure do |config|
+ config.alias_to :let_it_be_with_reload, reload: true
+end
diff --git a/spec/support/matchers/exceed_query_limit.rb b/spec/support/matchers/exceed_query_limit.rb
index b9630b00038..cc0abfa0dd6 100644
--- a/spec/support/matchers/exceed_query_limit.rb
+++ b/spec/support/matchers/exceed_query_limit.rb
@@ -59,11 +59,15 @@ module ExceedQueryLimitHelpers
def verify_count(&block)
@subject_block = block
- actual_count > expected_count + threshold
+ actual_count > maximum
+ end
+
+ def maximum
+ expected_count + threshold
end
def failure_message
- threshold_message = threshold > 0 ? " (+#{@threshold})" : ''
+ threshold_message = threshold > 0 ? " (+#{threshold})" : ''
counts = "#{expected_count}#{threshold_message}"
"Expected a maximum of #{counts} queries, got #{actual_count}:\n\n#{log_message}"
end
@@ -73,6 +77,55 @@ module ExceedQueryLimitHelpers
end
end
+RSpec::Matchers.define :issue_fewer_queries_than do
+ supports_block_expectations
+
+ include ExceedQueryLimitHelpers
+
+ def control
+ block_arg
+ end
+
+ def control_recorder
+ @control_recorder ||= ActiveRecord::QueryRecorder.new(&control)
+ end
+
+ def expected_count
+ control_recorder.count
+ end
+
+ def verify_count(&block)
+ @subject_block = block
+
+ # These blocks need to be evaluated in an expected order, in case
+ # the events in expected affect the counts in actual
+ expected_count
+ actual_count
+
+ actual_count < expected_count
+ end
+
+ match do |block|
+ verify_count(&block)
+ end
+
+ def failure_message
+ <<~MSG
+ Expected to issue fewer than #{expected_count} queries, but got #{actual_count}
+
+ #{log_message}
+ MSG
+ end
+
+ failure_message_when_negated do |actual|
+ <<~MSG
+ Expected query count of #{actual_count} to be less than #{expected_count}
+
+ #{log_message}
+ MSG
+ end
+end
+
RSpec::Matchers.define :issue_same_number_of_queries_as do
supports_block_expectations
@@ -82,30 +135,66 @@ RSpec::Matchers.define :issue_same_number_of_queries_as do
block_arg
end
+ chain :or_fewer do
+ @or_fewer = true
+ end
+
+ chain :ignoring_cached_queries do
+ @skip_cached = true
+ end
+
def control_recorder
@control_recorder ||= ActiveRecord::QueryRecorder.new(&control)
end
def expected_count
- @expected_count ||= control_recorder.count
+ control_recorder.count
end
def verify_count(&block)
@subject_block = block
- (expected_count - actual_count).abs <= threshold
+ # These blocks need to be evaluated in an expected order, in case
+ # the events in expected affect the counts in actual
+ expected_count
+ actual_count
+
+ if @or_fewer
+ actual_count <= expected_count
+ else
+ (expected_count - actual_count).abs <= threshold
+ end
end
match do |block|
verify_count(&block)
end
+ def failure_message
+ <<~MSG
+ Expected #{expected_count_message} queries, but got #{actual_count}
+
+ #{log_message}
+ MSG
+ end
+
failure_message_when_negated do |actual|
- failure_message
+ <<~MSG
+ Expected #{actual_count} not to equal #{expected_count_message}
+
+ #{log_message}
+ MSG
+ end
+
+ def expected_count_message
+ or_fewer_msg = "or fewer" if @or_fewer
+ threshold_msg = "(+/- #{threshold})" unless threshold.zero?
+
+ ["#{expected_count}", or_fewer_msg, threshold_msg].compact.join(' ')
end
def skip_cached
- false
+ @skip_cached || false
end
end
diff --git a/spec/support/matchers/graphql_matchers.rb b/spec/support/matchers/graphql_matchers.rb
index 3e2193a9069..7fa06e25405 100644
--- a/spec/support/matchers/graphql_matchers.rb
+++ b/spec/support/matchers/graphql_matchers.rb
@@ -82,12 +82,30 @@ RSpec::Matchers.define :have_graphql_mutation do |mutation_class|
end
end
+# note: connection arguments do not have to be named, they will be inferred.
RSpec::Matchers.define :have_graphql_arguments do |*expected|
include GraphqlHelpers
+ def expected_names(field)
+ @names ||= Array.wrap(expected).map { |name| GraphqlHelpers.fieldnamerize(name) }
+
+ if field.type.try(:ancestors)&.include?(GraphQL::Types::Relay::BaseConnection)
+ @names | %w(after before first last)
+ else
+ @names
+ end
+ end
+
match do |field|
- argument_names = expected.map { |name| GraphqlHelpers.fieldnamerize(name) }
- expect(field.arguments.keys).to contain_exactly(*argument_names)
+ names = expected_names(field)
+
+ expect(field.arguments.keys).to contain_exactly(*names)
+ end
+
+ failure_message do |field|
+ names = expected_names(field)
+
+ "expected that #{field.name} would have the following fields: #{names.inspect}, but it has #{field.arguments.keys.inspect}."
end
end
diff --git a/spec/support/rspec.rb b/spec/support/rspec.rb
index 1c9f9e5161e..7d011c5eb95 100644
--- a/spec/support/rspec.rb
+++ b/spec/support/rspec.rb
@@ -4,6 +4,7 @@ require_relative "helpers/stub_configuration"
require_relative "helpers/stub_metrics"
require_relative "helpers/stub_object_storage"
require_relative "helpers/stub_env"
+require_relative "helpers/expect_offense"
RSpec.configure do |config|
config.mock_with :rspec
@@ -13,4 +14,6 @@ RSpec.configure do |config|
config.include StubMetrics
config.include StubObjectStorage
config.include StubENV
+
+ config.include ExpectOffense, type: :rubocop
end
diff --git a/spec/support/shared_contexts/finders/merge_requests_finder_shared_contexts.rb b/spec/support/shared_contexts/finders/merge_requests_finder_shared_contexts.rb
index 617701abf27..2b8daa80ab4 100644
--- a/spec/support/shared_contexts/finders/merge_requests_finder_shared_contexts.rb
+++ b/spec/support/shared_contexts/finders/merge_requests_finder_shared_contexts.rb
@@ -45,11 +45,32 @@ RSpec.shared_context 'MergeRequestsFinder multiple projects with merge requests
allow_gitaly_n_plus_1 { create(:project, group: subgroup) }
end
- let!(:merge_request1) { create(:merge_request, assignees: [user], author: user, source_project: project2, target_project: project1, target_branch: 'merged-target') }
- let!(:merge_request2) { create(:merge_request, :conflict, assignees: [user], author: user, source_project: project2, target_project: project1, state: 'closed') }
- let!(:merge_request3) { create(:merge_request, :simple, author: user, assignees: [user2], source_project: project2, target_project: project2, state: 'locked', title: 'thing WIP thing') }
- let!(:merge_request4) { create(:merge_request, :simple, author: user, source_project: project3, target_project: project3, title: 'WIP thing') }
- let!(:merge_request5) { create(:merge_request, :simple, author: user, source_project: project4, target_project: project4, title: '[WIP]') }
+ let!(:merge_request1) do
+ create(:merge_request, assignees: [user], author: user,
+ source_project: project2, target_project: project1,
+ target_branch: 'merged-target')
+ end
+ let!(:merge_request2) do
+ create(:merge_request, :conflict, assignees: [user], author: user,
+ source_project: project2, target_project: project1,
+ state: 'closed')
+ end
+ let!(:merge_request3) do
+ create(:merge_request, :simple, author: user, assignees: [user2],
+ source_project: project2, target_project: project2,
+ state: 'locked',
+ title: 'thing WIP thing')
+ end
+ let!(:merge_request4) do
+ create(:merge_request, :simple, author: user,
+ source_project: project3, target_project: project3,
+ title: 'WIP thing')
+ end
+ let_it_be(:merge_request5) do
+ create(:merge_request, :simple, author: user,
+ source_project: project4, target_project: project4,
+ title: '[WIP]')
+ end
before do
project1.add_maintainer(user)
diff --git a/spec/support/shared_contexts/navbar_structure_context.rb b/spec/support/shared_contexts/navbar_structure_context.rb
index fe3c32ec0c5..79b5ff44d4f 100644
--- a/spec/support/shared_contexts/navbar_structure_context.rb
+++ b/spec/support/shared_contexts/navbar_structure_context.rb
@@ -80,10 +80,13 @@ RSpec.shared_context 'project navbar structure' do
nav_sub_items: []
},
{
+ nav_item: _('Members'),
+ nav_sub_items: []
+ },
+ {
nav_item: _('Settings'),
nav_sub_items: [
_('General'),
- _('Members'),
_('Integrations'),
_('Webhooks'),
_('Access Tokens'),
diff --git a/spec/support/shared_contexts/project_service_shared_context.rb b/spec/support/shared_contexts/project_service_shared_context.rb
index 21d67ea71a8..5b0dd26bd7b 100644
--- a/spec/support/shared_contexts/project_service_shared_context.rb
+++ b/spec/support/shared_contexts/project_service_shared_context.rb
@@ -5,7 +5,6 @@ shared_context 'project service activation' do
let(:user) { create(:user) }
before do
- stub_feature_flags(integration_form_refactor: false)
project.add_maintainer(user)
sign_in(user)
end
diff --git a/spec/support/shared_contexts/requests/api/graphql/jira_import/jira_projects_context.rb b/spec/support/shared_contexts/requests/api/graphql/jira_import/jira_projects_context.rb
new file mode 100644
index 00000000000..f0722beb3ed
--- /dev/null
+++ b/spec/support/shared_contexts/requests/api/graphql/jira_import/jira_projects_context.rb
@@ -0,0 +1,104 @@
+# frozen_string_literal: true
+
+shared_context 'jira projects request context' do
+ let(:url) { 'https://jira.example.com' }
+ let(:username) { 'jira-username' }
+ let(:password) { 'jira-password' }
+ let!(:jira_service) do
+ create(:jira_service,
+ project: project,
+ url: url,
+ username: username,
+ password: password
+ )
+ end
+
+ let_it_be(:jira_projects_json) do
+ '{
+ "self": "https://your-domain.atlassian.net/rest/api/2/project/search?startAt=0&maxResults=2",
+ "nextPage": "https://your-domain.atlassian.net/rest/api/2/project/search?startAt=2&maxResults=2",
+ "maxResults": 2,
+ "startAt": 0,
+ "total": 7,
+ "isLast": false,
+ "values": [
+ {
+ "self": "https://your-domain.atlassian.net/rest/api/2/project/EX",
+ "id": "10000",
+ "key": "EX",
+ "name": "Example",
+ "avatarUrls": {
+ "48x48": "https://your-domain.atlassian.net/secure/projectavatar?size=large&pid=10000",
+ "24x24": "https://your-domain.atlassian.net/secure/projectavatar?size=small&pid=10000",
+ "16x16": "https://your-domain.atlassian.net/secure/projectavatar?size=xsmall&pid=10000",
+ "32x32": "https://your-domain.atlassian.net/secure/projectavatar?size=medium&pid=10000"
+ },
+ "projectCategory": {
+ "self": "https://your-domain.atlassian.net/rest/api/2/projectCategory/10000",
+ "id": "10000",
+ "name": "FIRST",
+ "description": "First Project Category"
+ },
+ "simplified": false,
+ "style": "classic",
+ "insight": {
+ "totalIssueCount": 100,
+ "lastIssueUpdateTime": "2020-03-31T05:45:24.792+0000"
+ }
+ },
+ {
+ "self": "https://your-domain.atlassian.net/rest/api/2/project/ABC",
+ "id": "10001",
+ "key": "ABC",
+ "name": "Alphabetical",
+ "avatarUrls": {
+ "48x48": "https://your-domain.atlassian.net/secure/projectavatar?size=large&pid=10001",
+ "24x24": "https://your-domain.atlassian.net/secure/projectavatar?size=small&pid=10001",
+ "16x16": "https://your-domain.atlassian.net/secure/projectavatar?size=xsmall&pid=10001",
+ "32x32": "https://your-domain.atlassian.net/secure/projectavatar?size=medium&pid=10001"
+ },
+ "projectCategory": {
+ "self": "https://your-domain.atlassian.net/rest/api/2/projectCategory/10000",
+ "id": "10000",
+ "name": "FIRST",
+ "description": "First Project Category"
+ },
+ "simplified": false,
+ "style": "classic",
+ "insight": {
+ "totalIssueCount": 100,
+ "lastIssueUpdateTime": "2020-03-31T05:45:24.792+0000"
+ }
+ }
+ ]
+ }'
+ end
+
+ let_it_be(:empty_jira_projects_json) do
+ '{
+ "self": "https://your-domain.atlassian.net/rest/api/2/project/search?startAt=0&maxResults=2",
+ "nextPage": "https://your-domain.atlassian.net/rest/api/2/project/search?startAt=2&maxResults=2",
+ "maxResults": 2,
+ "startAt": 0,
+ "total": 7,
+ "isLast": false,
+ "values": []
+ }'
+ end
+
+ let(:test_url) { "#{url}/rest/api/2/project/search?maxResults=50&query=&startAt=0" }
+ let(:start_at_20_url) { "#{url}/rest/api/2/project/search?maxResults=50&query=&startAt=20" }
+ let(:start_at_1_url) { "#{url}/rest/api/2/project/search?maxResults=50&query=&startAt=1" }
+ let(:max_results_1_url) { "#{url}/rest/api/2/project/search?maxResults=1&query=&startAt=0" }
+
+ before do
+ WebMock.stub_request(:get, test_url).with(basic_auth: [username, password])
+ .to_return(body: jira_projects_json, headers: { "Content-Type": "application/json" })
+ WebMock.stub_request(:get, start_at_20_url).with(basic_auth: [username, password])
+ .to_return(body: empty_jira_projects_json, headers: { "Content-Type": "application/json" })
+ WebMock.stub_request(:get, start_at_1_url).with(basic_auth: [username, password])
+ .to_return(body: jira_projects_json, headers: { "Content-Type": "application/json" })
+ WebMock.stub_request(:get, max_results_1_url).with(basic_auth: [username, password])
+ .to_return(body: jira_projects_json, headers: { "Content-Type": "application/json" })
+ end
+end
diff --git a/spec/support/shared_contexts/spam_constants.rb b/spec/support/shared_contexts/spam_constants.rb
index b6e92ea3050..32371f4b92f 100644
--- a/spec/support/shared_contexts/spam_constants.rb
+++ b/spec/support/shared_contexts/spam_constants.rb
@@ -1,7 +1,10 @@
# frozen_string_literal: true
shared_context 'includes Spam constants' do
- REQUIRE_RECAPTCHA = Spam::SpamConstants::REQUIRE_RECAPTCHA
- DISALLOW = Spam::SpamConstants::DISALLOW
- ALLOW = Spam::SpamConstants::ALLOW
+ before do
+ stub_const('CONDITIONAL_ALLOW', Spam::SpamConstants::CONDITIONAL_ALLOW)
+ stub_const('DISALLOW', Spam::SpamConstants::DISALLOW)
+ stub_const('ALLOW', Spam::SpamConstants::ALLOW)
+ stub_const('BLOCK_USER', Spam::SpamConstants::BLOCK_USER)
+ end
end
diff --git a/spec/support/shared_examples/controllers/import_controller_new_import_ui_shared_examples.rb b/spec/support/shared_examples/controllers/import_controller_new_import_ui_shared_examples.rb
new file mode 100644
index 00000000000..88ad1f6cde2
--- /dev/null
+++ b/spec/support/shared_examples/controllers/import_controller_new_import_ui_shared_examples.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'import controller with new_import_ui feature flag' do
+ include ImportSpecHelper
+
+ context 'with new_import_ui feature flag enabled' do
+ let(:group) { create(:group) }
+
+ before do
+ stub_feature_flags(new_import_ui: true)
+ group.add_owner(user)
+ end
+
+ it "returns variables for json request" do
+ project = create(:project, import_type: provider_name, creator_id: user.id)
+ stub_client(client_repos_field => [repo])
+
+ get :status, format: :json
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response.dig("imported_projects", 0, "id")).to eq(project.id)
+ expect(json_response.dig("provider_repos", 0, "id")).to eq(repo_id)
+ expect(json_response.dig("namespaces", 0, "id")).to eq(group.id)
+ end
+
+ it "does not show already added project" do
+ project = create(:project, import_type: provider_name, namespace: user.namespace, import_status: :finished, import_source: import_source)
+ stub_client(client_repos_field => [repo])
+
+ get :status, format: :json
+
+ expect(json_response.dig("imported_projects", 0, "id")).to eq(project.id)
+ expect(json_response.dig("provider_repos")).to eq([])
+ end
+ end
+end
diff --git a/spec/support/shared_examples/controllers/issuables_list_metadata_shared_examples.rb b/spec/support/shared_examples/controllers/issuables_list_metadata_shared_examples.rb
index 2dbaea57c44..62a1a07b6c1 100644
--- a/spec/support/shared_examples/controllers/issuables_list_metadata_shared_examples.rb
+++ b/spec/support/shared_examples/controllers/issuables_list_metadata_shared_examples.rb
@@ -34,7 +34,7 @@ RSpec.shared_examples 'issuables list meta-data' do |issuable_type, action = nil
aggregate_failures do
expect(meta_data.keys).to match_array(issuables.map(&:id))
- expect(meta_data.values).to all(be_kind_of(Issuable::IssuableMeta))
+ expect(meta_data.values).to all(be_kind_of(Gitlab::IssuableMetadata::IssuableMeta))
end
end
diff --git a/spec/support/shared_examples/controllers/milestone_tabs_shared_examples.rb b/spec/support/shared_examples/controllers/milestone_tabs_shared_examples.rb
index d9656824452..925c45005f0 100644
--- a/spec/support/shared_examples/controllers/milestone_tabs_shared_examples.rb
+++ b/spec/support/shared_examples/controllers/milestone_tabs_shared_examples.rb
@@ -2,15 +2,7 @@
RSpec.shared_examples 'milestone tabs' do
def go(path, extra_params = {})
- params =
- case milestone
- when DashboardMilestone
- { id: milestone.safe_title, title: milestone.title }
- when GroupMilestone
- { group_id: group.to_param, id: milestone.safe_title, title: milestone.title }
- else
- { namespace_id: project.namespace.to_param, project_id: project, id: milestone.iid }
- end
+ params = { namespace_id: project.namespace.to_param, project_id: project, id: milestone.iid }
get path, params: params.merge(extra_params)
end
diff --git a/spec/support/shared_examples/controllers/namespace_storage_limit_alert_shared_examples.rb b/spec/support/shared_examples/controllers/namespace_storage_limit_alert_shared_examples.rb
new file mode 100644
index 00000000000..7885eb6c1f8
--- /dev/null
+++ b/spec/support/shared_examples/controllers/namespace_storage_limit_alert_shared_examples.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'namespace storage limit alert' do
+ let(:alert_level) { :info }
+
+ before do
+ allow_next_instance_of(Namespaces::CheckStorageSizeService, namespace, user) do |check_storage_size_service|
+ expect(check_storage_size_service).to receive(:execute).and_return(
+ ServiceResponse.success(
+ payload: {
+ alert_level: alert_level,
+ usage_message: "Usage",
+ explanation_message: "Explanation",
+ root_namespace: namespace
+ }
+ )
+ )
+ end
+
+ allow(controller).to receive(:current_user).and_return(user)
+ end
+
+ render_views
+
+ it 'does render' do
+ subject
+
+ expect(response.body).to match(/Explanation/)
+ expect(response.body).to have_css('.js-namespace-storage-alert-dismiss')
+ end
+
+ context 'when alert_level is error' do
+ let(:alert_level) { :error }
+
+ it 'does not render a dismiss button' do
+ subject
+
+ expect(response.body).not_to have_css('.js-namespace-storage-alert-dismiss')
+ end
+ end
+
+ context 'when cookie is set' do
+ before do
+ cookies["hide_storage_limit_alert_#{namespace.id}_info"] = 'true'
+ end
+
+ it 'does not render alert' do
+ subject
+
+ expect(response.body).not_to match(/Explanation/)
+ end
+ end
+end
diff --git a/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb b/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb
new file mode 100644
index 00000000000..c128bbe5e02
--- /dev/null
+++ b/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb
@@ -0,0 +1,302 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'wiki controller actions' do
+ let(:container) { raise NotImplementedError }
+ let(:routing_params) { raise NotImplementedError }
+
+ let_it_be(:user) { create(:user) }
+ let(:wiki) { Wiki.for_container(container, user) }
+ let(:wiki_title) { 'page title test' }
+
+ before do
+ create(:wiki_page, wiki: wiki, title: wiki_title, content: 'hello world')
+
+ sign_in(user)
+ end
+
+ describe 'GET #new' do
+ subject { get :new, params: routing_params }
+
+ it 'redirects to #show and appends a `random_title` param' do
+ subject
+
+ expect(response).to be_redirect
+ expect(response.redirect_url).to match(%r{
+ #{Regexp.quote(wiki.wiki_base_path)} # wiki base path
+ /[-\h]{36} # page slug
+ \?random_title=true\Z # random_title param
+ }x)
+ end
+
+ context 'when the wiki repository cannot be created' do
+ before do
+ expect(Wiki).to receive(:for_container).and_return(wiki)
+ expect(wiki).to receive(:wiki) { raise Wiki::CouldNotCreateWikiError }
+ end
+
+ it 'redirects to the wiki container and displays an error message' do
+ subject
+
+ expect(response).to redirect_to(container)
+ expect(flash[:notice]).to eq('Could not create Wiki Repository at this time. Please try again later.')
+ end
+ end
+ end
+
+ describe 'GET #pages' do
+ before do
+ get :pages, params: routing_params.merge(id: wiki_title)
+ end
+
+ it 'assigns the page collections' do
+ expect(assigns(:wiki_pages)).to contain_exactly(an_instance_of(WikiPage))
+ expect(assigns(:wiki_entries)).to contain_exactly(an_instance_of(WikiPage))
+ end
+
+ it 'does not load the page content' do
+ expect(assigns(:page)).to be_nil
+ end
+
+ it 'does not load the sidebar' do
+ expect(assigns(:sidebar_wiki_entries)).to be_nil
+ expect(assigns(:sidebar_limited)).to be_nil
+ end
+ end
+
+ describe 'GET #history' do
+ before do
+ allow(controller)
+ .to receive(:can?)
+ .with(any_args)
+ .and_call_original
+
+ # The :create_wiki permission is irrelevant to reading history.
+ expect(controller)
+ .not_to receive(:can?)
+ .with(anything, :create_wiki, any_args)
+
+ allow(controller)
+ .to receive(:can?)
+ .with(anything, :read_wiki, any_args)
+ .and_return(allow_read_wiki)
+ end
+
+ shared_examples 'fetching history' do |expected_status|
+ before do
+ get :history, params: routing_params.merge(id: wiki_title)
+ end
+
+ it "returns status #{expected_status}" do
+ expect(response).to have_gitlab_http_status(expected_status)
+ end
+ end
+
+ it_behaves_like 'fetching history', :ok do
+ let(:allow_read_wiki) { true }
+
+ it 'assigns @page_versions' do
+ expect(assigns(:page_versions)).to be_present
+ end
+ end
+
+ it_behaves_like 'fetching history', :not_found do
+ let(:allow_read_wiki) { false }
+ end
+ end
+
+ describe 'GET #show' do
+ render_views
+
+ let(:random_title) { nil }
+
+ subject { get :show, params: routing_params.merge(id: id, random_title: random_title) }
+
+ context 'when page exists' do
+ let(:id) { wiki_title }
+
+ it 'renders the page' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(assigns(:page).title).to eq(wiki_title)
+ expect(assigns(:sidebar_wiki_entries)).to contain_exactly(an_instance_of(WikiPage))
+ expect(assigns(:sidebar_limited)).to be(false)
+ end
+
+ context 'when page content encoding is invalid' do
+ it 'sets flash error' do
+ allow(controller).to receive(:valid_encoding?).and_return(false)
+
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(flash[:notice]).to eq(_('The content of this page is not encoded in UTF-8. Edits can only be made via the Git repository.'))
+ end
+ end
+ end
+
+ context 'when the page does not exist' do
+ let(:id) { 'does not exist' }
+
+ before do
+ subject
+ end
+
+ it 'builds a new wiki page with the id as the title' do
+ expect(assigns(:page).title).to eq(id)
+ end
+
+ context 'when a random_title param is present' do
+ let(:random_title) { true }
+
+ it 'builds a new wiki page with no title' do
+ expect(assigns(:page).title).to be_empty
+ end
+ end
+ end
+
+ context 'when page is a file' do
+ include WikiHelpers
+
+ let(:id) { upload_file_to_wiki(container, user, file_name) }
+
+ context 'when file is an image' do
+ let(:file_name) { 'dk.png' }
+
+ it 'delivers the image' do
+ subject
+
+ expect(response.headers['Content-Disposition']).to match(/^inline/)
+ expect(response.headers[Gitlab::Workhorse::DETECT_HEADER]).to eq "true"
+ end
+
+ context 'when file is a svg' do
+ let(:file_name) { 'unsanitized.svg' }
+
+ it 'delivers the image' do
+ subject
+
+ expect(response.headers['Content-Disposition']).to match(/^inline/)
+ expect(response.headers[Gitlab::Workhorse::DETECT_HEADER]).to eq "true"
+ end
+ end
+
+ it_behaves_like 'project cache control headers' do
+ let(:project) { container }
+ end
+ end
+
+ context 'when file is a pdf' do
+ let(:file_name) { 'git-cheat-sheet.pdf' }
+
+ it 'sets the content type to sets the content response headers' do
+ subject
+
+ expect(response.headers['Content-Disposition']).to match(/^inline/)
+ expect(response.headers[Gitlab::Workhorse::DETECT_HEADER]).to eq "true"
+ end
+
+ it_behaves_like 'project cache control headers' do
+ let(:project) { container }
+ end
+ end
+ end
+ end
+
+ describe 'POST #preview_markdown' do
+ it 'renders json in a correct format' do
+ post :preview_markdown, params: routing_params.merge(id: 'page/path', text: '*Markdown* text')
+
+ expect(json_response.keys).to match_array(%w(body references))
+ end
+ end
+
+ describe 'GET #edit' do
+ subject { get(:edit, params: routing_params.merge(id: wiki_title)) }
+
+ context 'when page content encoding is invalid' do
+ it 'redirects to show' do
+ allow(controller).to receive(:valid_encoding?).and_return(false)
+
+ subject
+
+ expect(response).to redirect_to_wiki(wiki, wiki.list_pages.first)
+ end
+ end
+
+ context 'when the page has nil content' do
+ let(:page) { create(:wiki_page) }
+
+ it 'redirects to show' do
+ allow(page).to receive(:content).and_return(nil)
+ allow(controller).to receive(:page).and_return(page)
+
+ subject
+
+ expect(response).to redirect_to_wiki(wiki, page)
+ end
+ end
+
+ context 'when page content encoding is valid' do
+ render_views
+
+ it 'shows the edit page' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.body).to include(s_('Wiki|Edit Page'))
+ end
+ end
+ end
+
+ describe 'PATCH #update' do
+ let(:new_title) { 'New title' }
+ let(:new_content) { 'New content' }
+
+ subject do
+ patch(:update,
+ params: routing_params.merge(
+ id: wiki_title,
+ wiki: { title: new_title, content: new_content }
+ ))
+ end
+
+ context 'when page content encoding is invalid' do
+ it 'redirects to show' do
+ allow(controller).to receive(:valid_encoding?).and_return(false)
+
+ subject
+ expect(response).to redirect_to_wiki(wiki, wiki.list_pages.first)
+ end
+ end
+
+ context 'when page content encoding is valid' do
+ render_views
+
+ it 'updates the page' do
+ subject
+
+ wiki_page = wiki.list_pages(load_content: true).first
+
+ expect(wiki_page.title).to eq new_title
+ expect(wiki_page.content).to eq new_content
+ end
+ end
+
+ context 'when user does not have edit permissions' do
+ before do
+ sign_out(:user)
+ end
+
+ it 'renders the empty state' do
+ subject
+
+ expect(response).to render_template('shared/wikis/empty')
+ end
+ end
+ end
+
+ def redirect_to_wiki(wiki, page)
+ redirect_to(controller.wiki_page_path(wiki, page))
+ end
+end
diff --git a/spec/support/shared_examples/features/comments_on_merge_request_files_shared_examples.rb b/spec/support/shared_examples/features/comments_on_merge_request_files_shared_examples.rb
index fb3b17d05ee..e0a032b1a43 100644
--- a/spec/support/shared_examples/features/comments_on_merge_request_files_shared_examples.rb
+++ b/spec/support/shared_examples/features/comments_on_merge_request_files_shared_examples.rb
@@ -6,13 +6,14 @@ RSpec.shared_examples 'comment on merge request file' do
page.within('.js-discussion-note-form') do
fill_in(:note_note, with: 'Line is wrong')
- click_button('Comment')
+ find('.js-comment-button').click
end
wait_for_requests
page.within('.notes_holder') do
expect(page).to have_content('Line is wrong')
+ expect(page).not_to have_content('Comment on lines')
end
visit(merge_request_path(merge_request))
diff --git a/spec/support/shared_examples/features/discussion_comments_shared_example.rb b/spec/support/shared_examples/features/discussion_comments_shared_example.rb
index 81433d124c9..6007798c290 100644
--- a/spec/support/shared_examples/features/discussion_comments_shared_example.rb
+++ b/spec/support/shared_examples/features/discussion_comments_shared_example.rb
@@ -15,7 +15,7 @@ RSpec.shared_examples 'thread comments' do |resource_name|
find("#{form_selector} .note-textarea").send_keys(comment)
- click_button 'Comment'
+ find('.js-comment-button').click
expect(page).to have_content(comment)
@@ -30,6 +30,8 @@ RSpec.shared_examples 'thread comments' do |resource_name|
click_button 'Comment & close issue'
+ wait_for_all_requests
+
expect(page).to have_content(comment)
expect(page).to have_content "@#{user.username} closed"
@@ -144,7 +146,7 @@ RSpec.shared_examples 'thread comments' do |resource_name|
find("#{comments_selector} .js-vue-discussion-reply").click
find("#{comments_selector} .note-textarea").send_keys(text)
- click_button "Comment"
+ find("#{comments_selector} .js-comment-button").click
wait_for_requests
end
diff --git a/spec/support/shared_examples/graphql/container_expiration_policy_shared_examples.rb b/spec/support/shared_examples/graphql/container_expiration_policy_shared_examples.rb
new file mode 100644
index 00000000000..9914de7c847
--- /dev/null
+++ b/spec/support/shared_examples/graphql/container_expiration_policy_shared_examples.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'exposing container expiration policy option' do |model_option|
+ it 'exposes all options' do
+ expect(described_class.values.keys).to contain_exactly(*expected_values)
+ end
+
+ it 'uses all possible options from model' do
+ all_options = ContainerExpirationPolicy.public_send("#{model_option}_options").keys
+ expect(described_class::OPTIONS_MAPPING.keys).to contain_exactly(*all_options)
+ end
+end
diff --git a/spec/support/shared_examples/graphql/label_fields.rb b/spec/support/shared_examples/graphql/label_fields.rb
new file mode 100644
index 00000000000..b1bfb395bc6
--- /dev/null
+++ b/spec/support/shared_examples/graphql/label_fields.rb
@@ -0,0 +1,124 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'a GraphQL type with labels' do
+ it 'has label fields' do
+ expected_fields = %w[label labels]
+
+ expect(described_class).to include_graphql_fields(*expected_fields)
+ end
+
+ describe 'label field' do
+ subject { described_class.fields['label'] }
+
+ it { is_expected.to have_graphql_type(Types::LabelType) }
+ it { is_expected.to have_graphql_arguments(:title) }
+ end
+
+ describe 'labels field' do
+ subject { described_class.fields['labels'] }
+
+ it { is_expected.to have_graphql_type(Types::LabelType.connection_type) }
+ it { is_expected.to have_graphql_arguments(:search_term) }
+ end
+end
+
+RSpec.shared_examples 'querying a GraphQL type with labels' do
+ let_it_be(:current_user) { create(:user) }
+
+ let_it_be(:label_a) { create(label_factory, :described, **label_attrs) }
+ let_it_be(:label_b) { create(label_factory, :described, **label_attrs) }
+ let_it_be(:label_c) { create(label_factory, :described, :scoped, prefix: 'matching', **label_attrs) }
+ let_it_be(:label_d) { create(label_factory, :described, :scoped, prefix: 'matching', **label_attrs) }
+
+ let(:label_title) { label_b.title }
+
+ let(:label_params) { { title: label_title } }
+ let(:labels_params) { nil }
+
+ let(:label_response) { graphql_data.dig(*path_prefix, 'label') }
+ let(:labels_response) { graphql_data.dig(*path_prefix, 'labels', 'nodes') }
+
+ let(:query) do
+ make_query(
+ [
+ query_graphql_field(:label, label_params, all_graphql_fields_for(Label)),
+ query_graphql_field(:labels, labels_params, [
+ query_graphql_field(:nodes, nil, all_graphql_fields_for(Label))
+ ])
+ ]
+ )
+ end
+
+ context 'running a query' do
+ before do
+ run_query(query)
+ end
+
+ context 'minimum required arguments' do
+ it 'returns the label information' do
+ expect(label_response).to include(
+ 'title' => label_title,
+ 'description' => label_b.description
+ )
+ end
+
+ it 'returns the labels information' do
+ expect(labels_response.pluck('title')).to contain_exactly(
+ label_a.title,
+ label_b.title,
+ label_c.title,
+ label_d.title
+ )
+ end
+ end
+
+ context 'with a search param' do
+ let(:labels_params) { { search_term: 'matching' } }
+
+ it 'finds the matching labels' do
+ expect(labels_response.pluck('title')).to contain_exactly(
+ label_c.title,
+ label_d.title
+ )
+ end
+ end
+
+ context 'the label does not exist' do
+ let(:label_title) { 'not-a-label' }
+
+ it 'returns nil' do
+ expect(label_response).to be_nil
+ end
+ end
+ end
+
+ describe 'performance' do
+ def query_for(*labels)
+ selections = labels.map do |label|
+ %Q[#{label.title.gsub(/:+/, '_')}: label(title: "#{label.title}") { description }]
+ end
+
+ make_query(selections)
+ end
+
+ before do
+ run_query(query_for(label_a))
+ end
+
+ it 'batches queries for labels by title' do
+ pending('See: https://gitlab.com/gitlab-org/gitlab/-/issues/217767')
+
+ multi_selection = query_for(label_b, label_c)
+ single_selection = query_for(label_d)
+
+ expect { run_query(multi_selection) }
+ .to issue_same_number_of_queries_as { run_query(single_selection) }
+ end
+ end
+
+ # Run a known good query with the current user
+ def run_query(query)
+ post_graphql(query, current_user: current_user)
+ expect(graphql_errors).not_to be_present
+ end
+end
diff --git a/spec/support/shared_examples/graphql/members_shared_examples.rb b/spec/support/shared_examples/graphql/members_shared_examples.rb
new file mode 100644
index 00000000000..a58e716efd2
--- /dev/null
+++ b/spec/support/shared_examples/graphql/members_shared_examples.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'a working membership object query' do |model_option|
+ let_it_be(:member_source) { member.source }
+ let_it_be(:member_source_type) { member_source.class.to_s.downcase }
+
+ it 'contains edge to expected project' do
+ expect(
+ graphql_data.dig('user', "#{member_source_type}Memberships", 'nodes', 0, member_source_type, 'id')
+ ).to eq(member.send(member_source_type).to_global_id.to_s)
+ end
+
+ it 'contains correct access level' do
+ expect(
+ graphql_data.dig('user', "#{member_source_type}Memberships", 'nodes', 0, 'accessLevel', 'integerValue')
+ ).to eq(30)
+
+ expect(
+ graphql_data.dig('user', "#{member_source_type}Memberships", 'nodes', 0, 'accessLevel', 'stringValue')
+ ).to eq('DEVELOPER')
+ end
+end
diff --git a/spec/support/shared_examples/graphql/resolves_issuable_shared_examples.rb b/spec/support/shared_examples/graphql/resolves_issuable_shared_examples.rb
index b56181371c3..58cd3d21f66 100644
--- a/spec/support/shared_examples/graphql/resolves_issuable_shared_examples.rb
+++ b/spec/support/shared_examples/graphql/resolves_issuable_shared_examples.rb
@@ -22,26 +22,16 @@ RSpec.shared_examples 'resolving an issuable in GraphQL' do |type|
.with(full_path: parent.full_path)
.and_return(resolved_parent)
- expect(resolver_class).to receive(:new)
+ expect(resolver_class.single).to receive(:new)
.with(object: resolved_parent, context: context, field: nil)
.and_call_original
subject
end
- it 'uses correct Resolver to resolve issuable parent' do
- resolver_class = type == :epic ? 'Resolvers::GroupResolver' : 'Resolvers::ProjectResolver'
-
- expect(resolver_class.constantize).to receive(:new)
- .with(object: nil, context: context, field: nil)
- .and_call_original
-
- subject
- end
-
it 'returns nil if issuable is not found' do
result = mutation.resolve_issuable(type: type, parent_path: parent.full_path, iid: "100")
- result = type == :merge_request ? result.sync : result
+ result = result.respond_to?(:sync) ? result.sync : result
expect(result).to be_nil
end
diff --git a/spec/support/shared_examples/graphql/sorted_paginated_query_shared_examples.rb b/spec/support/shared_examples/graphql/sorted_paginated_query_shared_examples.rb
index fb7e24eecf2..2ef71d275a2 100644
--- a/spec/support/shared_examples/graphql/sorted_paginated_query_shared_examples.rb
+++ b/spec/support/shared_examples/graphql/sorted_paginated_query_shared_examples.rb
@@ -23,7 +23,7 @@
# graphql_query_for(
# 'project',
# { 'fullPath' => sort_project.full_path },
-# "issues(#{params}) { #{page_info} edges { node { iid weight } } }"
+# query_graphql_field('issues', params, "#{page_info} edges { node { id } }")
# )
# end
#
@@ -47,11 +47,13 @@ RSpec.shared_examples 'sorted paginated query' do
end
describe do
- let(:params) { "sort: #{sort_param}" }
- let(:start_cursor) { graphql_data_at(*data_path, :pageInfo, :startCursor) }
- let(:end_cursor) { graphql_data_at(*data_path, :pageInfo, :endCursor) }
- let(:sorted_edges) { graphql_data_at(*data_path, :edges) }
- let(:page_info) { "pageInfo { startCursor endCursor }" }
+ let(:sort_argument) { "sort: #{sort_param}" if sort_param.present? }
+ let(:first_argument) { "first: #{first_param}" if first_param.present? }
+ let(:params) { sort_argument }
+ let(:start_cursor) { graphql_data_at(*data_path, :pageInfo, :startCursor) }
+ let(:end_cursor) { graphql_data_at(*data_path, :pageInfo, :endCursor) }
+ let(:sorted_edges) { graphql_data_at(*data_path, :edges) }
+ let(:page_info) { "pageInfo { startCursor endCursor }" }
def pagination_query(params, page_info)
raise('pagination_query(params, page_info) must be defined in the test, see example in comment') unless defined?(super)
@@ -75,12 +77,12 @@ RSpec.shared_examples 'sorted paginated query' do
end
context 'when paginating' do
- let(:params) { "sort: #{sort_param}, first: #{first_param}" }
+ let(:params) { [sort_argument, first_argument].compact.join(',') }
it 'paginates correctly' do
expect(pagination_results_data(sorted_edges)).to eq expected_results.first(first_param)
- cursored_query = pagination_query("sort: #{sort_param}, after: \"#{end_cursor}\"", page_info)
+ cursored_query = pagination_query([sort_argument, "after: \"#{end_cursor}\""].compact.join(','), page_info)
post_graphql(cursored_query, current_user: current_user)
response_data = graphql_dig_at(Gitlab::Json.parse(response.body), :data, *data_path, :edges)
diff --git a/spec/support/shared_examples/integrations/test_examples.rb b/spec/support/shared_examples/integrations/test_examples.rb
new file mode 100644
index 00000000000..eb2e83ce5d1
--- /dev/null
+++ b/spec/support/shared_examples/integrations/test_examples.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'tests for integration with pipeline data' do
+ it 'tests the integration with pipeline data' do
+ create(:ci_empty_pipeline, project: project)
+ allow(Gitlab::DataBuilder::Pipeline).to receive(:build).and_return(sample_data)
+
+ expect(integration).to receive(:test).with(sample_data).and_return(success_result)
+ expect(subject).to eq(success_result)
+ end
+end
diff --git a/spec/support/shared_examples/lib/gitlab/gl_repository_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/gl_repository_shared_examples.rb
new file mode 100644
index 00000000000..97f4341340d
--- /dev/null
+++ b/spec/support/shared_examples/lib/gitlab/gl_repository_shared_examples.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'parsing gl_repository identifier' do
+ subject { described_class.new(identifier) }
+
+ it 'returns correct information' do
+ aggregate_failures do
+ expect(subject.repo_type).to eq(expected_type)
+ expect(subject.fetch_container!).to eq(expected_container)
+ end
+ end
+end
diff --git a/spec/support/shared_examples/lib/gitlab/import/stuck_import_job_workers_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/import/stuck_import_job_workers_shared_examples.rb
new file mode 100644
index 00000000000..06ea540706a
--- /dev/null
+++ b/spec/support/shared_examples/lib/gitlab/import/stuck_import_job_workers_shared_examples.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+shared_examples 'stuck import job detection' do
+ context 'when the job has completed' do
+ context 'when the import status was already updated' do
+ before do
+ allow(Gitlab::SidekiqStatus).to receive(:completed_jids) do
+ import_state.start
+ import_state.finish
+
+ [import_state.jid]
+ end
+ end
+
+ it 'does not mark the import as failed' do
+ worker.perform
+
+ expect(import_state.reload.status).to eq('finished')
+ end
+ end
+
+ context 'when the import status was not updated' do
+ before do
+ allow(Gitlab::SidekiqStatus).to receive(:completed_jids).and_return([import_state.jid])
+ end
+
+ it 'marks the import as failed' do
+ worker.perform
+
+ expect(import_state.reload.status).to eq('failed')
+ end
+ end
+ end
+
+ context 'when the job is still in Sidekiq' do
+ before do
+ allow(Gitlab::SidekiqStatus).to receive(:completed_jids).and_return([])
+ end
+
+ it 'does not mark the import as failed' do
+ expect { worker.perform }.not_to change { import_state.reload.status }
+ end
+ end
+end
diff --git a/spec/support/shared_examples/lib/gitlab/position_formatters_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/position_formatters_shared_examples.rb
index c9300aff3e6..326800e6dc2 100644
--- a/spec/support/shared_examples/lib/gitlab/position_formatters_shared_examples.rb
+++ b/spec/support/shared_examples/lib/gitlab/position_formatters_shared_examples.rb
@@ -32,7 +32,21 @@ RSpec.shared_examples "position formatter" do
subject { formatter.to_h }
- it { is_expected.to eq(formatter_hash) }
+ context 'when file_identifier_hash is disabled' do
+ before do
+ stub_feature_flags(file_identifier_hash: false)
+ end
+
+ it { is_expected.to eq(formatter_hash.except(:file_identifier_hash)) }
+ end
+
+ context 'when file_identifier_hash is enabled' do
+ before do
+ stub_feature_flags(file_identifier_hash: true)
+ end
+
+ it { is_expected.to eq(formatter_hash) }
+ end
end
describe '#==' do
diff --git a/spec/support/shared_examples/lib/gitlab/repo_type_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/repo_type_shared_examples.rb
index 69ae9339f10..4aeae788114 100644
--- a/spec/support/shared_examples/lib/gitlab/repo_type_shared_examples.rb
+++ b/spec/support/shared_examples/lib/gitlab/repo_type_shared_examples.rb
@@ -7,26 +7,6 @@ RSpec.shared_examples 'a repo type' do
it { is_expected.to eq(expected_identifier) }
end
- describe '#fetch_id' do
- it 'finds an id match in the identifier' do
- expect(described_class.fetch_id(expected_identifier)).to eq(expected_id)
- end
-
- it 'does not break on other identifiers' do
- expect(described_class.fetch_id('wiki-noid')).to eq(nil)
- end
- end
-
- describe '#fetch_container!' do
- it 'returns the container' do
- expect(described_class.fetch_container!(expected_identifier)).to eq expected_container
- end
-
- it 'raises an exception if the identifier is invalid' do
- expect { described_class.fetch_container!('project-noid') }.to raise_error ArgumentError
- end
- end
-
describe '#path_suffix' do
subject { described_class.path_suffix }
diff --git a/spec/support/shared_examples/models/application_setting_shared_examples.rb b/spec/support/shared_examples/models/application_setting_shared_examples.rb
index aed85a6630a..01513161d24 100644
--- a/spec/support/shared_examples/models/application_setting_shared_examples.rb
+++ b/spec/support/shared_examples/models/application_setting_shared_examples.rb
@@ -288,11 +288,37 @@ RSpec.shared_examples 'application settings examples' do
end
describe '#pick_repository_storage' do
- it 'uses Array#sample to pick a random storage' do
- array = double('array', sample: 'random')
- expect(setting).to receive(:repository_storages).and_return(array)
+ before do
+ allow(setting).to receive(:repository_storages_weighted).and_return({ 'default' => 20, 'backup' => 80 })
+ end
+
+ it 'chooses repository based on weight' do
+ picked_storages = { 'default' => 0.0, 'backup' => 0.0 }
+ 10_000.times { picked_storages[setting.pick_repository_storage] += 1 }
+
+ expect(((picked_storages['default'] / 10_000) * 100).round.to_i).to be_between(19, 21)
+ expect(((picked_storages['backup'] / 10_000) * 100).round.to_i).to be_between(79, 81)
+ end
+ end
+
+ describe '#normalized_repository_storage_weights' do
+ using RSpec::Parameterized::TableSyntax
- expect(setting.pick_repository_storage).to eq('random')
+ where(:storages, :normalized) do
+ { 'default' => 0, 'backup' => 100 } | { 'default' => 0.0, 'backup' => 1.0 }
+ { 'default' => 100, 'backup' => 100 } | { 'default' => 0.5, 'backup' => 0.5 }
+ { 'default' => 20, 'backup' => 80 } | { 'default' => 0.2, 'backup' => 0.8 }
+ { 'default' => 0, 'backup' => 0 } | { 'default' => 0.0, 'backup' => 0.0 }
+ end
+
+ with_them do
+ before do
+ allow(setting).to receive(:repository_storages_weighted).and_return(storages)
+ end
+
+ it 'normalizes storage weights' do
+ expect(setting.normalized_repository_storage_weights).to eq(normalized)
+ end
end
end
diff --git a/spec/support/shared_examples/models/cluster_application_helm_cert_shared_examples.rb b/spec/support/shared_examples/models/cluster_application_helm_cert_shared_examples.rb
index fa6b0c3afdd..239588d3b2f 100644
--- a/spec/support/shared_examples/models/cluster_application_helm_cert_shared_examples.rb
+++ b/spec/support/shared_examples/models/cluster_application_helm_cert_shared_examples.rb
@@ -54,7 +54,7 @@ RSpec.shared_examples 'cluster application helm specs' do |application_name|
context 'managed_apps_local_tiller feature flag is enabled' do
before do
- stub_feature_flags(managed_apps_local_tiller: true)
+ stub_feature_flags(managed_apps_local_tiller: application.cluster.clusterable)
end
it 'does not include cert files' do
diff --git a/spec/support/shared_examples/models/cluster_application_initial_status_shared_examples.rb b/spec/support/shared_examples/models/cluster_application_initial_status_shared_examples.rb
index 0b21e9a3aa7..7f0c60d4204 100644
--- a/spec/support/shared_examples/models/cluster_application_initial_status_shared_examples.rb
+++ b/spec/support/shared_examples/models/cluster_application_initial_status_shared_examples.rb
@@ -18,7 +18,7 @@ RSpec.shared_examples 'cluster application initial status specs' do
context 'local tiller feature flag is enabled' do
before do
- stub_feature_flags(managed_apps_local_tiller: true)
+ stub_feature_flags(managed_apps_local_tiller: cluster.clusterable)
end
it 'sets a default status' do
diff --git a/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb b/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb
index c2fd04d648b..0efa5e56199 100644
--- a/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb
+++ b/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb
@@ -66,7 +66,7 @@ RSpec.shared_examples 'cluster application status specs' do |application_name|
context 'managed_apps_local_tiller feature flag enabled' do
before do
- stub_feature_flags(managed_apps_local_tiller: true)
+ stub_feature_flags(managed_apps_local_tiller: subject.cluster.clusterable)
end
it 'does not update the helm version' do
@@ -197,12 +197,73 @@ RSpec.shared_examples 'cluster application status specs' do |application_name|
describe '#make_externally_installed' do
subject { create(application_name, :installing) }
+ let(:old_helm) { create(:clusters_applications_helm, version: '1.2.3') }
+
it 'is installed' do
subject.make_externally_installed
expect(subject).to be_installed
end
+ context 'local tiller flag enabled' do
+ before do
+ stub_feature_flags(managed_apps_local_tiller: true)
+ end
+
+ context 'helm record does not exist' do
+ subject { build(application_name, :installing, :no_helm_installed) }
+
+ it 'does not create a helm record' do
+ subject.make_externally_installed!
+
+ subject.cluster.reload
+ expect(subject.cluster.application_helm).to be_nil
+ end
+ end
+
+ context 'helm record exists' do
+ subject { build(application_name, :installing, cluster: old_helm.cluster) }
+
+ it 'does not update helm version' do
+ subject.make_externally_installed!
+
+ subject.cluster.application_helm.reload
+
+ expect(subject.cluster.application_helm.version).to eq('1.2.3')
+ end
+ end
+ end
+
+ context 'local tiller flag disabled' do
+ before do
+ stub_feature_flags(managed_apps_local_tiller: false)
+ end
+
+ context 'helm record does not exist' do
+ subject { build(application_name, :installing, :no_helm_installed) }
+
+ it 'creates a helm record' do
+ subject.make_externally_installed!
+
+ subject.cluster.reload
+ expect(subject.cluster.application_helm).to be_present
+ expect(subject.cluster.application_helm).to be_persisted
+ end
+ end
+
+ context 'helm record exists' do
+ subject { build(application_name, :installing, cluster: old_helm.cluster) }
+
+ it 'does not update helm version' do
+ subject.make_externally_installed!
+
+ subject.cluster.application_helm.reload
+
+ expect(subject.cluster.application_helm.version).to eq('1.2.3')
+ end
+ end
+ end
+
context 'application is updated' do
subject { create(application_name, :updated) }
diff --git a/spec/support/shared_examples/models/concerns/blob_replicator_strategy_shared_examples.rb b/spec/support/shared_examples/models/concerns/blob_replicator_strategy_shared_examples.rb
deleted file mode 100644
index 76339837351..00000000000
--- a/spec/support/shared_examples/models/concerns/blob_replicator_strategy_shared_examples.rb
+++ /dev/null
@@ -1,117 +0,0 @@
-# frozen_string_literal: true
-
-# Include these shared examples in specs of Replicators that include
-# BlobReplicatorStrategy.
-#
-# A let variable called model_record should be defined in the spec. It should be
-# a valid, unpersisted instance of the model class.
-#
-RSpec.shared_examples 'a blob replicator' do
- include EE::GeoHelpers
-
- let_it_be(:primary) { create(:geo_node, :primary) }
- let_it_be(:secondary) { create(:geo_node) }
-
- subject(:replicator) { model_record.replicator }
-
- before do
- stub_current_geo_node(primary)
- end
-
- describe '#handle_after_create_commit' do
- it 'creates a Geo::Event' do
- expect do
- replicator.handle_after_create_commit
- end.to change { ::Geo::Event.count }.by(1)
-
- expect(::Geo::Event.last.attributes).to include(
- "replicable_name" => replicator.replicable_name, "event_name" => "created", "payload" => { "model_record_id" => replicator.model_record.id })
- end
-
- it 'schedules the checksum calculation if needed' do
- expect(Geo::BlobVerificationPrimaryWorker).to receive(:perform_async)
- expect(replicator).to receive(:needs_checksum?).and_return(true)
-
- replicator.handle_after_create_commit
- end
-
- it 'does not schedule the checksum calculation if feature flag is disabled' do
- stub_feature_flags(geo_self_service_framework: false)
-
- expect(Geo::BlobVerificationPrimaryWorker).not_to receive(:perform_async)
- allow(replicator).to receive(:needs_checksum?).and_return(true)
-
- replicator.handle_after_create_commit
- end
- end
-
- describe '#calculate_checksum!' do
- it 'calculates the checksum' do
- model_record.save!
-
- replicator.calculate_checksum!
-
- expect(model_record.reload.verification_checksum).not_to be_nil
- expect(model_record.reload.verified_at).not_to be_nil
- end
-
- it 'saves the error message and increments retry counter' do
- model_record.save!
-
- allow(model_record).to receive(:calculate_checksum!) do
- raise StandardError.new('Failure to calculate checksum')
- end
-
- replicator.calculate_checksum!
-
- expect(model_record.reload.verification_failure).to eq 'Failure to calculate checksum'
- expect(model_record.verification_retry_count).to be 1
- end
- end
-
- describe '#consume_created_event' do
- it 'invokes Geo::BlobDownloadService' do
- service = double(:service)
-
- expect(service).to receive(:execute)
- expect(::Geo::BlobDownloadService).to receive(:new).with(replicator: replicator).and_return(service)
-
- replicator.consume_event_created
- end
- end
-
- describe '#carrierwave_uploader' do
- it 'is implemented' do
- expect do
- replicator.carrierwave_uploader
- end.not_to raise_error
- end
- end
-
- describe '#model' do
- let(:invoke_model) { replicator.class.model }
-
- it 'is implemented' do
- expect do
- invoke_model
- end.not_to raise_error
- end
-
- it 'is a Class' do
- expect(invoke_model).to be_a(Class)
- end
-
- # For convenience (and reliability), instead of asking developers to include shared examples on each model spec as well
- context 'replicable model' do
- it 'defines #replicator' do
- expect(model_record).to respond_to(:replicator)
- end
-
- it 'invokes replicator.handle_after_create_commit on create' do
- expect(replicator).to receive(:handle_after_create_commit)
-
- model_record.save!
- end
- end
- end
-end
diff --git a/spec/support/shared_examples/models/concerns/has_repository_shared_examples.rb b/spec/support/shared_examples/models/concerns/has_repository_shared_examples.rb
index 30c8c7d0fe5..f37ef3533c3 100644
--- a/spec/support/shared_examples/models/concerns/has_repository_shared_examples.rb
+++ b/spec/support/shared_examples/models/concerns/has_repository_shared_examples.rb
@@ -148,18 +148,18 @@ RSpec.shared_examples 'model with repository' do
expect(subject).to eq('picked')
end
- it 'picks from the latest available storage', :request_store do
+ it 'picks from the available storages based on weight', :request_store do
stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
Gitlab::CurrentSettings.expire_current_application_settings
Gitlab::CurrentSettings.current_application_settings
settings = ApplicationSetting.last
- settings.repository_storages = %w(picked)
+ settings.repository_storages_weighted = { 'picked' => 100, 'default' => 0 }
settings.save!
- expect(Gitlab::CurrentSettings.repository_storages).to eq(%w(default))
+ expect(Gitlab::CurrentSettings.repository_storages_weighted).to eq({ 'default' => 100 })
expect(subject).to eq('picked')
- expect(Gitlab::CurrentSettings.repository_storages).to eq(%w(picked))
+ expect(Gitlab::CurrentSettings.repository_storages_weighted).to eq({ 'default' => 0, 'picked' => 100 })
end
end
end
diff --git a/spec/support/shared_examples/models/concerns/limitable_shared_examples.rb b/spec/support/shared_examples/models/concerns/limitable_shared_examples.rb
index 4bcea36fd42..d21823661f8 100644
--- a/spec/support/shared_examples/models/concerns/limitable_shared_examples.rb
+++ b/spec/support/shared_examples/models/concerns/limitable_shared_examples.rb
@@ -26,7 +26,7 @@ RSpec.shared_examples 'includes Limitable concern' do
subject.dup.save
end
- it 'cannot create new models exceding the plan limits' do
+ it 'cannot create new models exceeding the plan limits' do
expect { subject.save }.not_to change { described_class.count }
expect(subject.errors[:base]).to contain_exactly("Maximum number of #{subject.class.limit_name.humanize(capitalize: false)} (1) exceeded")
end
diff --git a/spec/support/shared_examples/models/wiki_shared_examples.rb b/spec/support/shared_examples/models/wiki_shared_examples.rb
index 84569e95e11..a881d5f036c 100644
--- a/spec/support/shared_examples/models/wiki_shared_examples.rb
+++ b/spec/support/shared_examples/models/wiki_shared_examples.rb
@@ -32,6 +32,13 @@ RSpec.shared_examples 'wiki model' do
it 'returns the wiki base path' do
expect(subject.wiki_base_path).to eq("#{wiki_container.web_url(only_path: true)}/-/wikis")
end
+
+ it 'includes the relative URL root' do
+ allow(Rails.application.routes).to receive(:default_url_options).and_return(script_name: '/root')
+
+ expect(subject.wiki_base_path).to start_with('/root/')
+ expect(subject.wiki_base_path).not_to start_with('/root/root')
+ end
end
describe '#wiki' do
diff --git a/spec/support/shared_examples/path_extraction_shared_examples.rb b/spec/support/shared_examples/path_extraction_shared_examples.rb
new file mode 100644
index 00000000000..19c6f2404e5
--- /dev/null
+++ b/spec/support/shared_examples/path_extraction_shared_examples.rb
@@ -0,0 +1,118 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'assigns ref vars' do
+ it 'assigns the repository var' do
+ assign_ref_vars
+
+ expect(@repo).to eq container.repository
+ end
+
+ context 'ref contains %20' do
+ let(:ref) { 'foo%20bar' }
+
+ it 'is not converted to a space in @id' do
+ container.repository.add_branch(owner, 'foo%20bar', 'master')
+
+ assign_ref_vars
+
+ expect(@id).to start_with('foo%20bar/')
+ end
+ end
+
+ context 'ref contains trailing space' do
+ let(:ref) { 'master ' }
+
+ it 'strips surrounding space' do
+ assign_ref_vars
+
+ expect(@ref).to eq('master')
+ end
+ end
+
+ context 'ref contains leading space' do
+ let(:ref) { ' master ' }
+
+ it 'strips surrounding space' do
+ assign_ref_vars
+
+ expect(@ref).to eq('master')
+ end
+ end
+
+ context 'path contains space' do
+ let(:params) { { path: 'with space', ref: '38008cb17ce1466d8fec2dfa6f6ab8dcfe5cf49e' } }
+
+ it 'is not converted to %20 in @path' do
+ assign_ref_vars
+
+ expect(@path).to eq(params[:path])
+ end
+ end
+
+ context 'subclass overrides get_id' do
+ it 'uses ref returned by get_id' do
+ allow_next_instance_of(self.class) do |instance|
+ allow(instance).to receive(:get_id) { '38008cb17ce1466d8fec2dfa6f6ab8dcfe5cf49e' }
+ end
+
+ assign_ref_vars
+
+ expect(@id).to eq(get_id)
+ end
+ end
+end
+
+RSpec.shared_examples 'extracts refs' do
+ describe '#extract_ref' do
+ it 'returns an empty pair when no repository_container is set' do
+ allow_any_instance_of(described_class).to receive(:repository_container).and_return(nil)
+ expect(extract_ref('master/CHANGELOG')).to eq(['', ''])
+ end
+
+ context 'without a path' do
+ it 'extracts a valid branch' do
+ expect(extract_ref('master')).to eq(['master', ''])
+ end
+
+ it 'extracts a valid tag' do
+ expect(extract_ref('v2.0.0')).to eq(['v2.0.0', ''])
+ end
+
+ it 'extracts a valid commit ref without a path' do
+ expect(extract_ref('f4b14494ef6abf3d144c28e4af0c20143383e062')).to eq(
+ ['f4b14494ef6abf3d144c28e4af0c20143383e062', '']
+ )
+ end
+
+ it 'falls back to a primitive split for an invalid ref' do
+ expect(extract_ref('stable')).to eq(['stable', ''])
+ end
+
+ it 'extracts the longest matching ref' do
+ expect(extract_ref('release/app/v1.0.0/README.md')).to eq(
+ ['release/app/v1.0.0', 'README.md'])
+ end
+ end
+
+ context 'with a path' do
+ it 'extracts a valid branch' do
+ expect(extract_ref('foo/bar/baz/CHANGELOG')).to eq(
+ ['foo/bar/baz', 'CHANGELOG'])
+ end
+
+ it 'extracts a valid tag' do
+ expect(extract_ref('v2.0.0/CHANGELOG')).to eq(['v2.0.0', 'CHANGELOG'])
+ end
+
+ it 'extracts a valid commit SHA' do
+ expect(extract_ref('f4b14494ef6abf3d144c28e4af0c20143383e062/CHANGELOG')).to eq(
+ %w(f4b14494ef6abf3d144c28e4af0c20143383e062 CHANGELOG)
+ )
+ end
+
+ it 'falls back to a primitive split for an invalid ref' do
+ expect(extract_ref('stable/CHANGELOG')).to eq(%w(stable CHANGELOG))
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/quick_actions/issuable/time_tracking_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/time_tracking_quick_action_shared_examples.rb
index 37a504cd56a..37ee2548dfe 100644
--- a/spec/support/shared_examples/quick_actions/issuable/time_tracking_quick_action_shared_examples.rb
+++ b/spec/support/shared_examples/quick_actions/issuable/time_tracking_quick_action_shared_examples.rb
@@ -86,7 +86,7 @@ RSpec.shared_examples 'issuable time tracker' do |issuable_type|
page.within '.time-tracking-component-wrap' do
find('.help-button').click
- expect(find_link('Learn more')[:href]).to have_content('/help/workflow/time_tracking.md')
+ expect(find_link('Learn more')[:href]).to have_content('/help/user/project/time_tracking.md')
end
end
end
diff --git a/spec/support/shared_examples/requests/api/container_repositories_shared_examples.rb b/spec/support/shared_examples/requests/api/container_repositories_shared_examples.rb
index 0f277c11913..3e058838773 100644
--- a/spec/support/shared_examples/requests/api/container_repositories_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/container_repositories_shared_examples.rb
@@ -54,6 +54,29 @@ RSpec.shared_examples 'returns repositories for allowed users' do |user_type, sc
expect(response).to match_response_schema('registry/repositories')
end
end
+
+ context 'with tags_count param' do
+ let(:url) { "/#{scope}s/#{object.id}/registry/repositories?tags_count=true" }
+
+ before do
+ stub_container_registry_tags(repository: root_repository.path, tags: %w(rootA latest), with_manifest: true)
+ stub_container_registry_tags(repository: test_repository.path, tags: %w(rootA latest), with_manifest: true)
+ end
+
+ it 'returns a list of repositories and their tags_count' do
+ subject
+
+ expect(response.body).to include('tags_count')
+ expect(json_response[0]['tags_count']).to eq(2)
+ end
+
+ it 'returns a matching schema' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to match_response_schema('registry/repositories')
+ end
+ end
end
end
diff --git a/spec/support/shared_examples/requests/api/diff_discussions_shared_examples.rb b/spec/support/shared_examples/requests/api/diff_discussions_shared_examples.rb
index 3d25b9076ad..518c5b8dc28 100644
--- a/spec/support/shared_examples/requests/api/diff_discussions_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/diff_discussions_shared_examples.rb
@@ -30,7 +30,9 @@ RSpec.shared_examples 'diff discussions API' do |parent_type, noteable_type, id_
it "creates a new diff note" do
line_range = {
"start_line_code" => Gitlab::Git.diff_line_code(diff_note.position.file_path, 1, 1),
- "end_line_code" => Gitlab::Git.diff_line_code(diff_note.position.file_path, 2, 2)
+ "end_line_code" => Gitlab::Git.diff_line_code(diff_note.position.file_path, 2, 2),
+ "start_line_type" => diff_note.position.type,
+ "end_line_type" => diff_note.position.type
}
position = diff_note.position.to_h.merge({ line_range: line_range })
diff --git a/spec/support/shared_examples/requests/api/graphql/group_and_project_boards_query_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/group_and_project_boards_query_shared_examples.rb
index feb3ba46353..f26af6cb766 100644
--- a/spec/support/shared_examples/requests/api/graphql/group_and_project_boards_query_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/graphql/group_and_project_boards_query_shared_examples.rb
@@ -45,44 +45,37 @@ RSpec.shared_examples 'group and project boards query' do
end
describe 'sorting and pagination' do
+ let(:data_path) { [board_parent_type, :boards] }
+
+ def pagination_query(params, page_info)
+ graphql_query_for(
+ board_parent_type,
+ { 'fullPath' => board_parent.full_path },
+ query_graphql_field('boards', params, "#{page_info} edges { node { id } }")
+ )
+ end
+
+ def pagination_results_data(data)
+ data.map { |board| board.dig('node', 'id') }
+ end
+
context 'when using default sorting' do
let!(:board_B) { create(:board, resource_parent: board_parent, name: 'B') }
let!(:board_C) { create(:board, resource_parent: board_parent, name: 'C') }
let!(:board_a) { create(:board, resource_parent: board_parent, name: 'a') }
let!(:board_A) { create(:board, resource_parent: board_parent, name: 'A') }
-
- before do
- post_graphql(query, current_user: current_user)
- end
-
- it_behaves_like 'a working graphql query'
+ let(:boards) { [board_a, board_A, board_B, board_C] }
context 'when ascending' do
- let(:boards) { [board_a, board_A, board_B, board_C] }
- let(:expected_boards) do
- if board_parent.multiple_issue_boards_available?
- boards
- else
- [boards.first]
- end
- end
-
- it 'sorts boards' do
- expect(grab_names).to eq expected_boards.map(&:name)
- end
-
- context 'when paginating' do
- let(:params) { 'first: 2' }
-
- it 'sorts boards' do
- expect(grab_names).to eq expected_boards.first(2).map(&:name)
-
- cursored_query = query("after: \"#{end_cursor}\"")
- post_graphql(cursored_query, current_user: current_user)
-
- response_data = Gitlab::Json.parse(response.body)['data'][board_parent_type]['boards']['edges']
-
- expect(grab_names(response_data)).to eq expected_boards.drop(2).first(2).map(&:name)
+ it_behaves_like 'sorted paginated query' do
+ let(:sort_param) { }
+ let(:first_param) { 2 }
+ let(:expected_results) do
+ if board_parent.multiple_issue_boards_available?
+ boards.map { |board| board.to_global_id.to_s }
+ else
+ [boards.first.to_global_id.to_s]
+ end
end
end
end
diff --git a/spec/support/shared_examples/requests/api/issuable_update_shared_examples.rb b/spec/support/shared_examples/requests/api/issuable_update_shared_examples.rb
new file mode 100644
index 00000000000..ded381fd402
--- /dev/null
+++ b/spec/support/shared_examples/requests/api/issuable_update_shared_examples.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'issuable update endpoint' do
+ let(:area) { entity.class.name.underscore.pluralize }
+
+ describe 'PUT /projects/:id/issues/:issue_id' do
+ let(:url) { "/projects/#{project.id}/#{area}/#{entity.iid}" }
+
+ it 'clears labels when labels param is nil' do
+ put api(url, user), params: { labels: 'label1' }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['labels']).to contain_exactly('label1')
+
+ put api(url, user), params: { labels: nil }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ json_response = Gitlab::Json.parse(response.body)
+ expect(json_response['labels']).to be_empty
+ end
+
+ it 'updates the issuable with labels param as array' do
+ stub_const("Gitlab::QueryLimiting::Transaction::THRESHOLD", 110)
+
+ params = { labels: ['label1', 'label2', 'foo, bar', '&,?'] }
+
+ put api(url, user), params: params
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['labels']).to include 'label1'
+ expect(json_response['labels']).to include 'label2'
+ expect(json_response['labels']).to include 'foo'
+ expect(json_response['labels']).to include 'bar'
+ expect(json_response['labels']).to include '&'
+ expect(json_response['labels']).to include '?'
+ end
+ end
+end
diff --git a/spec/support/shared_examples/requests/api/resource_label_events_api_shared_examples.rb b/spec/support/shared_examples/requests/api/resource_label_events_api_shared_examples.rb
index f49f944f38d..675b6c5cef6 100644
--- a/spec/support/shared_examples/requests/api/resource_label_events_api_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/resource_label_events_api_shared_examples.rb
@@ -93,6 +93,20 @@ RSpec.shared_examples 'resource_label_events API' do |parent_type, eventable_typ
end
end
+ describe 'pagination' do
+ let!(:event1) { create_event(label) }
+ let!(:event2) { create_event(label) }
+
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/220192
+ it "returns the second page" do
+ get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_label_events?page=2&per_page=1", user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response.count).to eq(1)
+ expect(json_response.first['id']).to eq(event2.id)
+ end
+ end
+
def create_event(label)
create(:resource_label_event, eventable.class.name.underscore => eventable, label: label)
end
diff --git a/spec/support/shared_examples/requests/api/resource_milestone_events_api_shared_examples.rb b/spec/support/shared_examples/requests/api/resource_milestone_events_api_shared_examples.rb
new file mode 100644
index 00000000000..bca51dab353
--- /dev/null
+++ b/spec/support/shared_examples/requests/api/resource_milestone_events_api_shared_examples.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'resource_milestone_events API' do |parent_type, eventable_type, id_name|
+ describe "GET /#{parent_type}/:id/#{eventable_type}/:noteable_id/resource_milestone_events" do
+ let!(:event) { create_event(milestone) }
+
+ it "returns an array of resource milestone events" do
+ url = "/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_milestone_events"
+ get api(url, user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.first['id']).to eq(event.id)
+ expect(json_response.first['milestone']['id']).to eq(event.milestone.id)
+ expect(json_response.first['action']).to eq(event.action)
+ end
+
+ it "returns a 404 error when eventable id not found" do
+ get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{non_existing_record_id}/resource_milestone_events", user)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+
+ it "returns 404 when not authorized" do
+ parent.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
+ private_user = create(:user)
+
+ get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_milestone_events", private_user)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ describe "GET /#{parent_type}/:id/#{eventable_type}/:noteable_id/resource_milestone_events/:event_id" do
+ let!(:event) { create_event(milestone) }
+
+ it "returns a resource milestone event by id" do
+ get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_milestone_events/#{event.id}", user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['id']).to eq(event.id)
+ expect(json_response['milestone']['id']).to eq(event.milestone.id)
+ expect(json_response['action']).to eq(event.action)
+ end
+
+ it "returns 404 when not authorized" do
+ parent.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
+ private_user = create(:user)
+
+ get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_milestone_events/#{event.id}", private_user)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+
+ it "returns a 404 error if resource milestone event not found" do
+ get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_milestone_events/#{non_existing_record_id}", user)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ def create_event(milestone, action: :add)
+ create(:resource_milestone_event, eventable.class.name.underscore => eventable, milestone: milestone, action: action)
+ end
+end
diff --git a/spec/support/shared_examples/serializers/diff_file_entity_shared_examples.rb b/spec/support/shared_examples/serializers/diff_file_entity_shared_examples.rb
index db5c4b45b70..1ef08de31a9 100644
--- a/spec/support/shared_examples/serializers/diff_file_entity_shared_examples.rb
+++ b/spec/support/shared_examples/serializers/diff_file_entity_shared_examples.rb
@@ -8,7 +8,7 @@ RSpec.shared_examples 'diff file base entity' do
:file_hash, :file_path, :old_path, :new_path,
:viewer, :diff_refs, :stored_externally,
:external_storage, :renamed_file, :deleted_file,
- :a_mode, :b_mode, :new_file)
+ :a_mode, :b_mode, :new_file, :file_identifier_hash)
end
# Converted diff files from GitHub import does not contain blob file
diff --git a/spec/support/shared_examples/serializers/import/import_entity_shared_examples.rb b/spec/support/shared_examples/serializers/import/import_entity_shared_examples.rb
new file mode 100644
index 00000000000..6422c4beb1d
--- /dev/null
+++ b/spec/support/shared_examples/serializers/import/import_entity_shared_examples.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'exposes required fields for import entity' do
+ describe 'exposes required fields' do
+ it 'correctly exposes id' do
+ expect(subject[:id]).to eql(expected_values[:id])
+ end
+
+ it 'correctly exposes full name' do
+ expect(subject[:full_name]).to eql(expected_values[:full_name])
+ end
+
+ it 'correctly exposes sanitized name' do
+ expect(subject[:sanitized_name]).to eql(expected_values[:sanitized_name])
+ end
+
+ it 'correctly exposes provider link' do
+ expect(subject[:provider_link]).to eql(expected_values[:provider_link])
+ end
+ end
+end
diff --git a/spec/support/shared_examples/services/container_expiration_policy_shared_examples.rb b/spec/support/shared_examples/services/container_expiration_policy_shared_examples.rb
new file mode 100644
index 00000000000..28bf46a57d5
--- /dev/null
+++ b/spec/support/shared_examples/services/container_expiration_policy_shared_examples.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'updating the container expiration policy attributes' do |mode:, from: {}, to:|
+ if mode == :create
+ it 'creates a new container expiration policy' do
+ expect { subject }
+ .to change { project.reload.container_expiration_policy.present? }.from(false).to(true)
+ .and change { ContainerExpirationPolicy.count }.by(1)
+ end
+ else
+ it_behaves_like 'not creating the container expiration policy'
+ end
+
+ it 'updates the container expiration policy' do
+ if from.empty?
+ subject
+
+ expect(container_expiration_policy.reload.cadence).to eq(to[:cadence])
+ expect(container_expiration_policy.keep_n).to eq(to[:keep_n])
+ expect(container_expiration_policy.older_than).to eq(to[:older_than])
+ else
+ expect { subject }
+ .to change { container_expiration_policy.reload.cadence }.from(from[:cadence]).to(to[:cadence])
+ .and change { container_expiration_policy.reload.keep_n }.from(from[:keep_n]).to(to[:keep_n])
+ .and change { container_expiration_policy.reload.older_than }.from(from[:older_than]).to(to[:older_than])
+ end
+ end
+end
+
+RSpec.shared_examples 'not creating the container expiration policy' do
+ it "doesn't create the container expiration policy" do
+ expect { subject }.not_to change { ContainerExpirationPolicy.count }
+ end
+end
+
+RSpec.shared_examples 'creating the container expiration policy' do
+ it_behaves_like 'updating the container expiration policy attributes', mode: :create, to: { cadence: '3month', keep_n: 100, older_than: '14d' }
+
+ it_behaves_like 'returning a success'
+end
diff --git a/spec/support/shared_examples/services/wiki_pages/create_service_shared_examples.rb b/spec/support/shared_examples/services/wiki_pages/create_service_shared_examples.rb
index 71bdd46572f..efcb83a34af 100644
--- a/spec/support/shared_examples/services/wiki_pages/create_service_shared_examples.rb
+++ b/spec/support/shared_examples/services/wiki_pages/create_service_shared_examples.rb
@@ -45,7 +45,7 @@ RSpec.shared_examples 'WikiPages::CreateService#execute' do |container_type|
expect { service.execute }.to change { Event.count }.by 1
expect(Event.recent.first).to have_attributes(
- action: Event::CREATED,
+ action: 'created',
target: have_attributes(canonical_slug: page_title)
)
end
diff --git a/spec/support/shared_examples/services/wiki_pages/destroy_service_shared_examples.rb b/spec/support/shared_examples/services/wiki_pages/destroy_service_shared_examples.rb
index 62541eb3da9..1231c012c31 100644
--- a/spec/support/shared_examples/services/wiki_pages/destroy_service_shared_examples.rb
+++ b/spec/support/shared_examples/services/wiki_pages/destroy_service_shared_examples.rb
@@ -27,7 +27,7 @@ RSpec.shared_examples 'WikiPages::DestroyService#execute' do |container_type|
expect { service.execute(page) }.to change { Event.count }.by 1
expect(Event.recent.first).to have_attributes(
- action: Event::DESTROYED,
+ action: 'destroyed',
target: have_attributes(canonical_slug: page.slug)
)
end
diff --git a/spec/support/shared_examples/services/wiki_pages/update_service_shared_examples.rb b/spec/support/shared_examples/services/wiki_pages/update_service_shared_examples.rb
index 0dfc99d043b..77354fec069 100644
--- a/spec/support/shared_examples/services/wiki_pages/update_service_shared_examples.rb
+++ b/spec/support/shared_examples/services/wiki_pages/update_service_shared_examples.rb
@@ -48,7 +48,7 @@ RSpec.shared_examples 'WikiPages::UpdateService#execute' do |container_type|
expect { service.execute(page) }.to change { Event.count }.by 1
expect(Event.recent.first).to have_attributes(
- action: Event::UPDATED,
+ action: 'updated',
wiki_page: page,
target_title: page.title
)
diff --git a/spec/support/shared_examples/uncached_response_shared_examples.rb b/spec/support/shared_examples/uncached_response_shared_examples.rb
new file mode 100644
index 00000000000..3997017ff35
--- /dev/null
+++ b/spec/support/shared_examples/uncached_response_shared_examples.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+#
+# Pairs with lib/gitlab/no_cache_headers.rb
+#
+
+RSpec.shared_examples 'uncached response' do
+ it 'defines an uncached header response' do
+ expect(response.headers["Cache-Control"]).to include("no-store", "no-cache")
+ expect(response.headers["Pragma"]).to eq("no-cache")
+ expect(response.headers["Expires"]).to eq("Fri, 01 Jan 1990 00:00:00 GMT")
+ end
+end