summaryrefslogtreecommitdiff
path: root/spec/support
diff options
context:
space:
mode:
Diffstat (limited to 'spec/support')
-rw-r--r--spec/support/capybara.rb13
-rw-r--r--spec/support/cycle_analytics_helpers/test_generation.rb4
-rw-r--r--spec/support/database_cleaner.rb2
-rw-r--r--spec/support/helpers/admin_mode_helpers.rb3
-rw-r--r--spec/support/helpers/concurrent_helpers.rb40
-rw-r--r--spec/support/helpers/design_management_test_helpers.rb45
-rw-r--r--spec/support/helpers/exclusive_lease_helpers.rb4
-rw-r--r--spec/support/helpers/fake_blob_helpers.rb6
-rw-r--r--spec/support/helpers/graphql_helpers.rb9
-rw-r--r--spec/support/helpers/jira_service_helper.rb5
-rw-r--r--spec/support/helpers/kubernetes_helpers.rb112
-rw-r--r--spec/support/helpers/login_helpers.rb2
-rw-r--r--spec/support/helpers/query_recorder.rb4
-rw-r--r--spec/support/helpers/reactive_caching_helpers.rb11
-rw-r--r--spec/support/helpers/smime_helper.rb14
-rw-r--r--spec/support/helpers/stub_feature_flags.rb34
-rw-r--r--spec/support/helpers/stub_gitlab_calls.rb8
-rw-r--r--spec/support/helpers/stub_object_storage.rb2
-rw-r--r--spec/support/helpers/test_env.rb20
-rw-r--r--spec/support/helpers/usage_data_helpers.rb95
-rw-r--r--spec/support/helpers/wiki_helpers.rb7
-rw-r--r--spec/support/helpers/workhorse_helpers.rb2
-rw-r--r--spec/support/helpers/x509_helpers.rb137
-rw-r--r--spec/support/import_export/common_util.rb19
-rw-r--r--spec/support/import_export/configuration_helper.rb4
-rw-r--r--spec/support/kubeclient.rb10
-rw-r--r--spec/support/matchers/disallow_request_matchers.rb2
-rw-r--r--spec/support/matchers/graphql_matchers.rb10
-rw-r--r--spec/support/rails/test_case_patch.rb53
-rw-r--r--spec/support/redis/redis_shared_examples.rb2
-rw-r--r--spec/support/renameable_upload.rb15
-rw-r--r--spec/support/shared_contexts/cache_allowed_users_in_namespace_shared_context.rb31
-rw-r--r--spec/support/shared_contexts/design_management_shared_contexts.rb38
-rw-r--r--spec/support/shared_contexts/features/error_tracking_shared_context.rb4
-rw-r--r--spec/support/shared_contexts/issuable/merge_request_shared_context.rb49
-rw-r--r--spec/support/shared_contexts/issuable/project_shared_context.rb16
-rw-r--r--spec/support/shared_contexts/json_response_shared_context.rb2
-rw-r--r--spec/support/shared_contexts/lib/gitlab/import_export/project/rake_task_object_storage_shared_context.rb17
-rw-r--r--spec/support/shared_contexts/navbar_structure_context.rb2
-rw-r--r--spec/support/shared_contexts/policies/group_policy_shared_context.rb10
-rw-r--r--spec/support/shared_contexts/policies/project_policy_shared_context.rb3
-rw-r--r--spec/support/shared_contexts/project_service_shared_context.rb5
-rw-r--r--spec/support/shared_contexts/services_shared_context.rb3
-rw-r--r--spec/support/shared_contexts/spam_constants.rb7
-rw-r--r--spec/support/shared_examples/controllers/instance_statistics_controllers_shared_examples.rb20
-rw-r--r--spec/support/shared_examples/controllers/known_sign_in_shared_examples.rb59
-rw-r--r--spec/support/shared_examples/controllers/variables_shared_examples.rb25
-rw-r--r--spec/support/shared_examples/features/error_tracking_shared_example.rb14
-rw-r--r--spec/support/shared_examples/features/variable_list_shared_examples.rb3
-rw-r--r--spec/support/shared_examples/graphql/design_fields_shared_examples.rb80
-rw-r--r--spec/support/shared_examples/graphql/jira_import/jira_import_resolver_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/graphql/sorted_paginated_query_shared_examples.rb92
-rw-r--r--spec/support/shared_examples/helm_commands_shared_examples.rb131
-rw-r--r--spec/support/shared_examples/lib/gitlab/cycle_analytics/base_stage_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/lib/gitlab/helm_generated_script_shared_examples.rb15
-rw-r--r--spec/support/shared_examples/lib/gitlab/import_export/project/rake_task_object_storage_shared_examples.rb22
-rw-r--r--spec/support/shared_examples/lib/gitlab/migration_helpers_shared_examples.rb3
-rw-r--r--spec/support/shared_examples/models/chat_service_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/models/cluster_application_status_shared_examples.rb61
-rw-r--r--spec/support/shared_examples/models/concerns/blob_replicator_strategy_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/models/concerns/has_repository_shared_examples.rb20
-rw-r--r--spec/support/shared_examples/models/concerns/has_wiki_shared_examples.rb79
-rw-r--r--spec/support/shared_examples/models/concerns/limitable_shared_examples.rb36
-rw-r--r--spec/support/shared_examples/models/concerns/timebox_shared_examples.rb242
-rw-r--r--spec/support/shared_examples/models/diff_positionable_note_shared_examples.rb24
-rw-r--r--spec/support/shared_examples/models/email_format_shared_examples.rb41
-rw-r--r--spec/support/shared_examples/models/issuable_hook_data_shared_examples.rb1
-rw-r--r--spec/support/shared_examples/models/mentionable_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/models/slack_mattermost_notifications_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/models/wiki_shared_examples.rb423
-rw-r--r--spec/support/shared_examples/policies/project_policy_shared_examples.rb14
-rw-r--r--spec/support/shared_examples/policies/wiki_policies_shared_examples.rb228
-rw-r--r--spec/support/shared_examples/quick_actions/issuable/issuable_quick_actions_shared_examples.rb22
-rw-r--r--spec/support/shared_examples/requests/api/award_emoji_todo_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/requests/api/graphql/group_and_project_boards_query_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/requests/api/graphql/mutations/snippets_shared_examples.rb10
-rw-r--r--spec/support/shared_examples/requests/snippet_shared_examples.rb70
-rw-r--r--spec/support/shared_examples/requires_variables_shared_example.rb13
-rw-r--r--spec/support/shared_examples/resource_events.rb18
-rw-r--r--spec/support/shared_examples/serializers/diff_file_entity_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/services/boards/lists_list_service_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/services/measurable_service_shared_examples.rb32
-rw-r--r--spec/support/shared_examples/services/metrics/dashboard_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/services/projects/update_repository_storage_service_shared_examples.rb26
-rw-r--r--spec/support/shared_examples/services/resource_events/change_milestone_service_shared_examples.rb7
-rw-r--r--spec/support/shared_examples/services/snippets_shared_examples.rb42
-rw-r--r--spec/support/shared_examples/services/wiki_pages/create_service_shared_examples.rb94
-rw-r--r--spec/support/shared_examples/services/wiki_pages/destroy_service_shared_examples.rb50
-rw-r--r--spec/support/shared_examples/services/wiki_pages/update_service_shared_examples.rb98
-rw-r--r--spec/support/shared_examples/services/wikis/create_attachment_service_shared_examples.rb62
-rw-r--r--spec/support/shared_examples/tasks/gitlab/import_export/measurable_shared_examples.rb31
-rw-r--r--spec/support/shared_examples/workers/authorized_projects_worker_shared_example.rb42
-rw-r--r--spec/support/shared_examples/workers/gitlab/jira_import/jira_import_workers_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/workers/pages_domain_cron_worker_shared_examples.rb10
-rw-r--r--spec/support/shared_examples/workers/reactive_cacheable_shared_examples.rb59
-rw-r--r--spec/support/sidekiq.rb13
-rw-r--r--spec/support/sidekiq_middleware.rb13
-rw-r--r--spec/support/unicorn.rb27
-rw-r--r--spec/support/webmock.rb18
99 files changed, 2863 insertions, 442 deletions
diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb
index 90adfb1a2ee..38f9ccf23f5 100644
--- a/spec/support/capybara.rb
+++ b/spec/support/capybara.rb
@@ -7,7 +7,7 @@ require 'capybara-screenshot/rspec'
require 'selenium-webdriver'
# Give CI some extra time
-timeout = (ENV['CI'] || ENV['CI_SERVER']) ? 60 : 30
+timeout = ENV['CI'] || ENV['CI_SERVER'] ? 60 : 30
# Define an error class for JS console messages
JSConsoleError = Class.new(StandardError)
@@ -95,12 +95,23 @@ RSpec.configure do |config|
config.include CapybaraHelpers, type: :feature
config.before(:context, :js) do
+ # This prevents Selenium from creating thousands of connections while waiting for
+ # an element to appear
+ webmock_enable_with_http_connect_on_start!
+
next if $capybara_server_already_started
TestEnv.eager_load_driver_server
$capybara_server_already_started = true
end
+ config.after(:context, :js) do
+ # WebMock doesn't stub connections, so we need to restore the original behavior
+ # to prevent many specs from failing:
+ # https://github.com/bblimke/webmock/blob/master/README.md#connecting-on-nethttpstart
+ webmock_enable!
+ end
+
config.before(:example, :js) do
session = Capybara.current_session
diff --git a/spec/support/cycle_analytics_helpers/test_generation.rb b/spec/support/cycle_analytics_helpers/test_generation.rb
index 34018263339..c577e5cc665 100644
--- a/spec/support/cycle_analytics_helpers/test_generation.rb
+++ b/spec/support/cycle_analytics_helpers/test_generation.rb
@@ -29,6 +29,10 @@ module CycleAnalyticsHelpers
scenarios.each do |start_time_conditions, end_time_conditions|
let_it_be(:other_project) { create(:project, :repository) }
+ before do
+ other_project.add_developer(self.user)
+ end
+
context "start condition: #{start_time_conditions.map(&:first).to_sentence}" do
context "end condition: #{end_time_conditions.map(&:first).to_sentence}" do
it "finds the median of available durations between the two conditions", :sidekiq_might_not_need_inline do
diff --git a/spec/support/database_cleaner.rb b/spec/support/database_cleaner.rb
index f6339d7343c..60d82f7e92a 100644
--- a/spec/support/database_cleaner.rb
+++ b/spec/support/database_cleaner.rb
@@ -35,6 +35,8 @@ RSpec.configure do |config|
puts "Recreating the database"
start = Gitlab::Metrics::System.monotonic_time
+ ActiveRecord::AdvisoryLockBase.clear_all_connections!
+
ActiveRecord::Tasks::DatabaseTasks.drop_current
ActiveRecord::Tasks::DatabaseTasks.create_current
ActiveRecord::Tasks::DatabaseTasks.load_schema_current
diff --git a/spec/support/helpers/admin_mode_helpers.rb b/spec/support/helpers/admin_mode_helpers.rb
index e995a7d4f5e..36ed262f8ae 100644
--- a/spec/support/helpers/admin_mode_helpers.rb
+++ b/spec/support/helpers/admin_mode_helpers.rb
@@ -7,6 +7,9 @@ module AdminModeHelper
# mode for accessing any administrative functionality. This helper lets a user
# be in admin mode without requiring a second authentication step (provided
# the user is an admin)
+ #
+ # See also tag :enable_admin_mode in spec/spec_helper.rb for a spec-wide
+ # alternative
def enable_admin_mode!(user)
fake_user_mode = instance_double(Gitlab::Auth::CurrentUserMode)
diff --git a/spec/support/helpers/concurrent_helpers.rb b/spec/support/helpers/concurrent_helpers.rb
new file mode 100644
index 00000000000..4eecc2133e7
--- /dev/null
+++ b/spec/support/helpers/concurrent_helpers.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+module ConcurrentHelpers
+ Cancelled = Class.new(StandardError)
+
+ # To test for contention, we may need to run some actions in parallel. This
+ # helper takes an array of blocks and schedules them all on different threads
+ # in a fixed-size thread pool.
+ #
+ # @param [Array[Proc]] blocks
+ # @param [Integer] task_wait_time: time to wait for each task (upper bound on
+ # reasonable task execution time)
+ # @param [Integer] max_concurrency: maximum number of tasks to run at once
+ #
+ def run_parallel(blocks, task_wait_time: 20.seconds, max_concurrency: Concurrent.processor_count - 1)
+ thread_pool = Concurrent::FixedThreadPool.new(
+ [2, max_concurrency].max, { max_queue: blocks.size }
+ )
+ opts = { executor: thread_pool }
+
+ error = Concurrent::MVar.new
+
+ blocks.map { |block| Concurrent::Future.execute(opts, &block) }.each do |future|
+ future.wait(task_wait_time)
+
+ if future.complete?
+ error.put(future.reason) if future.reason && error.empty?
+ else
+ future.cancel
+ error.put(Cancelled.new) if error.empty?
+ end
+ end
+
+ raise error.take if error.full?
+ ensure
+ thread_pool.shutdown
+ thread_pool.wait_for_termination(10)
+ thread_pool.kill if thread_pool.running?
+ end
+end
diff --git a/spec/support/helpers/design_management_test_helpers.rb b/spec/support/helpers/design_management_test_helpers.rb
new file mode 100644
index 00000000000..bf41e2f5079
--- /dev/null
+++ b/spec/support/helpers/design_management_test_helpers.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+module DesignManagementTestHelpers
+ def enable_design_management(enabled = true, ref_filter = true)
+ stub_lfs_setting(enabled: enabled)
+ stub_feature_flags(design_management_reference_filter_gfm_pipeline: ref_filter)
+ end
+
+ def delete_designs(*designs)
+ act_on_designs(designs) { ::DesignManagement::Action.deletion }
+ end
+
+ def restore_designs(*designs)
+ act_on_designs(designs) { ::DesignManagement::Action.creation }
+ end
+
+ def modify_designs(*designs)
+ act_on_designs(designs) { ::DesignManagement::Action.modification }
+ end
+
+ def path_for_design(design)
+ path_options = { vueroute: design.filename }
+ Gitlab::Routing.url_helpers.designs_project_issue_path(design.project, design.issue, path_options)
+ end
+
+ def url_for_design(design)
+ path_options = { vueroute: design.filename }
+ Gitlab::Routing.url_helpers.designs_project_issue_url(design.project, design.issue, path_options)
+ end
+
+ def url_for_designs(issue)
+ Gitlab::Routing.url_helpers.designs_project_issue_url(issue.project, issue)
+ end
+
+ private
+
+ def act_on_designs(designs, &block)
+ issue = designs.first.issue
+ version = build(:design_version, :empty, issue: issue).tap { |v| v.save(validate: false) }
+ designs.each do |d|
+ yield.create(design: d, version: version)
+ end
+ version
+ end
+end
diff --git a/spec/support/helpers/exclusive_lease_helpers.rb b/spec/support/helpers/exclusive_lease_helpers.rb
index 77703e20602..95cfc56c273 100644
--- a/spec/support/helpers/exclusive_lease_helpers.rb
+++ b/spec/support/helpers/exclusive_lease_helpers.rb
@@ -9,7 +9,9 @@ module ExclusiveLeaseHelpers
Gitlab::ExclusiveLease,
try_obtain: uuid,
exists?: true,
- renew: renew
+ renew: renew,
+ cancel: nil,
+ ttl: timeout
)
allow(Gitlab::ExclusiveLease)
diff --git a/spec/support/helpers/fake_blob_helpers.rb b/spec/support/helpers/fake_blob_helpers.rb
index a7eafb0fd23..6c8866deac4 100644
--- a/spec/support/helpers/fake_blob_helpers.rb
+++ b/spec/support/helpers/fake_blob_helpers.rb
@@ -22,7 +22,11 @@ module FakeBlobHelpers
alias_method :name, :path
def id
- 0
+ "00000000"
+ end
+
+ def commit_id
+ "11111111"
end
def binary_in_repo?
diff --git a/spec/support/helpers/graphql_helpers.rb b/spec/support/helpers/graphql_helpers.rb
index fc543186b08..b3d7f7bcece 100644
--- a/spec/support/helpers/graphql_helpers.rb
+++ b/spec/support/helpers/graphql_helpers.rb
@@ -246,12 +246,19 @@ module GraphqlHelpers
# Raises an error if no data is found
def graphql_data
+ # Note that `json_response` is defined as `let(:json_response)` and
+ # therefore, in a spec with multiple queries, will only contain data
+ # from the _first_ query, not subsequent ones
json_response['data'] || (raise NoData, graphql_errors)
end
def graphql_data_at(*path)
+ graphql_dig_at(graphql_data, *path)
+ end
+
+ def graphql_dig_at(data, *path)
keys = path.map { |segment| GraphqlHelpers.fieldnamerize(segment) }
- graphql_data.dig(*keys)
+ data.dig(*keys)
end
def graphql_errors
diff --git a/spec/support/helpers/jira_service_helper.rb b/spec/support/helpers/jira_service_helper.rb
index c23a8d52c84..198bedfe3bc 100644
--- a/spec/support/helpers/jira_service_helper.rb
+++ b/spec/support/helpers/jira_service_helper.rb
@@ -78,6 +78,11 @@ module JiraServiceHelper
JIRA_API + "/issue/#{issue_id}"
end
+ def stub_jira_service_test
+ WebMock.stub_request(:get, 'https://jira.example.com/rest/api/2/serverInfo')
+ .to_return(body: { url: 'http://url' }.to_json)
+ end
+
def stub_jira_urls(issue_id)
WebMock.stub_request(:get, jira_project_url)
WebMock.stub_request(:get, jira_api_comment_url(issue_id)).to_return(body: jira_issue_comments)
diff --git a/spec/support/helpers/kubernetes_helpers.rb b/spec/support/helpers/kubernetes_helpers.rb
index ca910e47695..8882f31e2f4 100644
--- a/spec/support/helpers/kubernetes_helpers.rb
+++ b/spec/support/helpers/kubernetes_helpers.rb
@@ -3,6 +3,8 @@
module KubernetesHelpers
include Gitlab::Kubernetes
+ NODE_NAME = "gke-cluster-applications-default-pool-49b7f225-v527"
+
def kube_response(body)
{ body: body.to_json }
end
@@ -11,6 +13,14 @@ module KubernetesHelpers
kube_response(kube_pods_body)
end
+ def nodes_response
+ kube_response(nodes_body)
+ end
+
+ def nodes_metrics_response
+ kube_response(nodes_metrics_body)
+ end
+
def kube_pod_response
kube_response(kube_pod)
end
@@ -34,6 +44,9 @@ module KubernetesHelpers
WebMock
.stub_request(:get, api_url + '/apis/rbac.authorization.k8s.io/v1')
.to_return(kube_response(kube_v1_rbac_authorization_discovery_body))
+ WebMock
+ .stub_request(:get, api_url + '/apis/metrics.k8s.io/v1beta1')
+ .to_return(kube_response(kube_metrics_v1beta1_discovery_body))
end
def stub_kubeclient_discover_istio(api_url)
@@ -76,6 +89,22 @@ module KubernetesHelpers
WebMock.stub_request(:get, pods_url).to_return(response || kube_pods_response)
end
+ def stub_kubeclient_nodes(api_url)
+ stub_kubeclient_discover_base(api_url)
+
+ nodes_url = api_url + "/api/v1/nodes"
+
+ WebMock.stub_request(:get, nodes_url).to_return(nodes_response)
+ end
+
+ def stub_kubeclient_nodes_and_nodes_metrics(api_url)
+ stub_kubeclient_nodes(api_url)
+
+ nodes_url = api_url + "/apis/metrics.k8s.io/v1beta1/nodes"
+
+ WebMock.stub_request(:get, nodes_url).to_return(nodes_metrics_response)
+ end
+
def stub_kubeclient_pods(namespace, status: nil)
stub_kubeclient_discover(service.api_url)
pods_url = service.api_url + "/api/v1/namespaces/#{namespace}/pods"
@@ -201,28 +230,8 @@ module KubernetesHelpers
.to_return(kube_response({}))
end
- def stub_kubeclient_get_cluster_role_binding_error(api_url, name, status: 404)
- WebMock.stub_request(:get, api_url + "/apis/rbac.authorization.k8s.io/v1/clusterrolebindings/#{name}")
- .to_return(status: [status, "Internal Server Error"])
- end
-
- def stub_kubeclient_create_cluster_role_binding(api_url)
- WebMock.stub_request(:post, api_url + '/apis/rbac.authorization.k8s.io/v1/clusterrolebindings')
- .to_return(kube_response({}))
- end
-
- def stub_kubeclient_get_role_binding(api_url, name, namespace: 'default')
- WebMock.stub_request(:get, api_url + "/apis/rbac.authorization.k8s.io/v1/namespaces/#{namespace}/rolebindings/#{name}")
- .to_return(kube_response({}))
- end
-
- def stub_kubeclient_get_role_binding_error(api_url, name, namespace: 'default', status: 404)
- WebMock.stub_request(:get, api_url + "/apis/rbac.authorization.k8s.io/v1/namespaces/#{namespace}/rolebindings/#{name}")
- .to_return(status: [status, "Internal Server Error"])
- end
-
- def stub_kubeclient_create_role_binding(api_url, namespace: 'default')
- WebMock.stub_request(:post, api_url + "/apis/rbac.authorization.k8s.io/v1/namespaces/#{namespace}/rolebindings")
+ def stub_kubeclient_put_cluster_role_binding(api_url, name)
+ WebMock.stub_request(:put, api_url + "/apis/rbac.authorization.k8s.io/v1/clusterrolebindings/#{name}")
.to_return(kube_response({}))
end
@@ -274,6 +283,7 @@ module KubernetesHelpers
{
"kind" => "APIResourceList",
"resources" => [
+ { "name" => "nodes", "namespaced" => false, "kind" => "Node" },
{ "name" => "pods", "namespaced" => true, "kind" => "Pod" },
{ "name" => "deployments", "namespaced" => true, "kind" => "Deployment" },
{ "name" => "secrets", "namespaced" => true, "kind" => "Secret" },
@@ -334,6 +344,16 @@ module KubernetesHelpers
}
end
+ def kube_metrics_v1beta1_discovery_body
+ {
+ "kind" => "APIResourceList",
+ "resources" => [
+ { "name" => "nodes", "namespaced" => false, "kind" => "NodeMetrics" },
+ { "name" => "pods", "namespaced" => true, "kind" => "PodMetrics" }
+ ]
+ }
+ end
+
def kube_istio_discovery_body
{
"kind" => "APIResourceList",
@@ -462,6 +482,20 @@ module KubernetesHelpers
}
end
+ def nodes_body
+ {
+ "kind" => "NodeList",
+ "items" => [kube_node]
+ }
+ end
+
+ def nodes_metrics_body
+ {
+ "kind" => "List",
+ "items" => [kube_node_metrics]
+ }
+ end
+
def kube_logs_body
"2019-12-13T14:04:22.123456Z Log 1\n2019-12-13T14:04:23.123456Z Log 2\n2019-12-13T14:04:24.123456Z Log 3"
end
@@ -514,6 +548,40 @@ module KubernetesHelpers
}
end
+ # This is a partial response, it will have many more elements in reality but
+ # these are the ones we care about at the moment
+ def kube_node
+ {
+ "metadata" => {
+ "name" => NODE_NAME
+ },
+ "status" => {
+ "capacity" => {
+ "cpu" => "2",
+ "memory" => "7657228Ki"
+ },
+ "allocatable" => {
+ "cpu" => "1930m",
+ "memory" => "5777164Ki"
+ }
+ }
+ }
+ end
+
+ # This is a partial response, it will have many more elements in reality but
+ # these are the ones we care about at the moment
+ def kube_node_metrics
+ {
+ "metadata" => {
+ "name" => NODE_NAME
+ },
+ "usage" => {
+ "cpu" => "144208668n",
+ "memory" => "1789048Ki"
+ }
+ }
+ end
+
# Similar to a kube_pod, but should contain a running service
def kube_knative_pod(name: "kube-pod", namespace: "default", status: "Running")
{
diff --git a/spec/support/helpers/login_helpers.rb b/spec/support/helpers/login_helpers.rb
index 6a4dcfcdb1e..cb880939b1c 100644
--- a/spec/support/helpers/login_helpers.rb
+++ b/spec/support/helpers/login_helpers.rb
@@ -50,9 +50,7 @@ module LoginHelpers
def gitlab_enable_admin_mode_sign_in(user)
visit new_admin_session_path
-
fill_in 'user_password', with: user.password
-
click_button 'Enter Admin Mode'
end
diff --git a/spec/support/helpers/query_recorder.rb b/spec/support/helpers/query_recorder.rb
index fd200a1abf3..61634813a1c 100644
--- a/spec/support/helpers/query_recorder.rb
+++ b/spec/support/helpers/query_recorder.rb
@@ -19,7 +19,9 @@ module ActiveRecord
def show_backtrace(values)
Rails.logger.debug("QueryRecorder SQL: #{values[:sql]}")
- Gitlab::BacktraceCleaner.clean_backtrace(caller).each { |line| Rails.logger.debug(" --> #{line}") }
+ Gitlab::BacktraceCleaner.clean_backtrace(caller).each do |line|
+ Rails.logger.debug("QueryRecorder backtrace: --> #{line}")
+ end
end
def get_sql_source(sql)
diff --git a/spec/support/helpers/reactive_caching_helpers.rb b/spec/support/helpers/reactive_caching_helpers.rb
index aa9d3b3a199..0b0b0622696 100644
--- a/spec/support/helpers/reactive_caching_helpers.rb
+++ b/spec/support/helpers/reactive_caching_helpers.rb
@@ -10,8 +10,11 @@ module ReactiveCachingHelpers
end
def stub_reactive_cache(subject = nil, data = nil, *qualifiers)
- allow(ReactiveCachingWorker).to receive(:perform_async)
- allow(ReactiveCachingWorker).to receive(:perform_in)
+ ReactiveCaching::WORK_TYPE.values.each do |worker|
+ allow(worker).to receive(:perform_async)
+ allow(worker).to receive(:perform_in)
+ end
+
write_reactive_cache(subject, data, *qualifiers) unless subject.nil?
end
@@ -42,8 +45,8 @@ module ReactiveCachingHelpers
Rails.cache.write(alive_reactive_cache_key(subject, *qualifiers), true)
end
- def expect_reactive_cache_update_queued(subject)
- expect(ReactiveCachingWorker)
+ def expect_reactive_cache_update_queued(subject, worker_klass: ReactiveCachingWorker)
+ expect(worker_klass)
.to receive(:perform_in)
.with(subject.class.reactive_cache_refresh_interval, subject.class, subject.id)
end
diff --git a/spec/support/helpers/smime_helper.rb b/spec/support/helpers/smime_helper.rb
index 96da3d81708..261aef9518e 100644
--- a/spec/support/helpers/smime_helper.rb
+++ b/spec/support/helpers/smime_helper.rb
@@ -5,20 +5,24 @@ module SmimeHelper
SHORT_EXPIRY = 30.minutes
def generate_root
- issue(signed_by: nil, expires_in: INFINITE_EXPIRY, certificate_authority: true)
+ issue(cn: 'RootCA', signed_by: nil, expires_in: INFINITE_EXPIRY, certificate_authority: true)
end
- def generate_cert(root_ca:, expires_in: SHORT_EXPIRY)
- issue(signed_by: root_ca, expires_in: expires_in, certificate_authority: false)
+ def generate_intermediate(signer_ca:)
+ issue(cn: 'IntermediateCA', signed_by: signer_ca, expires_in: INFINITE_EXPIRY, certificate_authority: true)
+ end
+
+ def generate_cert(signer_ca:, expires_in: SHORT_EXPIRY)
+ issue(signed_by: signer_ca, expires_in: expires_in, certificate_authority: false)
end
# returns a hash { key:, cert: } containing a generated key, cert pair
- def issue(email_address: 'test@example.com', signed_by:, expires_in:, certificate_authority:)
+ def issue(email_address: 'test@example.com', cn: nil, signed_by:, expires_in:, certificate_authority:)
key = OpenSSL::PKey::RSA.new(4096)
public_key = key.public_key
subject = if certificate_authority
- OpenSSL::X509::Name.parse("/CN=EU")
+ OpenSSL::X509::Name.parse("/CN=#{cn}")
else
OpenSSL::X509::Name.parse("/CN=#{email_address}")
end
diff --git a/spec/support/helpers/stub_feature_flags.rb b/spec/support/helpers/stub_feature_flags.rb
index 6c3efff7262..5b8a85b206f 100644
--- a/spec/support/helpers/stub_feature_flags.rb
+++ b/spec/support/helpers/stub_feature_flags.rb
@@ -9,23 +9,27 @@ module StubFeatureFlags
# Examples
# - `stub_feature_flags(ci_live_trace: false)` ... Disable `ci_live_trace`
# feature flag globally.
- # - `stub_feature_flags(ci_live_trace: { enabled: false, thing: project })` ...
- # Disable `ci_live_trace` feature flag on the specified project.
+ # - `stub_feature_flags(ci_live_trace: project)` ...
+ # - `stub_feature_flags(ci_live_trace: [project1, project2])` ...
+ # Enable `ci_live_trace` feature flag only on the specified projects.
def stub_feature_flags(features)
- features.each do |feature_name, option|
- if option.is_a?(Hash)
- enabled, thing = option.values_at(:enabled, :thing)
- else
- enabled = option
- thing = nil
- end
+ 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)
+
+ Array(actors).each do |actor|
+ raise ArgumentError, "actor cannot be Hash" if actor.is_a?(Hash)
- if thing
- allow(Feature).to receive(:enabled?).with(feature_name, thing, any_args) { enabled }
- allow(Feature).to receive(:enabled?).with(feature_name.to_s, thing, any_args) { enabled }
- else
- allow(Feature).to receive(:enabled?).with(feature_name, any_args) { enabled }
- allow(Feature).to receive(:enabled?).with(feature_name.to_s, any_args) { enabled }
+ 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)
+ else
+ raise ArgumentError, "#stub_feature_flags accepts only `nil`, `true`, `false`, `ActiveRecord::Base` or `Symbol` as actors"
+ end
end
end
end
diff --git a/spec/support/helpers/stub_gitlab_calls.rb b/spec/support/helpers/stub_gitlab_calls.rb
index 40f4151c0fb..120d432655b 100644
--- a/spec/support/helpers/stub_gitlab_calls.rb
+++ b/spec/support/helpers/stub_gitlab_calls.rb
@@ -86,7 +86,7 @@ module StubGitlabCalls
def stub_container_registry_tag_manifest_content
fixture_path = 'spec/fixtures/container_registry/tag_manifest.json'
- JSON.parse(File.read(Rails.root + fixture_path))
+ Gitlab::Json.parse(File.read(Rails.root + fixture_path))
end
def stub_container_registry_blob_content
@@ -113,12 +113,12 @@ module StubGitlabCalls
def stub_project_8
data = File.read(Rails.root.join('spec/support/gitlab_stubs/project_8.json'))
- allow_any_instance_of(Network).to receive(:project).and_return(JSON.parse(data))
+ allow_any_instance_of(Network).to receive(:project).and_return(Gitlab::Json.parse(data))
end
def stub_project_8_hooks
data = File.read(Rails.root.join('spec/support/gitlab_stubs/project_8_hooks.json'))
- allow_any_instance_of(Network).to receive(:project_hooks).and_return(JSON.parse(data))
+ allow_any_instance_of(Network).to receive(:project_hooks).and_return(Gitlab::Json.parse(data))
end
def stub_projects
@@ -143,7 +143,7 @@ module StubGitlabCalls
def project_hash_array
f = File.read(Rails.root.join('spec/support/gitlab_stubs/projects.json'))
- JSON.parse f
+ Gitlab::Json.parse(f)
end
end
diff --git a/spec/support/helpers/stub_object_storage.rb b/spec/support/helpers/stub_object_storage.rb
index d4ac286e959..b473cdaefc1 100644
--- a/spec/support/helpers/stub_object_storage.rb
+++ b/spec/support/helpers/stub_object_storage.rb
@@ -45,7 +45,7 @@ module StubObjectStorage
def stub_external_diffs_object_storage(uploader = described_class, **params)
stub_object_storage_uploader(config: Gitlab.config.external_diffs.object_store,
uploader: uploader,
- remote_directory: 'external_diffs',
+ remote_directory: 'external-diffs',
**params)
end
diff --git a/spec/support/helpers/test_env.rb b/spec/support/helpers/test_env.rb
index 47d69ca1f6a..130650b7e2e 100644
--- a/spec/support/helpers/test_env.rb
+++ b/spec/support/helpers/test_env.rb
@@ -1,7 +1,5 @@
# frozen_string_literal: true
-require 'rspec/mocks'
-
module TestEnv
extend ActiveSupport::Concern
extend self
@@ -61,7 +59,7 @@ module TestEnv
'merge-commit-analyze-side-branch' => '8a99451',
'merge-commit-analyze-after' => '646ece5',
'snippet/single-file' => '43e4080aaa14fc7d4b77ee1f5c9d067d5a7df10e',
- 'snippet/multiple-files' => 'b80faa8c5b2b62f6489a0d84755580e927e1189b',
+ 'snippet/multiple-files' => '40232f7eb98b3f221886432def6e8bab2432add9',
'snippet/rename-and-edit-file' => '220a1e4b4dff37feea0625a7947a4c60fbe78365',
'snippet/edit-file' => 'c2f074f4f26929c92795a75775af79a6ed6d8430',
'snippet/no-files' => '671aaa842a4875e5f30082d1ab6feda345fdb94d',
@@ -284,29 +282,33 @@ module TestEnv
end
def setup_factory_repo
- setup_repo(factory_repo_path, factory_repo_path_bare, factory_repo_name,
- BRANCH_SHA)
+ setup_repo(factory_repo_path, factory_repo_path_bare, factory_repo_name, BRANCH_SHA)
end
# This repo has a submodule commit that is not present in the main test
# repository.
def setup_forked_repo
- setup_repo(forked_repo_path, forked_repo_path_bare, forked_repo_name,
- FORKED_BRANCH_SHA)
+ setup_repo(forked_repo_path, forked_repo_path_bare, forked_repo_name, FORKED_BRANCH_SHA)
end
def setup_repo(repo_path, repo_path_bare, repo_name, refs)
clone_url = "https://gitlab.com/gitlab-org/#{repo_name}.git"
unless File.directory?(repo_path)
- system(*%W(#{Gitlab.config.git.bin_path} clone -q #{clone_url} #{repo_path}))
+ puts "\n==> Setting up #{repo_name} repository in #{repo_path}..."
+ start = Time.now
+ system(*%W(#{Gitlab.config.git.bin_path} clone --quiet -- #{clone_url} #{repo_path}))
+ puts " #{repo_path} set up in #{Time.now - start} seconds...\n"
end
set_repo_refs(repo_path, refs)
unless File.directory?(repo_path_bare)
+ puts "\n==> Setting up #{repo_name} bare repository in #{repo_path_bare}..."
+ start = Time.now
# We must copy bare repositories because we will push to them.
- system(git_env, *%W(#{Gitlab.config.git.bin_path} clone -q --bare #{repo_path} #{repo_path_bare}))
+ system(git_env, *%W(#{Gitlab.config.git.bin_path} clone --quiet --bare -- #{repo_path} #{repo_path_bare}))
+ puts " #{repo_path_bare} set up in #{Time.now - start} seconds...\n"
end
end
diff --git a/spec/support/helpers/usage_data_helpers.rb b/spec/support/helpers/usage_data_helpers.rb
index 1f1e686fb21..382e4f6a1a4 100644
--- a/spec/support/helpers/usage_data_helpers.rb
+++ b/spec/support/helpers/usage_data_helpers.rb
@@ -19,6 +19,9 @@ module UsageDataHelpers
cycle_analytics_views
productivity_analytics_views
source_code_pushes
+ design_management_designs_create
+ design_management_designs_update
+ design_management_designs_delete
).freeze
COUNTS_KEYS = %i(
@@ -96,6 +99,24 @@ module UsageDataHelpers
projects_with_error_tracking_enabled
projects_with_alerts_service_enabled
projects_with_prometheus_alerts
+ projects_with_expiration_policy_enabled
+ projects_with_expiration_policy_disabled
+ projects_with_expiration_policy_enabled_with_keep_n_unset
+ projects_with_expiration_policy_enabled_with_keep_n_set_to_1
+ projects_with_expiration_policy_enabled_with_keep_n_set_to_5
+ projects_with_expiration_policy_enabled_with_keep_n_set_to_10
+ projects_with_expiration_policy_enabled_with_keep_n_set_to_25
+ projects_with_expiration_policy_enabled_with_keep_n_set_to_50
+ projects_with_expiration_policy_enabled_with_older_than_unset
+ projects_with_expiration_policy_enabled_with_older_than_set_to_7d
+ projects_with_expiration_policy_enabled_with_older_than_set_to_14d
+ projects_with_expiration_policy_enabled_with_older_than_set_to_30d
+ projects_with_expiration_policy_enabled_with_older_than_set_to_90d
+ projects_with_expiration_policy_enabled_with_cadence_set_to_1d
+ projects_with_expiration_policy_enabled_with_cadence_set_to_7d
+ 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
pages_domains
protected_branches
releases
@@ -130,28 +151,62 @@ module UsageDataHelpers
gitaly
database
avg_cycle_analytics
- influxdb_metrics_enabled
prometheus_metrics_enabled
web_ide_clientside_preview_enabled
ingress_modsecurity_enabled
- projects_with_expiration_policy_disabled
- projects_with_expiration_policy_enabled
- projects_with_expiration_policy_enabled_with_keep_n_unset
- projects_with_expiration_policy_enabled_with_older_than_unset
- projects_with_expiration_policy_enabled_with_keep_n_set_to_1
- projects_with_expiration_policy_enabled_with_keep_n_set_to_5
- projects_with_expiration_policy_enabled_with_keep_n_set_to_10
- projects_with_expiration_policy_enabled_with_keep_n_set_to_25
- projects_with_expiration_policy_enabled_with_keep_n_set_to_50
- projects_with_expiration_policy_enabled_with_keep_n_set_to_100
- projects_with_expiration_policy_enabled_with_cadence_set_to_1d
- projects_with_expiration_policy_enabled_with_cadence_set_to_7d
- 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_expiration_policy_enabled_with_older_than_set_to_7d
- projects_with_expiration_policy_enabled_with_older_than_set_to_14d
- projects_with_expiration_policy_enabled_with_older_than_set_to_30d
- projects_with_expiration_policy_enabled_with_older_than_set_to_90d
+ object_store
).freeze
+
+ def stub_object_store_settings
+ allow(Settings).to receive(:[]).with('artifacts')
+ .and_return(
+ { 'enabled' => true,
+ 'object_store' =>
+ { 'enabled' => true,
+ 'remote_directory' => 'artifacts',
+ 'direct_upload' => true,
+ 'connection' =>
+ { 'provider' => 'AWS', 'aws_access_key_id' => 'minio', 'aws_secret_access_key' => 'gdk-minio', 'region' => 'gdk', 'endpoint' => 'http://127.0.0.1:9000', 'path_style' => true },
+ 'background_upload' => false,
+ 'proxy_download' => false } }
+ )
+
+ allow(Settings).to receive(:[]).with('external_diffs').and_return({ 'enabled' => false })
+
+ allow(Settings).to receive(:[]).with('lfs')
+ .and_return(
+ { 'enabled' => true,
+ 'object_store' =>
+ { 'enabled' => false,
+ 'remote_directory' => 'lfs-objects',
+ 'direct_upload' => true,
+ 'connection' =>
+ { 'provider' => 'AWS', 'aws_access_key_id' => 'minio', 'aws_secret_access_key' => 'gdk-minio', 'region' => 'gdk', 'endpoint' => 'http://127.0.0.1:9000', 'path_style' => true },
+ 'background_upload' => false,
+ 'proxy_download' => false } }
+ )
+ allow(Settings).to receive(:[]).with('uploads')
+ .and_return(
+ { 'object_store' =>
+ { 'enabled' => false,
+ 'remote_directory' => 'uploads',
+ 'direct_upload' => true,
+ 'connection' =>
+ { 'provider' => 'AWS', 'aws_access_key_id' => 'minio', 'aws_secret_access_key' => 'gdk-minio', 'region' => 'gdk', 'endpoint' => 'http://127.0.0.1:9000', 'path_style' => true },
+ 'background_upload' => false,
+ 'proxy_download' => false } }
+ )
+ allow(Settings).to receive(:[]).with('packages')
+ .and_return(
+ { 'enabled' => true,
+ 'object_store' =>
+ { 'enabled' => false,
+ 'remote_directory' => 'packages',
+ 'direct_upload' => false,
+ 'connection' =>
+ { 'provider' => 'AWS', 'aws_access_key_id' => 'minio', 'aws_secret_access_key' => 'gdk-minio', 'region' => 'gdk', 'endpoint' => 'http://127.0.0.1:9000', 'path_style' => true },
+ 'background_upload' => true,
+ 'proxy_download' => false } }
+ )
+ end
end
diff --git a/spec/support/helpers/wiki_helpers.rb b/spec/support/helpers/wiki_helpers.rb
index 86eb1793707..e6818ff8f0c 100644
--- a/spec/support/helpers/wiki_helpers.rb
+++ b/spec/support/helpers/wiki_helpers.rb
@@ -14,7 +14,10 @@ module WikiHelpers
file_content: File.read(expand_fixture_path(file_name))
}
- ::Wikis::CreateAttachmentService.new(project, user, opts)
- .execute[:result][:file_path]
+ ::Wikis::CreateAttachmentService.new(
+ container: project,
+ current_user: user,
+ params: opts
+ ).execute[:result][:file_path]
end
end
diff --git a/spec/support/helpers/workhorse_helpers.rb b/spec/support/helpers/workhorse_helpers.rb
index 53b36b3dd45..f16b6c1e910 100644
--- a/spec/support/helpers/workhorse_helpers.rb
+++ b/spec/support/helpers/workhorse_helpers.rb
@@ -11,7 +11,7 @@ module WorkhorseHelpers
header = split_header.join(':')
[
type,
- JSON.parse(Base64.urlsafe_decode64(header))
+ Gitlab::Json.parse(Base64.urlsafe_decode64(header))
]
end
end
diff --git a/spec/support/helpers/x509_helpers.rb b/spec/support/helpers/x509_helpers.rb
index 9ea997bf5f4..ce0fa268ace 100644
--- a/spec/support/helpers/x509_helpers.rb
+++ b/spec/support/helpers/x509_helpers.rb
@@ -173,22 +173,155 @@ module X509Helpers
Time.at(1561027326)
end
+ def signed_tag_signature
+ <<~SIGNATURE
+ -----BEGIN SIGNED MESSAGE-----
+ MIISfwYJKoZIhvcNAQcCoIIScDCCEmwCAQExDTALBglghkgBZQMEAgEwCwYJKoZI
+ hvcNAQcBoIIP8zCCB3QwggVcoAMCAQICBBXXLOIwDQYJKoZIhvcNAQELBQAwgbYx
+ CzAJBgNVBAYTAkRFMQ8wDQYDVQQIDAZCYXllcm4xETAPBgNVBAcMCE11ZW5jaGVu
+ MRAwDgYDVQQKDAdTaWVtZW5zMREwDwYDVQQFEwhaWlpaWlpBNjEdMBsGA1UECwwU
+ U2llbWVucyBUcnVzdCBDZW50ZXIxPzA9BgNVBAMMNlNpZW1lbnMgSXNzdWluZyBD
+ QSBNZWRpdW0gU3RyZW5ndGggQXV0aGVudGljYXRpb24gMjAxNjAeFw0xNzAyMDMw
+ NjU4MzNaFw0yMDAyMDMwNjU4MzNaMFsxETAPBgNVBAUTCFowMDBOV0RIMQ4wDAYD
+ VQQqDAVSb2dlcjEOMAwGA1UEBAwFTWVpZXIxEDAOBgNVBAoMB1NpZW1lbnMxFDAS
+ BgNVBAMMC01laWVyIFJvZ2VyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
+ AQEAuBNea/68ZCnHYQjpm/k3ZBG0wBpEKSwG6lk9CEQlSxsqVLQHAoAKBIlJm1in
+ YVLcK/Sq1yhYJ/qWcY/M53DhK2rpPuhtrWJUdOUy8EBWO20F4bd4Fw9pO7jt8bme
+ u33TSrK772vKjuppzB6SeG13Cs08H+BIeD106G27h7ufsO00pvsxoSDL+uc4slnr
+ pBL+2TAL7nSFnB9QHWmRIK27SPqJE+lESdb0pse11x1wjvqKy2Q7EjL9fpqJdHzX
+ NLKHXd2r024TOORTa05DFTNR+kQEKKV96XfpYdtSBomXNQ44cisiPBJjFtYvfnFE
+ wgrHa8fogn/b0C+A+HAoICN12wIDAQABo4IC4jCCAt4wHQYDVR0OBBYEFCF+gkUp
+ XQ6xGc0kRWXuDFxzA14zMEMGA1UdEQQ8MDqgIwYKKwYBBAGCNxQCA6AVDBNyLm1l
+ aWVyQHNpZW1lbnMuY29tgRNyLm1laWVyQHNpZW1lbnMuY29tMA4GA1UdDwEB/wQE
+ AwIHgDAdBgNVHSUEFjAUBggrBgEFBQcDAgYIKwYBBQUHAwQwgcoGA1UdHwSBwjCB
+ vzCBvKCBuaCBtoYmaHR0cDovL2NoLnNpZW1lbnMuY29tL3BraT9aWlpaWlpBNi5j
+ cmyGQWxkYXA6Ly9jbC5zaWVtZW5zLm5ldC9DTj1aWlpaWlpBNixMPVBLST9jZXJ0
+ aWZpY2F0ZVJldm9jYXRpb25MaXN0hklsZGFwOi8vY2wuc2llbWVucy5jb20vQ049
+ WlpaWlpaQTYsbz1UcnVzdGNlbnRlcj9jZXJ0aWZpY2F0ZVJldm9jYXRpb25MaXN0
+ MEUGA1UdIAQ+MDwwOgYNKwYBBAGhaQcCAgMBAzApMCcGCCsGAQUFBwIBFhtodHRw
+ Oi8vd3d3LnNpZW1lbnMuY29tL3BraS8wDAYDVR0TAQH/BAIwADAfBgNVHSMEGDAW
+ gBT4FV1HDGx3e3LEAheRaKK292oJRDCCAQQGCCsGAQUFBwEBBIH3MIH0MDIGCCsG
+ AQUFBzAChiZodHRwOi8vYWguc2llbWVucy5jb20vcGtpP1paWlpaWkE2LmNydDBB
+ BggrBgEFBQcwAoY1bGRhcDovL2FsLnNpZW1lbnMubmV0L0NOPVpaWlpaWkE2LEw9
+ UEtJP2NBQ2VydGlmaWNhdGUwSQYIKwYBBQUHMAKGPWxkYXA6Ly9hbC5zaWVtZW5z
+ LmNvbS9DTj1aWlpaWlpBNixvPVRydXN0Y2VudGVyP2NBQ2VydGlmaWNhdGUwMAYI
+ KwYBBQUHMAGGJGh0dHA6Ly9vY3NwLnBraS1zZXJ2aWNlcy5zaWVtZW5zLmNvbTAN
+ BgkqhkiG9w0BAQsFAAOCAgEAXPVcX6vaEcszJqg5IemF9aFTlwTrX5ITNIpzcqG+
+ kD5haOf2mZYLjl+MKtLC1XfmIsGCUZNb8bjP6QHQEI+2d6x/ZOqPq7Kd7PwVu6x6
+ xZrkDjUyhUbUntT5+RBy++l3Wf6Cq6Kx+K8ambHBP/bu90/p2U8KfFAG3Kr2gI2q
+ fZrnNMOxmJfZ3/sXxssgLkhbZ7hRa+MpLfQ6uFsSiat3vlawBBvTyHnoZ/7oRc8y
+ qi6QzWcd76CPpMElYWibl+hJzKbBZUWvc71AzHR6i1QeZ6wubYz7vr+FF5Y7tnxB
+ Vz6omPC9XAg0F+Dla6Zlz3Awj5imCzVXa+9SjtnsidmJdLcKzTAKyDewewoxYOOJ
+ j3cJU7VSjJPl+2fVmDBaQwcNcUcu/TPAKApkegqO7tRF9IPhjhW8QkRnkqMetO3D
+ OXmAFVIsEI0Hvb2cdb7B6jSpjGUuhaFm9TCKhQtCk2p8JCDTuaENLm1x34rrJKbT
+ 2vzyYN0CZtSkUdgD4yQxK9VWXGEzexRisWb4AnZjD2NAquLPpXmw8N0UwFD7MSpC
+ dpaX7FktdvZmMXsnGiAdtLSbBgLVWOD1gmJFDjrhNbI8NOaOaNk4jrfGqNh5lhGU
+ 4DnBT2U6Cie1anLmFH/oZooAEXR2o3Nu+1mNDJChnJp0ovs08aa3zZvBdcloOvfU
+ qdowggh3MIIGX6ADAgECAgQtyi/nMA0GCSqGSIb3DQEBCwUAMIGZMQswCQYDVQQG
+ EwJERTEPMA0GA1UECAwGQmF5ZXJuMREwDwYDVQQHDAhNdWVuY2hlbjEQMA4GA1UE
+ CgwHU2llbWVuczERMA8GA1UEBRMIWlpaWlpaQTExHTAbBgNVBAsMFFNpZW1lbnMg
+ VHJ1c3QgQ2VudGVyMSIwIAYDVQQDDBlTaWVtZW5zIFJvb3QgQ0EgVjMuMCAyMDE2
+ MB4XDTE2MDcyMDEzNDYxMFoXDTIyMDcyMDEzNDYxMFowgbYxCzAJBgNVBAYTAkRF
+ MQ8wDQYDVQQIDAZCYXllcm4xETAPBgNVBAcMCE11ZW5jaGVuMRAwDgYDVQQKDAdT
+ aWVtZW5zMREwDwYDVQQFEwhaWlpaWlpBNjEdMBsGA1UECwwUU2llbWVucyBUcnVz
+ dCBDZW50ZXIxPzA9BgNVBAMMNlNpZW1lbnMgSXNzdWluZyBDQSBNZWRpdW0gU3Ry
+ ZW5ndGggQXV0aGVudGljYXRpb24gMjAxNjCCAiIwDQYJKoZIhvcNAQEBBQADggIP
+ ADCCAgoCggIBAL9UfK+JAZEqVMVvECdYF9IK4KSw34AqyNl3rYP5x03dtmKaNu+2
+ 0fQqNESA1NGzw3s6LmrKLh1cR991nB2cvKOXu7AvEGpSuxzIcOROd4NpvRx+Ej1p
+ JIPeqf+ScmVK7lMSO8QL/QzjHOpGV3is9sG+ZIxOW9U1ESooy4Hal6ZNs4DNItsz
+ piCKqm6G3et4r2WqCy2RRuSqvnmMza7Y8BZsLy0ZVo5teObQ37E/FxqSrbDI8nxn
+ B7nVUve5ZjrqoIGSkEOtyo11003dVO1vmWB9A0WQGDqE/q3w178hGhKfxzRaqzyi
+ SoADUYS2sD/CglGTUxVq6u0pGLLsCFjItcCWqW+T9fPYfJ2CEd5b3hvqdCn+pXjZ
+ /gdX1XAcdUF5lRnGWifaYpT9n4s4adzX8q6oHSJxTppuAwLRKH6eXALbGQ1I9lGQ
+ DSOipD/09xkEsPw6HOepmf2U3YxZK1VU2sHqugFJboeLcHMzp6E1n2ctlNG1GKE9
+ FDHmdyFzDi0Nnxtf/GgVjnHF68hByEE1MYdJ4nJLuxoT9hyjYdRW9MpeNNxxZnmz
+ W3zh7QxIqP0ZfIz6XVhzrI9uZiqwwojDiM5tEOUkQ7XyW6grNXe75yt6mTj89LlB
+ H5fOW2RNmCy/jzBXDjgyskgK7kuCvUYTuRv8ITXbBY5axFA+CpxZqokpAgMBAAGj
+ ggKmMIICojCCAQUGCCsGAQUFBwEBBIH4MIH1MEEGCCsGAQUFBzAChjVsZGFwOi8v
+ YWwuc2llbWVucy5uZXQvQ049WlpaWlpaQTEsTD1QS0k/Y0FDZXJ0aWZpY2F0ZTAy
+ BggrBgEFBQcwAoYmaHR0cDovL2FoLnNpZW1lbnMuY29tL3BraT9aWlpaWlpBMS5j
+ cnQwSgYIKwYBBQUHMAKGPmxkYXA6Ly9hbC5zaWVtZW5zLmNvbS91aWQ9WlpaWlpa
+ QTEsbz1UcnVzdGNlbnRlcj9jQUNlcnRpZmljYXRlMDAGCCsGAQUFBzABhiRodHRw
+ Oi8vb2NzcC5wa2ktc2VydmljZXMuc2llbWVucy5jb20wHwYDVR0jBBgwFoAUcG2g
+ UOyp0CxnnRkV/v0EczXD4tQwEgYDVR0TAQH/BAgwBgEB/wIBADBABgNVHSAEOTA3
+ MDUGCCsGAQQBoWkHMCkwJwYIKwYBBQUHAgEWG2h0dHA6Ly93d3cuc2llbWVucy5j
+ b20vcGtpLzCBxwYDVR0fBIG/MIG8MIG5oIG2oIGzhj9sZGFwOi8vY2wuc2llbWVu
+ cy5uZXQvQ049WlpaWlpaQTEsTD1QS0k/YXV0aG9yaXR5UmV2b2NhdGlvbkxpc3SG
+ Jmh0dHA6Ly9jaC5zaWVtZW5zLmNvbS9wa2k/WlpaWlpaQTEuY3JshkhsZGFwOi8v
+ Y2wuc2llbWVucy5jb20vdWlkPVpaWlpaWkExLG89VHJ1c3RjZW50ZXI/YXV0aG9y
+ aXR5UmV2b2NhdGlvbkxpc3QwJwYDVR0lBCAwHgYIKwYBBQUHAwIGCCsGAQUFBwME
+ BggrBgEFBQcDCTAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFPgVXUcMbHd7csQC
+ F5Foorb3aglEMA0GCSqGSIb3DQEBCwUAA4ICAQBw+sqMp3SS7DVKcILEmXbdRAg3
+ lLO1r457KY+YgCT9uX4VG5EdRKcGfWXK6VHGCi4Dos5eXFV34Mq/p8nu1sqMuoGP
+ YjHn604eWDprhGy6GrTYdxzcE/GGHkpkuE3Ir/45UcmZlOU41SJ9SNjuIVrSHMOf
+ ccSY42BCspR/Q1Z/ykmIqQecdT3/Kkx02GzzSN2+HlW6cEO4GBW5RMqsvd2n0h2d
+ fe2zcqOgkLtx7u2JCR/U77zfyxG3qXtcymoz0wgSHcsKIl+GUjITLkHfS9Op8V7C
+ Gr/dX437sIg5pVHmEAWadjkIzqdHux+EF94Z6kaHywohc1xG0KvPYPX7iSNjkvhz
+ 4NY53DHmxl4YEMLffZnaS/dqyhe1GTpcpyN8WiR4KuPfxrkVDOsuzWFtMSvNdlOV
+ gdI0MXcLMP+EOeANZWX6lGgJ3vWyemo58nzgshKd24MY3w3i6masUkxJH2KvI7UH
+ /1Db3SC8oOUjInvSRej6M3ZhYWgugm6gbpUgFoDw/o9Cg6Qm71hY0JtcaPC13rzm
+ N8a2Br0+Fa5e2VhwLmAxyfe1JKzqPwuHT0S5u05SQghL5VdzqfA8FCL/j4XC9yI6
+ csZTAQi73xFQYVjZt3+aoSz84lOlTmVo/jgvGMY/JzH9I4mETGgAJRNj34Z/0meh
+ M+pKWCojNH/dgyJSwDGCAlIwggJOAgEBMIG/MIG2MQswCQYDVQQGEwJERTEPMA0G
+ A1UECAwGQmF5ZXJuMREwDwYDVQQHDAhNdWVuY2hlbjEQMA4GA1UECgwHU2llbWVu
+ czERMA8GA1UEBRMIWlpaWlpaQTYxHTAbBgNVBAsMFFNpZW1lbnMgVHJ1c3QgQ2Vu
+ dGVyMT8wPQYDVQQDDDZTaWVtZW5zIElzc3VpbmcgQ0EgTWVkaXVtIFN0cmVuZ3Ro
+ IEF1dGhlbnRpY2F0aW9uIDIwMTYCBBXXLOIwCwYJYIZIAWUDBAIBoGkwHAYJKoZI
+ hvcNAQkFMQ8XDTE5MTEyMDE0NTYyMFowLwYJKoZIhvcNAQkEMSIEIJDnZUpcVLzC
+ OdtpkH8gtxwLPIDE0NmAmFC9uM8q2z+OMBgGCSqGSIb3DQEJAzELBgkqhkiG9w0B
+ BwEwCwYJKoZIhvcNAQEBBIIBAH/Pqv2xp3a0jSPkwU1K3eGA/1lfoNJMUny4d/PS
+ LVWlkgrmedXdLmuBzAGEaaZOJS0lEpNd01pR/reHs7xxZ+RZ0olTs2ufM0CijQSx
+ OL9HDl2O3OoD77NWx4tl3Wy1yJCeV3XH/cEI7AkKHCmKY9QMoMYWh16ORBtr+YcS
+ YK+gONOjpjgcgTJgZ3HSFgQ50xiD4WT1kFBHsuYsLqaOSbTfTN6Ayyg4edjrPQqa
+ VcVf1OQcIrfWA3yMQrnEZfOYfN/D4EPjTfxBV+VCi/F2bdZmMbJ7jNk1FbewSwWO
+ SDH1i0K32NyFbnh0BSos7njq7ELqKlYBsoB/sZfaH2vKy5U=
+ -----END SIGNED MESSAGE-----
+ SIGNATURE
+ end
+
+ def signed_tag_base_data
+ <<~SIGNEDDATA
+ object 189a6c924013fc3fe40d6f1ec1dc20214183bc97
+ type commit
+ tag v1.1.1
+ tagger Roger Meier <r.meier@siemens.com> 1574261780 +0100
+
+ x509 signed tag
+ SIGNEDDATA
+ end
+
def certificate_crl
'http://ch.siemens.com/pki?ZZZZZZA2.crl'
end
+ def tag_certificate_crl
+ 'http://ch.siemens.com/pki?ZZZZZZA6.crl'
+ end
+
def certificate_serial
1810356222
end
+ def tag_certificate_serial
+ 3664232660
+ end
+
def certificate_subject_key_identifier
'EC:00:B5:28:02:5C:D3:A5:A1:AB:C2:A1:34:81:84:AA:BF:9B:CF:F8'
end
+ def tag_certificate_subject_key_identifier
+ '21:7E:82:45:29:5D:0E:B1:19:CD:24:45:65:EE:0C:5C:73:03:5E:33'
+ end
+
def issuer_subject_key_identifier
'BD:BD:2A:43:22:3D:48:4A:57:7E:98:31:17:A9:70:9D:EE:9F:A8:99'
end
+ def tag_issuer_subject_key_identifier
+ 'F8:15:5D:47:0C:6C:77:7B:72:C4:02:17:91:68:A2:B6:F7:6A:09:44'
+ end
+
def certificate_email
'r.meier@siemens.com'
end
@@ -197,6 +330,10 @@ module X509Helpers
'CN=Siemens Issuing CA EE Auth 2016,OU=Siemens Trust Center,serialNumber=ZZZZZZA2,O=Siemens,L=Muenchen,ST=Bayern,C=DE'
end
+ def tag_certificate_issuer
+ 'CN=Siemens Issuing CA Medium Strength Authentication 2016,OU=Siemens Trust Center,serialNumber=ZZZZZZA6,O=Siemens,L=Muenchen,ST=Bayern,C=DE'
+ end
+
def certificate_subject
'CN=Meier Roger,O=Siemens,SN=Meier,GN=Roger,serialNumber=Z000NWDH'
end
diff --git a/spec/support/import_export/common_util.rb b/spec/support/import_export/common_util.rb
index 1a5668946c6..0069ae81b76 100644
--- a/spec/support/import_export/common_util.rb
+++ b/spec/support/import_export/common_util.rb
@@ -15,28 +15,9 @@ module ImportExport
export_path = [prefix, 'spec', 'fixtures', 'lib', 'gitlab', 'import_export', name].compact
export_path = File.join(*export_path)
- if File.exist?(File.join(export_path, 'tree.tar.gz'))
- extract_archive(export_path, 'tree.tar.gz')
- end
-
allow_any_instance_of(Gitlab::ImportExport).to receive(:export_path) { export_path }
end
- def extract_archive(path, archive)
- output, exit_status = Gitlab::Popen.popen(["cd #{path}; tar xzf #{archive}"])
-
- raise "Failed to extract archive. Output: #{output}" unless exit_status.zero?
- end
-
- def cleanup_artifacts_from_extract_archive(name, prefix = nil)
- export_path = [prefix, 'spec', 'fixtures', 'lib', 'gitlab', 'import_export', name].compact
- export_path = File.join(*export_path)
-
- if File.exist?(File.join(export_path, 'tree.tar.gz'))
- system("cd #{export_path}; rm -fr tree")
- end
- end
-
def setup_reader(reader)
if reader == :ndjson_reader && Feature.enabled?(:project_import_ndjson)
allow_any_instance_of(Gitlab::ImportExport::JSON::LegacyReader::File).to receive(:exist?).and_return(false)
diff --git a/spec/support/import_export/configuration_helper.rb b/spec/support/import_export/configuration_helper.rb
index 4330c4314a8..6f67b0f3dd7 100644
--- a/spec/support/import_export/configuration_helper.rb
+++ b/spec/support/import_export/configuration_helper.rb
@@ -44,8 +44,8 @@ module ConfigurationHelper
import_export_config = config_hash(config)
excluded_attributes = import_export_config[:excluded_attributes][relation_name.to_sym]
included_attributes = import_export_config[:included_attributes][relation_name.to_sym]
- attributes = attributes - JSON.parse(excluded_attributes.to_json) if excluded_attributes
- attributes = attributes & JSON.parse(included_attributes.to_json) if included_attributes
+ attributes = attributes - Gitlab::Json.parse(excluded_attributes.to_json) if excluded_attributes
+ attributes = attributes & Gitlab::Json.parse(included_attributes.to_json) if included_attributes
attributes
end
diff --git a/spec/support/kubeclient.rb b/spec/support/kubeclient.rb
new file mode 100644
index 00000000000..56c5800c801
--- /dev/null
+++ b/spec/support/kubeclient.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+RSpec.configure do |config|
+ # Feature specs call webmock_enable_with_http_connect_on_start! by
+ # default. This is needed to prevent Kubeclient from connecting to a
+ # host before the request is stubbed.
+ config.before(:each, :kubeclient) do
+ webmock_enable!
+ end
+end
diff --git a/spec/support/matchers/disallow_request_matchers.rb b/spec/support/matchers/disallow_request_matchers.rb
index a161e3660cd..cb6f4bedbd5 100644
--- a/spec/support/matchers/disallow_request_matchers.rb
+++ b/spec/support/matchers/disallow_request_matchers.rb
@@ -11,7 +11,7 @@ end
RSpec::Matchers.define :disallow_request_in_json do
match do |response|
- json_response = JSON.parse(response.body)
+ json_response = Gitlab::Json.parse(response.body)
response.body.include?('You cannot perform write operations') && json_response.key?('message')
end
end
diff --git a/spec/support/matchers/graphql_matchers.rb b/spec/support/matchers/graphql_matchers.rb
index 6439b68764e..3e2193a9069 100644
--- a/spec/support/matchers/graphql_matchers.rb
+++ b/spec/support/matchers/graphql_matchers.rb
@@ -1,8 +1,14 @@
# frozen_string_literal: true
RSpec::Matchers.define :require_graphql_authorizations do |*expected|
- match do |field|
- expect(field.to_graphql.metadata[:authorize]).to eq(*expected)
+ match do |klass|
+ permissions = if klass.respond_to?(:required_permissions)
+ klass.required_permissions
+ else
+ [klass.to_graphql.metadata[:authorize]]
+ end
+
+ expect(permissions).to eq(expected)
end
end
diff --git a/spec/support/rails/test_case_patch.rb b/spec/support/rails/test_case_patch.rb
deleted file mode 100644
index 161e1ef2a4c..00000000000
--- a/spec/support/rails/test_case_patch.rb
+++ /dev/null
@@ -1,53 +0,0 @@
-# frozen_string_literal: true
-#
-# This file pulls in the changes in https://github.com/rails/rails/pull/38063
-# to fix controller specs updated with the latest Rack versions.
-#
-# This file should be removed after that change ships. It is not
-# present in Rails 6.0.2.2.
-module ActionController
- class TestRequest < ActionDispatch::TestRequest #:nodoc:
- def self.new_session
- TestSessionPatched.new
- end
- end
-
- # Methods #destroy and #load! are overridden to avoid calling methods on the
- # @store object, which does not exist for the TestSession class.
- class TestSessionPatched < Rack::Session::Abstract::PersistedSecure::SecureSessionHash #:nodoc:
- DEFAULT_OPTIONS = Rack::Session::Abstract::Persisted::DEFAULT_OPTIONS
-
- def initialize(session = {})
- super(nil, nil)
- @id = Rack::Session::SessionId.new(SecureRandom.hex(16))
- @data = stringify_keys(session)
- @loaded = true
- end
-
- def exists?
- true
- end
-
- def keys
- @data.keys
- end
-
- def values
- @data.values
- end
-
- def destroy
- clear
- end
-
- def fetch(key, *args, &block)
- @data.fetch(key.to_s, *args, &block)
- end
-
- private
-
- def load!
- @id
- end
- end
-end
diff --git a/spec/support/redis/redis_shared_examples.rb b/spec/support/redis/redis_shared_examples.rb
index 1e2d11a66cb..f5f6a69738b 100644
--- a/spec/support/redis/redis_shared_examples.rb
+++ b/spec/support/redis/redis_shared_examples.rb
@@ -150,7 +150,7 @@ RSpec.shared_examples "redis_shared_examples" do
it 'returns an array of hashes with host and port keys' do
is_expected.to include(host: 'localhost', port: sentinel_port)
- is_expected.to include(host: 'slave2', port: sentinel_port)
+ is_expected.to include(host: 'replica2', port: sentinel_port)
end
end
diff --git a/spec/support/renameable_upload.rb b/spec/support/renameable_upload.rb
new file mode 100644
index 00000000000..f7f00181605
--- /dev/null
+++ b/spec/support/renameable_upload.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class RenameableUpload < SimpleDelegator
+ attr_accessor :original_filename
+
+ # Get a fixture file with a new unique name, and the same extension
+ def self.unique_file(name)
+ upload = new(fixture_file_upload("spec/fixtures/#{name}"))
+ ext = File.extname(name)
+ new_name = File.basename(FactoryBot.generate(:filename), '.*')
+ upload.original_filename = new_name + ext
+
+ upload
+ end
+end
diff --git a/spec/support/shared_contexts/cache_allowed_users_in_namespace_shared_context.rb b/spec/support/shared_contexts/cache_allowed_users_in_namespace_shared_context.rb
new file mode 100644
index 00000000000..04f49e94647
--- /dev/null
+++ b/spec/support/shared_contexts/cache_allowed_users_in_namespace_shared_context.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+shared_examples 'allowed user IDs are cached' do
+ it 'caches the allowed user IDs in cache', :use_clean_rails_memory_store_caching do
+ expect do
+ expect(described_class.l1_cache_backend).to receive(:fetch).and_call_original
+ expect(described_class.l2_cache_backend).not_to receive(:fetch)
+ expect(subject).to be_truthy
+ end.not_to exceed_query_limit(0)
+ end
+
+ it 'caches the allowed user IDs in L1 cache for 1 minute', :use_clean_rails_memory_store_caching do
+ Timecop.travel 2.minutes do
+ expect do
+ expect(described_class.l1_cache_backend).to receive(:fetch).and_call_original
+ expect(described_class.l2_cache_backend).to receive(:fetch).and_call_original
+ expect(subject).to be_truthy
+ end.not_to exceed_query_limit(0)
+ end
+ end
+
+ it 'caches the allowed user IDs in L2 cache for 5 minutes', :use_clean_rails_memory_store_caching do
+ Timecop.travel 6.minutes do
+ expect do
+ expect(described_class.l1_cache_backend).to receive(:fetch).and_call_original
+ expect(described_class.l2_cache_backend).to receive(:fetch).and_call_original
+ expect(subject).to be_truthy
+ end.not_to exceed_query_limit(2)
+ end
+ end
+end
diff --git a/spec/support/shared_contexts/design_management_shared_contexts.rb b/spec/support/shared_contexts/design_management_shared_contexts.rb
new file mode 100644
index 00000000000..2866effb3a8
--- /dev/null
+++ b/spec/support/shared_contexts/design_management_shared_contexts.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+shared_context 'four designs in three versions' do
+ include DesignManagementTestHelpers
+
+ let_it_be(:issue) { create(:issue) }
+ let_it_be(:project) { issue.project }
+ let_it_be(:authorized_user) { create(:user) }
+
+ let_it_be(:design_a) { create(:design, issue: issue) }
+ let_it_be(:design_b) { create(:design, issue: issue) }
+ let_it_be(:design_c) { create(:design, issue: issue) }
+ let_it_be(:design_d) { create(:design, issue: issue) }
+
+ let_it_be(:first_version) do
+ create(:design_version, issue: issue,
+ created_designs: [design_a],
+ modified_designs: [],
+ deleted_designs: [])
+ end
+ let_it_be(:second_version) do
+ create(:design_version, issue: issue,
+ created_designs: [design_b, design_c, design_d],
+ modified_designs: [design_a],
+ deleted_designs: [])
+ end
+ let_it_be(:third_version) do
+ create(:design_version, issue: issue,
+ created_designs: [],
+ modified_designs: [design_a],
+ deleted_designs: [design_d])
+ end
+
+ before do
+ enable_design_management
+ project.add_developer(authorized_user)
+ end
+end
diff --git a/spec/support/shared_contexts/features/error_tracking_shared_context.rb b/spec/support/shared_contexts/features/error_tracking_shared_context.rb
index cbd33dd109b..102cf7c9b11 100644
--- a/spec/support/shared_contexts/features/error_tracking_shared_context.rb
+++ b/spec/support/shared_contexts/features/error_tracking_shared_context.rb
@@ -6,9 +6,9 @@ shared_context 'sentry error tracking context feature' do
let_it_be(:project) { create(:project) }
let_it_be(:project_error_tracking_settings) { create(:project_error_tracking_setting, project: project) }
let_it_be(:issue_response_body) { fixture_file('sentry/issue_sample_response.json') }
- let_it_be(:issue_response) { JSON.parse(issue_response_body) }
+ let_it_be(:issue_response) { Gitlab::Json.parse(issue_response_body) }
let_it_be(:event_response_body) { fixture_file('sentry/issue_latest_event_sample_response.json') }
- let_it_be(:event_response) { JSON.parse(event_response_body) }
+ let_it_be(:event_response) { Gitlab::Json.parse(event_response_body) }
let(:sentry_api_urls) { Sentry::ApiUrls.new(project_error_tracking_settings.api_url) }
let(:issue_id) { issue_response['id'] }
let(:issue_seen) { 1.year.ago.utc }
diff --git a/spec/support/shared_contexts/issuable/merge_request_shared_context.rb b/spec/support/shared_contexts/issuable/merge_request_shared_context.rb
new file mode 100644
index 00000000000..05ffb934c34
--- /dev/null
+++ b/spec/support/shared_contexts/issuable/merge_request_shared_context.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+shared_context 'merge request show action' do
+ include Devise::Test::ControllerHelpers
+ include ProjectForksHelper
+
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :public, :repository) }
+ let(:forked_project) { fork_project(project, user, repository: true) }
+ let(:unlink_project) { Projects::UnlinkForkService.new(forked_project, user) }
+ let(:note) { create(:note_on_merge_request, project: project, noteable: closed_merge_request) }
+
+ let(:closed_merge_request) do
+ create(:closed_merge_request,
+ source_project: forked_project,
+ target_project: project,
+ author: user)
+ end
+
+ def preload_view_requirements
+ # This will load the status fields of the author of the note and merge request
+ # to avoid queries in when rendering the view being tested.
+ closed_merge_request.author.status
+ note.author.status
+ end
+
+ before do
+ assign(:project, project)
+ assign(:merge_request, closed_merge_request)
+ assign(:commits_count, 0)
+ assign(:note, note)
+ assign(:noteable, closed_merge_request)
+ assign(:notes, [])
+ assign(:pipelines, Ci::Pipeline.none)
+ assign(:issuable_sidebar, serialize_issuable_sidebar(user, project, closed_merge_request))
+
+ preload_view_requirements
+
+ allow(view).to receive_messages(current_user: user,
+ can?: true,
+ current_application_settings: Gitlab::CurrentSettings.current_application_settings)
+ end
+
+ def serialize_issuable_sidebar(user, project, merge_request)
+ MergeRequestSerializer
+ .new(current_user: user, project: project)
+ .represent(closed_merge_request, serializer: 'sidebar')
+ end
+end
diff --git a/spec/support/shared_contexts/issuable/project_shared_context.rb b/spec/support/shared_contexts/issuable/project_shared_context.rb
new file mode 100644
index 00000000000..6dbf3154977
--- /dev/null
+++ b/spec/support/shared_contexts/issuable/project_shared_context.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+shared_context 'project show action' do
+ let(:project) { create(:project, :repository) }
+ let(:issue) { create(:issue, project: project, author: user) }
+ let(:user) { create(:user) }
+
+ before do
+ assign(:project, project)
+ assign(:issue, issue)
+ assign(:noteable, issue)
+ stub_template 'shared/issuable/_sidebar' => ''
+ stub_template 'projects/issues/_discussion' => ''
+ allow(view).to receive(:user_status).and_return('')
+ end
+end
diff --git a/spec/support/shared_contexts/json_response_shared_context.rb b/spec/support/shared_contexts/json_response_shared_context.rb
index 6a0734decd5..2f0a564d2bc 100644
--- a/spec/support/shared_contexts/json_response_shared_context.rb
+++ b/spec/support/shared_contexts/json_response_shared_context.rb
@@ -1,5 +1,5 @@
# frozen_string_literal: true
RSpec.shared_context 'JSON response' do
- let(:json_response) { JSON.parse(response.body) }
+ let(:json_response) { Gitlab::Json.parse(response.body) }
end
diff --git a/spec/support/shared_contexts/lib/gitlab/import_export/project/rake_task_object_storage_shared_context.rb b/spec/support/shared_contexts/lib/gitlab/import_export/project/rake_task_object_storage_shared_context.rb
new file mode 100644
index 00000000000..dc1a52e3629
--- /dev/null
+++ b/spec/support/shared_contexts/lib/gitlab/import_export/project/rake_task_object_storage_shared_context.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+RSpec.shared_context 'rake task object storage shared context' do
+ before do
+ allow(Settings.uploads.object_store).to receive(:[]=).and_call_original
+ end
+
+ around do |example|
+ old_object_store_setting = Settings.uploads.object_store['enabled']
+
+ Settings.uploads.object_store['enabled'] = true
+
+ example.run
+
+ Settings.uploads.object_store['enabled'] = old_object_store_setting
+ end
+end
diff --git a/spec/support/shared_contexts/navbar_structure_context.rb b/spec/support/shared_contexts/navbar_structure_context.rb
index 4606608866a..fe3c32ec0c5 100644
--- a/spec/support/shared_contexts/navbar_structure_context.rb
+++ b/spec/support/shared_contexts/navbar_structure_context.rb
@@ -62,6 +62,7 @@ RSpec.shared_context 'project navbar structure' do
nav_item: _('Operations'),
nav_sub_items: [
_('Metrics'),
+ _('Alerts'),
_('Environments'),
_('Error Tracking'),
_('Serverless'),
@@ -85,6 +86,7 @@ RSpec.shared_context 'project navbar structure' do
_('Members'),
_('Integrations'),
_('Webhooks'),
+ _('Access Tokens'),
_('Repository'),
_('CI / CD'),
_('Operations'),
diff --git a/spec/support/shared_contexts/policies/group_policy_shared_context.rb b/spec/support/shared_contexts/policies/group_policy_shared_context.rb
index c2797c49c02..a0d54666dff 100644
--- a/spec/support/shared_contexts/policies/group_policy_shared_context.rb
+++ b/spec/support/shared_contexts/policies/group_policy_shared_context.rb
@@ -14,17 +14,16 @@ RSpec.shared_context 'GroupPolicy context' do
%i[
read_label read_group upload_file read_namespace read_group_activity
read_group_issues read_group_boards read_group_labels read_group_milestones
- read_group_merge_requests read_wiki
+ read_group_merge_requests
]
end
let(:read_group_permissions) { %i[read_label read_list read_milestone read_board] }
- let(:reporter_permissions) { %i[admin_label read_container_image read_metrics_dashboard_annotation download_wiki_code] }
- let(:developer_permissions) { %i[admin_milestone create_metrics_dashboard_annotation delete_metrics_dashboard_annotation update_metrics_dashboard_annotation create_wiki] }
+ let(:reporter_permissions) { %i[admin_label read_container_image read_metrics_dashboard_annotation] }
+ let(:developer_permissions) { %i[admin_milestone create_metrics_dashboard_annotation delete_metrics_dashboard_annotation update_metrics_dashboard_annotation] }
let(:maintainer_permissions) do
%i[
create_projects
read_cluster create_cluster update_cluster admin_cluster add_cluster
- admin_wiki
]
end
let(:owner_permissions) do
@@ -35,7 +34,8 @@ RSpec.shared_context 'GroupPolicy context' do
:change_visibility_level,
:set_note_created_at,
:create_subgroup,
- :read_statistics
+ :read_statistics,
+ :update_default_branch_protection
].compact
end
diff --git a/spec/support/shared_contexts/policies/project_policy_shared_context.rb b/spec/support/shared_contexts/policies/project_policy_shared_context.rb
index 055164ec38e..5339fa003b9 100644
--- a/spec/support/shared_contexts/policies/project_policy_shared_context.rb
+++ b/spec/support/shared_contexts/policies/project_policy_shared_context.rb
@@ -39,7 +39,7 @@ RSpec.shared_context 'ProjectPolicy context' do
update_pipeline create_merge_request_from create_wiki push_code
resolve_note create_container_image update_container_image
create_environment create_deployment update_deployment create_release update_release
- update_environment
+ update_environment daily_statistics
]
end
@@ -49,7 +49,6 @@ RSpec.shared_context 'ProjectPolicy context' do
admin_snippet admin_project_member admin_note admin_wiki admin_project
admin_commit_status admin_build admin_container_image
admin_pipeline admin_environment admin_deployment destroy_release add_cluster
- daily_statistics
]
end
diff --git a/spec/support/shared_contexts/project_service_shared_context.rb b/spec/support/shared_contexts/project_service_shared_context.rb
index 89b196e7039..21d67ea71a8 100644
--- a/spec/support/shared_contexts/project_service_shared_context.rb
+++ b/spec/support/shared_contexts/project_service_shared_context.rb
@@ -5,6 +5,7 @@ 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
@@ -18,6 +19,10 @@ shared_context 'project service activation' do
click_link(name)
end
+ def click_active_toggle
+ find('input[name="service[active]"] + button').click
+ end
+
def click_test_integration
click_button('Test settings and save changes')
end
diff --git a/spec/support/shared_contexts/services_shared_context.rb b/spec/support/shared_contexts/services_shared_context.rb
index 21bc0651c44..bf4eac8f324 100644
--- a/spec/support/shared_contexts/services_shared_context.rb
+++ b/spec/support/shared_contexts/services_shared_context.rb
@@ -31,8 +31,7 @@ Service.available_services_names.each do |service|
let(:licensed_features) do
{
'github' => :github_project_service_integration,
- 'jenkins' => :jenkins_integration,
- 'jenkins_deprecated' => :jenkins_integration
+ 'jenkins' => :jenkins_integration
}
end
diff --git a/spec/support/shared_contexts/spam_constants.rb b/spec/support/shared_contexts/spam_constants.rb
new file mode 100644
index 00000000000..b6e92ea3050
--- /dev/null
+++ b/spec/support/shared_contexts/spam_constants.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+shared_context 'includes Spam constants' do
+ REQUIRE_RECAPTCHA = Spam::SpamConstants::REQUIRE_RECAPTCHA
+ DISALLOW = Spam::SpamConstants::DISALLOW
+ ALLOW = Spam::SpamConstants::ALLOW
+end
diff --git a/spec/support/shared_examples/controllers/instance_statistics_controllers_shared_examples.rb b/spec/support/shared_examples/controllers/instance_statistics_controllers_shared_examples.rb
index e4d59463d93..17087456720 100644
--- a/spec/support/shared_examples/controllers/instance_statistics_controllers_shared_examples.rb
+++ b/spec/support/shared_examples/controllers/instance_statistics_controllers_shared_examples.rb
@@ -27,12 +27,24 @@ RSpec.shared_examples 'instance statistics availability' do
context 'for admins' do
let(:user) { create(:admin) }
- it 'allows access when the feature is not available publicly' do
- stub_application_setting(instance_statistics_visibility_private: true)
+ context 'when admin mode disabled' do
+ it 'forbids access when the feature is not available publicly' do
+ stub_application_setting(instance_statistics_visibility_private: true)
- get :index
+ get :index
- expect(response).to have_gitlab_http_status(:success)
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'when admin mode enabled', :enable_admin_mode do
+ it 'allows access when the feature is not available publicly' do
+ stub_application_setting(instance_statistics_visibility_private: true)
+
+ get :index
+
+ expect(response).to have_gitlab_http_status(:success)
+ end
end
end
end
diff --git a/spec/support/shared_examples/controllers/known_sign_in_shared_examples.rb b/spec/support/shared_examples/controllers/known_sign_in_shared_examples.rb
new file mode 100644
index 00000000000..60abb76acec
--- /dev/null
+++ b/spec/support/shared_examples/controllers/known_sign_in_shared_examples.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'known sign in' do
+ def stub_remote_ip(ip)
+ request.remote_ip = ip
+ end
+
+ def stub_user_ip(ip)
+ user.update!(current_sign_in_ip: ip)
+ end
+
+ context 'with a valid post' do
+ context 'when remote IP does not match user last sign in IP' do
+ before do
+ stub_user_ip('127.0.0.1')
+ stub_remote_ip('169.0.0.1')
+ end
+
+ it 'notifies the user' do
+ expect_next_instance_of(NotificationService) do |instance|
+ expect(instance).to receive(:unknown_sign_in)
+ end
+
+ post_action
+ end
+ end
+
+ context 'when remote IP matches an active session' do
+ before do
+ existing_sessions = ActiveSession.session_ids_for_user(user.id)
+ existing_sessions.each { |sessions| ActiveSession.destroy(user, sessions) }
+
+ stub_user_ip('169.0.0.1')
+ stub_remote_ip('127.0.0.1')
+
+ ActiveSession.set(user, request)
+ end
+
+ it 'does not notify the user' do
+ expect_any_instance_of(NotificationService).not_to receive(:unknown_sign_in)
+
+ post_action
+ end
+ end
+
+ context 'when remote IP address matches last sign in IP' do
+ before do
+ stub_user_ip('127.0.0.1')
+ stub_remote_ip('127.0.0.1')
+ end
+
+ it 'does not notify the user' do
+ expect_any_instance_of(NotificationService).not_to receive(:unknown_sign_in)
+
+ post_action
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/controllers/variables_shared_examples.rb b/spec/support/shared_examples/controllers/variables_shared_examples.rb
index 752bdc47851..9ff0bc3d217 100644
--- a/spec/support/shared_examples/controllers/variables_shared_examples.rb
+++ b/spec/support/shared_examples/controllers/variables_shared_examples.rb
@@ -27,6 +27,9 @@ RSpec.shared_examples 'PATCH #update updates variables' do
protected: 'false' }
end
+ let(:variables_scope) { owner.variables }
+ let(:file_variables_scope) { owner.variables.file }
+
context 'with invalid new variable parameters' do
let(:variables_attributes) do
[
@@ -40,7 +43,7 @@ RSpec.shared_examples 'PATCH #update updates variables' do
end
it 'does not create the new variable' do
- expect { subject }.not_to change { owner.variables.count }
+ expect { subject }.not_to change { variables_scope.count }
end
it 'returns a bad request response' do
@@ -63,7 +66,7 @@ RSpec.shared_examples 'PATCH #update updates variables' do
end
it 'does not create the new variable' do
- expect { subject }.not_to change { owner.variables.count }
+ expect { subject }.not_to change { variables_scope.count }
end
it 'returns a bad request response' do
@@ -86,7 +89,7 @@ RSpec.shared_examples 'PATCH #update updates variables' do
end
it 'creates the new variable' do
- expect { subject }.to change { owner.variables.count }.by(1)
+ expect { subject }.to change { variables_scope.count }.by(1)
end
it 'returns a successful response' do
@@ -106,7 +109,7 @@ RSpec.shared_examples 'PATCH #update updates variables' do
let(:variables_attributes) { [variable_attributes.merge(_destroy: 'true')] }
it 'destroys the variable' do
- expect { subject }.to change { owner.variables.count }.by(-1)
+ expect { subject }.to change { variables_scope.count }.by(-1)
expect { variable.reload }.to raise_error ActiveRecord::RecordNotFound
end
@@ -123,6 +126,18 @@ RSpec.shared_examples 'PATCH #update updates variables' do
end
end
+ context 'with missing variable' do
+ let(:variables_attributes) do
+ [variable_attributes.merge(_destroy: 'true', id: 'some-id')]
+ end
+
+ it 'returns not found response' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
context 'for variables of type file' do
let(:variables_attributes) do
[
@@ -131,7 +146,7 @@ RSpec.shared_examples 'PATCH #update updates variables' do
end
it 'creates new variable of type file' do
- expect { subject }.to change { owner.variables.file.count }.by(1)
+ expect { subject }.to change { file_variables_scope.count }.by(1)
end
end
end
diff --git a/spec/support/shared_examples/features/error_tracking_shared_example.rb b/spec/support/shared_examples/features/error_tracking_shared_example.rb
index 922d2627bce..1cd05b22ae9 100644
--- a/spec/support/shared_examples/features/error_tracking_shared_example.rb
+++ b/spec/support/shared_examples/features/error_tracking_shared_example.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
shared_examples 'error tracking index page' do
- it 'renders the error index page' do
+ it 'renders the error index page', quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/217810' } do
within('div.js-title-container') do
expect(page).to have_content(project.namespace.name)
expect(page).to have_content(project.name)
@@ -15,7 +15,7 @@ shared_examples 'error tracking index page' do
end
end
- it 'loads the error show page on click' do
+ it 'loads the error show page on click', quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/217810' } do
click_on issues_response[0]['title']
wait_for_requests
@@ -23,7 +23,7 @@ shared_examples 'error tracking index page' do
expect(page).to have_content('Error Details')
end
- it 'renders the error index data' do
+ it 'renders the error index data', quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/217810' } do
within('div.error-list') do
expect(page).to have_content(issues_response[0]['title'])
expect(page).to have_content(issues_response[0]['count'].to_s)
@@ -34,7 +34,7 @@ shared_examples 'error tracking index page' do
end
shared_examples 'expanded stack trace context' do |selected_line: nil, expected_line: 1|
- it 'expands the stack trace context' do
+ it 'expands the stack trace context', quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/217810' } do
within('div.stacktrace') do
find("div.file-holder:nth-child(#{selected_line}) svg.ic-chevron-right").click if selected_line
@@ -49,7 +49,7 @@ shared_examples 'expanded stack trace context' do |selected_line: nil, expected_
end
shared_examples 'error tracking show page' do
- it 'renders the error details' do
+ it 'renders the error details', quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/217810' } do
content = page.find(".content")
nav = page.find("nav.breadcrumbs")
header = page.find(".error-details-header")
@@ -67,11 +67,11 @@ shared_examples 'error tracking show page' do
expect(content).to have_content('Users: 0')
end
- it 'renders the stack trace heading' do
+ it 'renders the stack trace heading', quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/217810' } do
expect(page).to have_content('Stack trace')
end
- it 'renders the stack trace' do
+ it 'renders the stack trace', quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/217810' } do
event_response['entries'][0]['data']['values'][0]['stacktrace']['frames'].each do |frame|
expect(frame['filename']).not_to be_nil
expect(page).to have_content(frame['filename'])
diff --git a/spec/support/shared_examples/features/variable_list_shared_examples.rb b/spec/support/shared_examples/features/variable_list_shared_examples.rb
index 4fd4d42003f..218ef070221 100644
--- a/spec/support/shared_examples/features/variable_list_shared_examples.rb
+++ b/spec/support/shared_examples/features/variable_list_shared_examples.rb
@@ -29,7 +29,6 @@ RSpec.shared_examples 'variable list' do
page.within('.js-ci-variable-list-section .js-row:last-child') do
find('.js-ci-variable-input-key').set('key')
find('.js-ci-variable-input-value').set('key_value')
- find('.ci-variable-protected-item .js-project-feature-toggle').click
expect(find('.js-ci-variable-input-protected', visible: false).value).to eq('true')
end
@@ -173,6 +172,7 @@ RSpec.shared_examples 'variable list' do
page.within('.js-ci-variable-list-section .js-row:last-child') do
find('.js-ci-variable-input-key').set('unprotected_key')
find('.js-ci-variable-input-value').set('unprotected_value')
+ find('.ci-variable-protected-item .js-project-feature-toggle').click
expect(find('.js-ci-variable-input-protected', visible: false).value).to eq('false')
end
@@ -207,7 +207,6 @@ RSpec.shared_examples 'variable list' do
page.within('.js-ci-variable-list-section .js-row:last-child') do
find('.js-ci-variable-input-key').set('protected_key')
find('.js-ci-variable-input-value').set('protected_value')
- find('.ci-variable-protected-item .js-project-feature-toggle').click
expect(find('.js-ci-variable-input-protected', visible: false).value).to eq('true')
end
diff --git a/spec/support/shared_examples/graphql/design_fields_shared_examples.rb b/spec/support/shared_examples/graphql/design_fields_shared_examples.rb
new file mode 100644
index 00000000000..029d7e677da
--- /dev/null
+++ b/spec/support/shared_examples/graphql/design_fields_shared_examples.rb
@@ -0,0 +1,80 @@
+# frozen_string_literal: true
+
+# To use these shared examples, you may define a value in scope named
+# `extra_design_fields`, to pass any extra fields in addition to the
+# standard design fields.
+RSpec.shared_examples 'a GraphQL type with design fields' do
+ let(:extra_design_fields) { [] }
+
+ it { expect(described_class).to require_graphql_authorizations(:read_design) }
+
+ it 'exposes the expected design fields' do
+ expected_fields = %i[
+ id
+ project
+ issue
+ filename
+ full_path
+ image
+ image_v432x230
+ diff_refs
+ event
+ notes_count
+ ] + extra_design_fields
+
+ expect(described_class).to have_graphql_fields(*expected_fields).only
+ end
+
+ describe '#image' do
+ let(:schema) { GitlabSchema }
+ let(:query) { GraphQL::Query.new(schema) }
+ let(:context) { double('Context', schema: schema, query: query, parent: nil) }
+ let(:field) { described_class.fields['image'] }
+ let(:args) { GraphQL::Query::Arguments::NO_ARGS }
+ let(:instance) do
+ object = GitlabSchema.sync_lazy(GitlabSchema.object_from_id(object_id))
+ object_type.authorized_new(object, query.context)
+ end
+ let(:instance_b) do
+ object_b = GitlabSchema.sync_lazy(GitlabSchema.object_from_id(object_id_b))
+ object_type.authorized_new(object_b, query.context)
+ end
+
+ it 'resolves to the design image URL' do
+ image = field.resolve_field(instance, args, context)
+ sha = design.versions.first.sha
+ url = ::Gitlab::Routing.url_helpers.project_design_management_designs_raw_image_url(design.project, design, sha)
+
+ expect(image).to eq(url)
+ end
+
+ it 'has better than O(N) peformance', :request_store do
+ # Assuming designs have been loaded (as they must be), the following
+ # queries are required:
+ # For each distinct version:
+ # - design_management_versions
+ # (Request store is needed so that each version is fetched only once.)
+ # For each distinct issue
+ # - issues
+ # For each distinct project
+ # - projects
+ # - routes
+ # - namespaces
+ # Here that total is:
+ # - 2 x issues
+ # - 2 x versions
+ # - 2 x (projects + routes + namespaces)
+ # = 10
+ expect(instance).not_to eq(instance_b) # preload designs themselves.
+ expect do
+ image_a = field.resolve_field(instance, args, context)
+ image_b = field.resolve_field(instance, args, context)
+ image_c = field.resolve_field(instance_b, args, context)
+ image_d = field.resolve_field(instance_b, args, context)
+ expect(image_a).to eq(image_b)
+ expect(image_c).not_to eq(image_b)
+ expect(image_c).to eq(image_d)
+ end.not_to exceed_query_limit(10)
+ end
+ end
+end
diff --git a/spec/support/shared_examples/graphql/jira_import/jira_import_resolver_shared_examples.rb b/spec/support/shared_examples/graphql/jira_import/jira_import_resolver_shared_examples.rb
index 3d97fe10a47..2b96010477c 100644
--- a/spec/support/shared_examples/graphql/jira_import/jira_import_resolver_shared_examples.rb
+++ b/spec/support/shared_examples/graphql/jira_import/jira_import_resolver_shared_examples.rb
@@ -1,12 +1,12 @@
# frozen_string_literal: true
-shared_examples 'no jira import data present' do
+shared_examples 'no Jira import data present' do
it 'returns none' do
expect(resolve_imports).to eq JiraImportState.none
end
end
-shared_examples 'no jira import access' do
+shared_examples 'no Jira import access' do
it 'raises error' do
expect do
resolve_imports
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
new file mode 100644
index 00000000000..fb7e24eecf2
--- /dev/null
+++ b/spec/support/shared_examples/graphql/sorted_paginated_query_shared_examples.rb
@@ -0,0 +1,92 @@
+# frozen_string_literal: true
+
+# Use this for testing how a GraphQL query handles sorting and pagination.
+# This is particularly important when using keyset pagination connection,
+# which is the default for ActiveRecord relations, as certain sort keys
+# might not be supportable.
+#
+# sort_param: the value to specify the sort
+# data_path: the keys necessary to dig into the return GraphQL data to get the
+# returned results
+# first_param: number of items expected (like a page size)
+# expected_results: array of comparison data of all items sorted correctly
+# pagination_query: method that specifies the GraphQL query
+# pagination_results_data: method that extracts the sorted data used to compare against
+# the expected results
+#
+# Example:
+# describe 'sorting and pagination' do
+# let(:sort_project) { create(:project, :public) }
+# let(:data_path) { [:project, :issues] }
+#
+# def pagination_query(params, page_info)
+# graphql_query_for(
+# 'project',
+# { 'fullPath' => sort_project.full_path },
+# "issues(#{params}) { #{page_info} edges { node { iid weight } } }"
+# )
+# end
+#
+# def pagination_results_data(data)
+# data.map { |issue| issue.dig('node', 'iid').to_i }
+# end
+#
+# context 'when sorting by weight' do
+# ...
+# context 'when ascending' do
+# it_behaves_like 'sorted paginated query' do
+# let(:sort_param) { 'WEIGHT_ASC' }
+# let(:first_param) { 2 }
+# let(:expected_results) { [weight_issue3.iid, weight_issue5.iid, weight_issue1.iid, weight_issue4.iid, weight_issue2.iid] }
+# end
+# end
+#
+RSpec.shared_examples 'sorted paginated query' do
+ it_behaves_like 'requires variables' do
+ let(:required_variables) { [:sort_param, :first_param, :expected_results, :data_path, :current_user] }
+ 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 }" }
+
+ 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)
+
+ super
+ end
+
+ def pagination_results_data(data)
+ raise('pagination_results_data(data) must be defined in the test, see example in comment') unless defined?(super)
+
+ super(data)
+ end
+
+ before do
+ post_graphql(pagination_query(params, page_info), current_user: current_user)
+ end
+
+ context 'when sorting' do
+ it 'sorts correctly' do
+ expect(pagination_results_data(sorted_edges)).to eq expected_results
+ end
+
+ context 'when paginating' do
+ let(:params) { "sort: #{sort_param}, first: #{first_param}" }
+
+ 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)
+ post_graphql(cursored_query, current_user: current_user)
+ response_data = graphql_dig_at(Gitlab::Json.parse(response.body), :data, *data_path, :edges)
+
+ expect(pagination_results_data(response_data)).to eq expected_results.drop(first_param)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/helm_commands_shared_examples.rb b/spec/support/shared_examples/helm_commands_shared_examples.rb
new file mode 100644
index 00000000000..f0624fbf29f
--- /dev/null
+++ b/spec/support/shared_examples/helm_commands_shared_examples.rb
@@ -0,0 +1,131 @@
+# frozen_string_literal: true
+
+shared_examples 'helm command generator' do
+ describe '#generate_script' do
+ let(:helm_setup) do
+ <<~EOS
+ set -xeo pipefail
+ EOS
+ end
+
+ it 'returns appropriate command' do
+ expect(subject.generate_script.strip).to eq((helm_setup + commands).strip)
+ end
+ end
+end
+
+shared_examples 'helm command' do
+ describe '#rbac?' do
+ subject { command.rbac? }
+
+ context 'rbac is enabled' do
+ let(:rbac) { true }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'rbac is not enabled' do
+ let(:rbac) { false }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ describe '#pod_resource' do
+ subject { command.pod_resource }
+
+ context 'rbac is enabled' do
+ let(:rbac) { true }
+
+ it { is_expected.to be_an_instance_of ::Kubeclient::Resource }
+
+ it 'generates a pod that uses the tiller serviceAccountName' do
+ expect(subject.spec.serviceAccountName).to eq('tiller')
+ end
+ end
+
+ context 'rbac is not enabled' do
+ let(:rbac) { false }
+
+ it { is_expected.to be_an_instance_of ::Kubeclient::Resource }
+
+ it 'generates a pod that uses the default serviceAccountName' do
+ expect(subject.spec.serviceAcccountName).to be_nil
+ end
+ end
+ end
+
+ describe '#config_map_resource' do
+ subject { command.config_map_resource }
+
+ let(:metadata) do
+ {
+ name: "values-content-configuration-#{command.name}",
+ namespace: 'gitlab-managed-apps',
+ labels: { name: "values-content-configuration-#{command.name}" }
+ }
+ end
+
+ let(:resource) { ::Kubeclient::Resource.new(metadata: metadata, data: command.files) }
+
+ it 'returns a KubeClient resource with config map content for the application' do
+ is_expected.to eq(resource)
+ end
+ end
+
+ describe '#service_account_resource' do
+ let(:resource) do
+ Kubeclient::Resource.new(metadata: { name: 'tiller', namespace: 'gitlab-managed-apps' })
+ end
+
+ subject { command.service_account_resource }
+
+ context 'rbac is enabled' do
+ let(:rbac) { true }
+
+ it 'generates a Kubeclient resource for the tiller ServiceAccount' do
+ is_expected.to eq(resource)
+ end
+ end
+
+ context 'rbac is not enabled' do
+ let(:rbac) { false }
+
+ it 'generates nothing' do
+ is_expected.to be_nil
+ end
+ end
+ end
+
+ describe '#cluster_role_binding_resource' do
+ let(:resource) do
+ Kubeclient::Resource.new(
+ metadata: { name: 'tiller-admin' },
+ roleRef: { apiGroup: 'rbac.authorization.k8s.io', kind: 'ClusterRole', name: 'cluster-admin' },
+ subjects: [{ kind: 'ServiceAccount', name: 'tiller', namespace: 'gitlab-managed-apps' }]
+ )
+ end
+
+ subject(:cluster_role_binding_resource) { command.cluster_role_binding_resource }
+
+ context 'rbac is enabled' do
+ let(:rbac) { true }
+
+ it 'generates a Kubeclient resource for the ClusterRoleBinding for tiller' do
+ is_expected.to eq(resource)
+ end
+
+ it 'binds the account in #service_account_resource' do
+ expect(cluster_role_binding_resource.subjects.first.name).to eq(command.service_account_resource.metadata.name)
+ end
+ end
+
+ context 'rbac is not enabled' do
+ let(:rbac) { false }
+
+ it 'generates nothing' do
+ is_expected.to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/lib/gitlab/cycle_analytics/base_stage_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/cycle_analytics/base_stage_shared_examples.rb
index 851ed9c65a3..14292f70228 100644
--- a/spec/support/shared_examples/lib/gitlab/cycle_analytics/base_stage_shared_examples.rb
+++ b/spec/support/shared_examples/lib/gitlab/cycle_analytics/base_stage_shared_examples.rb
@@ -63,7 +63,7 @@ shared_examples 'Gitlab::Analytics::CycleAnalytics::DataCollector backend exampl
context 'provides the same results as the old implementation' do
it 'for the median' do
- expect(data_collector.median.seconds).to eq(ISSUES_MEDIAN)
+ expect(data_collector.median.seconds).to be_within(0.5).of(ISSUES_MEDIAN)
end
it 'for the list of event records' do
diff --git a/spec/support/shared_examples/lib/gitlab/helm_generated_script_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/helm_generated_script_shared_examples.rb
deleted file mode 100644
index bbf8a946f8b..00000000000
--- a/spec/support/shared_examples/lib/gitlab/helm_generated_script_shared_examples.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-# frozen_string_literal: true
-
-RSpec.shared_examples 'helm commands' do
- describe '#generate_script' do
- let(:helm_setup) do
- <<~EOS
- set -xeo pipefail
- EOS
- end
-
- it 'returns appropriate command' do
- expect(subject.generate_script.strip).to eq((helm_setup + commands).strip)
- end
- end
-end
diff --git a/spec/support/shared_examples/lib/gitlab/import_export/project/rake_task_object_storage_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/import_export/project/rake_task_object_storage_shared_examples.rb
new file mode 100644
index 00000000000..d6dc89a2c15
--- /dev/null
+++ b/spec/support/shared_examples/lib/gitlab/import_export/project/rake_task_object_storage_shared_examples.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'rake task with disabled object_storage' do |service_class, method|
+ it 'disables direct & background upload only for service call' do
+ expect_next_instance_of(service_class) do |service|
+ expect(service).to receive(:execute).and_wrap_original do |m|
+ expect(Settings.uploads.object_store['enabled']).to eq(false)
+
+ m.call
+ end
+ end
+
+ expect(rake_task).to receive(method).and_wrap_original do |m, *args|
+ expect(Settings.uploads.object_store['enabled']).to eq(true)
+ expect(Settings.uploads.object_store).not_to receive(:[]=).with('enabled', false)
+
+ m.call(*args)
+ end
+
+ subject
+ end
+end
diff --git a/spec/support/shared_examples/lib/gitlab/migration_helpers_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/migration_helpers_shared_examples.rb
index 8893ed5504b..72d672fd36c 100644
--- a/spec/support/shared_examples/lib/gitlab/migration_helpers_shared_examples.rb
+++ b/spec/support/shared_examples/lib/gitlab/migration_helpers_shared_examples.rb
@@ -13,10 +13,11 @@ end
RSpec.shared_examples 'performs validation' do |validation_option|
it 'performs validation' do
expect(model).to receive(:disable_statement_timeout).and_call_original
+ expect(model).to receive(:statement_timeout_disabled?).and_return(false)
expect(model).to receive(:execute).with(/statement_timeout/)
expect(model).to receive(:execute).ordered.with(/NOT VALID/)
expect(model).to receive(:execute).ordered.with(/VALIDATE CONSTRAINT/)
- expect(model).to receive(:execute).with(/RESET ALL/)
+ expect(model).to receive(:execute).ordered.with(/RESET ALL/)
model.add_concurrent_foreign_key(*args, **options.merge(validation_option))
end
diff --git a/spec/support/shared_examples/models/chat_service_shared_examples.rb b/spec/support/shared_examples/models/chat_service_shared_examples.rb
index 1cc1a1c8176..0a1c27b32db 100644
--- a/spec/support/shared_examples/models/chat_service_shared_examples.rb
+++ b/spec/support/shared_examples/models/chat_service_shared_examples.rb
@@ -198,7 +198,7 @@ RSpec.shared_examples "chat service" do |service_name|
message: "user created page: Awesome wiki_page"
}
end
- let(:wiki_page) { create(:wiki_page, wiki: project.wiki, attrs: opts) }
+ let(:wiki_page) { create(:wiki_page, wiki: project.wiki, **opts) }
let(:sample_data) { Gitlab::DataBuilder::WikiPage.build(wiki_page, user, "create") }
it_behaves_like "triggered #{service_name} service"
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 37f1b33d455..c2fd04d648b 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
@@ -194,6 +194,66 @@ RSpec.shared_examples 'cluster application status specs' do |application_name|
end
end
+ describe '#make_externally_installed' do
+ subject { create(application_name, :installing) }
+
+ it 'is installed' do
+ subject.make_externally_installed
+
+ expect(subject).to be_installed
+ end
+
+ context 'application is updated' do
+ subject { create(application_name, :updated) }
+
+ it 'is installed' do
+ subject.make_externally_installed
+
+ expect(subject).to be_installed
+ end
+ end
+
+ context 'application is errored' do
+ subject { create(application_name, :errored) }
+
+ it 'is installed' do
+ subject.make_externally_installed
+
+ expect(subject).to be_installed
+ end
+ end
+ end
+
+ describe '#make_externally_uninstalled' do
+ subject { create(application_name, :installed) }
+
+ it 'is uninstalled' do
+ subject.make_externally_uninstalled
+
+ expect(subject).to be_uninstalled
+ end
+
+ context 'application is updated' do
+ subject { create(application_name, :updated) }
+
+ it 'is uninstalled' do
+ subject.make_externally_uninstalled
+
+ expect(subject).to be_uninstalled
+ end
+ end
+
+ context 'application is errored' do
+ subject { create(application_name, :errored) }
+
+ it 'is uninstalled' do
+ subject.make_externally_uninstalled
+
+ expect(subject).to be_uninstalled
+ end
+ end
+ end
+
describe '#make_scheduled' do
subject { create(application_name, :installable) }
@@ -278,6 +338,7 @@ RSpec.shared_examples 'cluster application status specs' do |application_name|
:update_errored | false
:uninstalling | false
:uninstall_errored | false
+ :uninstalled | false
:timed_out | false
end
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
index 8372ee9ac4a..76339837351 100644
--- 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
@@ -76,7 +76,7 @@ RSpec.shared_examples 'a blob replicator' do
expect(service).to receive(:execute)
expect(::Geo::BlobDownloadService).to receive(:new).with(replicator: replicator).and_return(service)
- replicator.consume_created_event
+ replicator.consume_event_created
end
end
@@ -89,7 +89,7 @@ RSpec.shared_examples 'a blob replicator' do
end
describe '#model' do
- let(:invoke_model) { replicator.send(:model) }
+ let(:invoke_model) { replicator.class.model }
it 'is implemented' do
expect do
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 41de499f590..30c8c7d0fe5 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
@@ -5,6 +5,7 @@ RSpec.shared_examples 'model with repository' do
let(:stubbed_container) { raise NotImplementedError }
let(:expected_full_path) { raise NotImplementedError }
let(:expected_web_url_path) { expected_full_path }
+ let(:expected_repo_url_path) { expected_full_path }
describe '#commits_by' do
let(:commits) { container.repository.commits('HEAD', limit: 3).commits }
@@ -53,19 +54,19 @@ RSpec.shared_examples 'model with repository' do
describe '#url_to_repo' do
it 'returns the SSH URL to the repository' do
- expect(container.url_to_repo).to eq("#{Gitlab.config.gitlab_shell.ssh_path_prefix}#{expected_web_url_path}.git")
+ expect(container.url_to_repo).to eq(container.ssh_url_to_repo)
end
end
describe '#ssh_url_to_repo' do
it 'returns the SSH URL to the repository' do
- expect(container.ssh_url_to_repo).to eq(container.url_to_repo)
+ expect(container.ssh_url_to_repo).to eq("#{Gitlab.config.gitlab_shell.ssh_path_prefix}#{expected_repo_url_path}.git")
end
end
describe '#http_url_to_repo' do
it 'returns the HTTP URL to the repository' do
- expect(container.http_url_to_repo).to eq("#{Gitlab.config.gitlab.url}/#{expected_web_url_path}.git")
+ expect(container.http_url_to_repo).to eq("#{Gitlab.config.gitlab.url}/#{expected_repo_url_path}.git")
end
end
@@ -95,12 +96,8 @@ RSpec.shared_examples 'model with repository' do
end
context 'when the repo exists' do
- it { expect(container.empty_repo?).to be(false) }
-
- it 'returns true when repository is empty' do
- allow(container.repository).to receive(:empty?).and_return(true)
-
- expect(container.empty_repo?).to be(true)
+ it 'returns the empty state of the repository' do
+ expect(container.empty_repo?).to be(container.repository.empty?)
end
end
end
@@ -146,15 +143,14 @@ RSpec.shared_examples 'model with repository' do
end
it 'picks storage from ApplicationSetting' do
- expect_next_instance_of(ApplicationSetting) do |instance|
- expect(instance).to receive(:pick_repository_storage).and_return('picked')
- end
+ expect(Gitlab::CurrentSettings).to receive(:pick_repository_storage).and_return('picked')
expect(subject).to eq('picked')
end
it 'picks from the latest available storage', :request_store do
stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
+ Gitlab::CurrentSettings.expire_current_application_settings
Gitlab::CurrentSettings.current_application_settings
settings = ApplicationSetting.last
diff --git a/spec/support/shared_examples/models/concerns/has_wiki_shared_examples.rb b/spec/support/shared_examples/models/concerns/has_wiki_shared_examples.rb
new file mode 100644
index 00000000000..0357b7462fb
--- /dev/null
+++ b/spec/support/shared_examples/models/concerns/has_wiki_shared_examples.rb
@@ -0,0 +1,79 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'model with wiki' do
+ describe '#create_wiki' do
+ it 'returns true if the wiki repository already exists' do
+ expect(container.wiki_repository_exists?).to be(true)
+ expect(container.create_wiki).to be(true)
+ end
+
+ it 'returns true if the wiki repository was created' do
+ expect(container_without_wiki.wiki_repository_exists?).to be(false)
+ expect(container_without_wiki.create_wiki).to be(true)
+ expect(container_without_wiki.wiki_repository_exists?).to be(true)
+ end
+
+ context 'when the repository cannot be created' do
+ before do
+ expect(container.wiki).to receive(:wiki) { raise Wiki::CouldNotCreateWikiError }
+ end
+
+ it 'returns false and adds a validation error' do
+ expect(container.create_wiki).to be(false)
+ expect(container.errors[:base]).to contain_exactly('Failed to create wiki')
+ end
+ end
+ end
+
+ describe '#wiki_repository_exists?' do
+ it 'returns true when the wiki repository exists' do
+ expect(container.wiki_repository_exists?).to eq(true)
+ end
+
+ it 'returns false when the wiki repository does not exist' do
+ expect(container_without_wiki.wiki_repository_exists?).to eq(false)
+ end
+ end
+
+ describe 'wiki path conflict' do
+ context 'when the new path has been used by the wiki of other Project' do
+ it 'has an error on the name attribute' do
+ create(:project, namespace: container.parent, path: 'existing')
+ container.path = 'existing.wiki'
+
+ expect(container).not_to be_valid
+ expect(container.errors[:name].first).to eq(_('has already been taken'))
+ end
+ end
+
+ context 'when the new wiki path has been used by the path of other Project' do
+ it 'has an error on the name attribute' do
+ create(:project, namespace: container.parent, path: 'existing.wiki')
+ container.path = 'existing'
+
+ expect(container).not_to be_valid
+ expect(container.errors[:name].first).to eq(_('has already been taken'))
+ end
+ end
+
+ context 'when the new path has been used by the wiki of other Group' do
+ it 'has an error on the name attribute' do
+ create(:group, parent: container.parent, path: 'existing')
+ container.path = 'existing.wiki'
+
+ expect(container).not_to be_valid
+ expect(container.errors[:name].first).to eq(_('has already been taken'))
+ end
+ end
+
+ context 'when the new wiki path has been used by the path of other Group' do
+ it 'has an error on the name attribute' do
+ create(:group, parent: container.parent, path: 'existing.wiki')
+ container.path = 'existing'
+
+ expect(container).not_to be_valid
+ expect(container.errors[:name].first).to eq(_('has already been taken'))
+ end
+ 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
new file mode 100644
index 00000000000..4bcea36fd42
--- /dev/null
+++ b/spec/support/shared_examples/models/concerns/limitable_shared_examples.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'includes Limitable concern' do
+ describe 'validations' do
+ let(:plan_limits) { create(:plan_limits, :default_plan) }
+
+ it { is_expected.to be_a(Limitable) }
+
+ context 'without plan limits configured' do
+ it 'can create new models' do
+ expect { subject.save }.to change { described_class.count }
+ end
+ end
+
+ context 'with plan limits configured' do
+ before do
+ plan_limits.update(subject.class.limit_name => 1)
+ end
+
+ it 'can create new models' do
+ expect { subject.save }.to change { described_class.count }
+ end
+
+ context 'with an existing model' do
+ before do
+ subject.dup.save
+ end
+
+ it 'cannot create new models exceding 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
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/models/concerns/timebox_shared_examples.rb b/spec/support/shared_examples/models/concerns/timebox_shared_examples.rb
new file mode 100644
index 00000000000..32d502af5a2
--- /dev/null
+++ b/spec/support/shared_examples/models/concerns/timebox_shared_examples.rb
@@ -0,0 +1,242 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'a timebox' do |timebox_type|
+ let(:project) { create(:project, :public) }
+ let(:group) { create(:group) }
+ let(:timebox) { create(timebox_type, project: project) }
+ let(:issue) { create(:issue, project: project) }
+ let(:user) { create(:user) }
+ let(:timebox_table_name) { timebox_type.to_s.pluralize.to_sym }
+
+ describe 'modules' do
+ context 'with a project' do
+ it_behaves_like 'AtomicInternalId' do
+ let(:internal_id_attribute) { :iid }
+ let(:instance) { build(timebox_type, project: build(:project), group: nil) }
+ let(:scope) { :project }
+ let(:scope_attrs) { { project: instance.project } }
+ let(:usage) { timebox_table_name }
+ end
+ end
+
+ context 'with a group' do
+ it_behaves_like 'AtomicInternalId' do
+ let(:internal_id_attribute) { :iid }
+ let(:instance) { build(timebox_type, project: nil, group: build(:group)) }
+ let(:scope) { :group }
+ let(:scope_attrs) { { namespace: instance.group } }
+ let(:usage) { timebox_table_name }
+ end
+ end
+ end
+
+ describe "Validation" do
+ before do
+ allow(subject).to receive(:set_iid).and_return(false)
+ end
+
+ describe 'start_date' do
+ it 'adds an error when start_date is greater then due_date' do
+ timebox = build(timebox_type, start_date: Date.tomorrow, due_date: Date.yesterday)
+
+ expect(timebox).not_to be_valid
+ expect(timebox.errors[:due_date]).to include("must be greater than start date")
+ end
+
+ it 'adds an error when start_date is greater than 9999-12-31' do
+ timebox = build(timebox_type, start_date: Date.new(10000, 1, 1))
+
+ expect(timebox).not_to be_valid
+ expect(timebox.errors[:start_date]).to include("date must not be after 9999-12-31")
+ end
+ end
+
+ describe 'due_date' do
+ it 'adds an error when due_date is greater than 9999-12-31' do
+ timebox = build(timebox_type, due_date: Date.new(10000, 1, 1))
+
+ expect(timebox).not_to be_valid
+ expect(timebox.errors[:due_date]).to include("date must not be after 9999-12-31")
+ end
+ end
+
+ describe 'title' do
+ it { is_expected.to validate_presence_of(:title) }
+
+ it 'is invalid if title would be empty after sanitation' do
+ timebox = build(timebox_type, project: project, title: '<img src=x onerror=prompt(1)>')
+
+ expect(timebox).not_to be_valid
+ expect(timebox.errors[:title]).to include("can't be blank")
+ end
+ end
+
+ describe '#timebox_type_check' do
+ it 'is invalid if it has both project_id and group_id' do
+ timebox = build(timebox_type, group: group)
+ timebox.project = project
+
+ expect(timebox).not_to be_valid
+ expect(timebox.errors[:project_id]).to include("#{timebox_type} should belong either to a project or a group.")
+ end
+ end
+
+ describe "#uniqueness_of_title" do
+ context "per project" do
+ it "does not accept the same title in a project twice" do
+ new_timebox = timebox.dup
+ expect(new_timebox).not_to be_valid
+ end
+
+ it "accepts the same title in another project" do
+ project = create(:project)
+ new_timebox = timebox.dup
+ new_timebox.project = project
+
+ expect(new_timebox).to be_valid
+ end
+ end
+
+ context "per group" do
+ let(:timebox) { create(timebox_type, group: group) }
+
+ before do
+ project.update(group: group)
+ end
+
+ it "does not accept the same title in a group twice" do
+ new_timebox = described_class.new(group: group, title: timebox.title)
+
+ expect(new_timebox).not_to be_valid
+ end
+
+ it "does not accept the same title of a child project timebox" do
+ create(timebox_type, project: group.projects.first)
+
+ new_timebox = described_class.new(group: group, title: timebox.title)
+
+ expect(new_timebox).not_to be_valid
+ end
+ end
+ end
+ end
+
+ describe "Associations" do
+ it { is_expected.to belong_to(:project) }
+ it { is_expected.to belong_to(:group) }
+ it { is_expected.to have_many(:issues) }
+ it { is_expected.to have_many(:merge_requests) }
+ it { is_expected.to have_many(:labels) }
+ end
+
+ describe '#timebox_name' do
+ it 'returns the name of the model' do
+ expect(timebox.timebox_name).to eq(timebox_type.to_s)
+ end
+ end
+
+ describe '#project_timebox?' do
+ context 'when project_id is present' do
+ it 'returns true' do
+ expect(timebox.project_timebox?).to be_truthy
+ end
+ end
+
+ context 'when project_id is not present' do
+ let(:timebox) { build(timebox_type, group: group) }
+
+ it 'returns false' do
+ expect(timebox.project_timebox?).to be_falsey
+ end
+ end
+ end
+
+ describe '#group_timebox?' do
+ context 'when group_id is present' do
+ let(:timebox) { build(timebox_type, group: group) }
+
+ it 'returns true' do
+ expect(timebox.group_timebox?).to be_truthy
+ end
+ end
+
+ context 'when group_id is not present' do
+ it 'returns false' do
+ expect(timebox.group_timebox?).to be_falsey
+ end
+ end
+ end
+
+ describe '#safe_title' do
+ let(:timebox) { create(timebox_type, title: "<b>foo & bar -> 2.2</b>") }
+
+ it 'normalizes the title for use as a slug' do
+ expect(timebox.safe_title).to eq('foo-bar-22')
+ end
+ end
+
+ describe '#resource_parent' do
+ context 'when group is present' do
+ let(:timebox) { build(timebox_type, group: group) }
+
+ it 'returns the group' do
+ expect(timebox.resource_parent).to eq(group)
+ end
+ end
+
+ context 'when project is present' do
+ it 'returns the project' do
+ expect(timebox.resource_parent).to eq(project)
+ end
+ end
+ end
+
+ describe "#title" do
+ let(:timebox) { create(timebox_type, title: "<b>foo & bar -> 2.2</b>") }
+
+ it "sanitizes title" do
+ expect(timebox.title).to eq("foo & bar -> 2.2")
+ end
+ end
+
+ describe '#merge_requests_enabled?' do
+ context "per project" do
+ it "is true for projects with MRs enabled" do
+ project = create(:project, :merge_requests_enabled)
+ timebox = create(timebox_type, project: project)
+
+ expect(timebox.merge_requests_enabled?).to be_truthy
+ end
+
+ it "is false for projects with MRs disabled" do
+ project = create(:project, :repository_enabled, :merge_requests_disabled)
+ timebox = create(timebox_type, project: project)
+
+ expect(timebox.merge_requests_enabled?).to be_falsey
+ end
+
+ it "is false for projects with repository disabled" do
+ project = create(:project, :repository_disabled)
+ timebox = create(timebox_type, project: project)
+
+ expect(timebox.merge_requests_enabled?).to be_falsey
+ end
+ end
+
+ context "per group" do
+ let(:timebox) { create(timebox_type, group: group) }
+
+ it "is always true for groups, for performance reasons" do
+ expect(timebox.merge_requests_enabled?).to be_truthy
+ end
+ end
+ end
+
+ describe '#to_ability_name' do
+ it 'returns timebox' do
+ timebox = build(timebox_type)
+
+ expect(timebox.to_ability_name).to eq(timebox_type.to_s)
+ end
+ end
+end
diff --git a/spec/support/shared_examples/models/diff_positionable_note_shared_examples.rb b/spec/support/shared_examples/models/diff_positionable_note_shared_examples.rb
index aa8979603b6..b0cdc77a378 100644
--- a/spec/support/shared_examples/models/diff_positionable_note_shared_examples.rb
+++ b/spec/support/shared_examples/models/diff_positionable_note_shared_examples.rb
@@ -49,5 +49,29 @@ RSpec.shared_examples 'a valid diff positionable note' do |factory_on_commit|
expect(subject.errors).to have_key(:commit_id)
end
end
+
+ %i(original_position position change_position).each do |method|
+ describe "#{method}=" do
+ it "doesn't accept non-hash JSON passed as a string" do
+ subject.send(:"#{method}=", "true")
+ expect(subject.attributes_before_type_cast[method.to_s]).to be(nil)
+ end
+
+ it "does accept a position hash as a string" do
+ subject.send(:"#{method}=", position.to_json)
+ expect(subject.position).to eq(position)
+ end
+
+ it "doesn't accept an array" do
+ subject.send(:"#{method}=", ["test"])
+ expect(subject.attributes_before_type_cast[method.to_s]).to be(nil)
+ end
+
+ it "does accept a hash" do
+ subject.send(:"#{method}=", position.to_h)
+ expect(subject.position).to eq(position)
+ end
+ end
+ end
end
end
diff --git a/spec/support/shared_examples/models/email_format_shared_examples.rb b/spec/support/shared_examples/models/email_format_shared_examples.rb
index 6797836e383..a8115e440a4 100644
--- a/spec/support/shared_examples/models/email_format_shared_examples.rb
+++ b/spec/support/shared_examples/models/email_format_shared_examples.rb
@@ -44,3 +44,44 @@ RSpec.shared_examples 'an object with email-formated attributes' do |*attributes
end
end
end
+
+RSpec.shared_examples 'an object with RFC3696 compliant email-formated attributes' do |*attributes|
+ attributes.each do |attribute|
+ describe "specifically its :#{attribute} attribute" do
+ %w[
+ info@example.com
+ info+test@example.com
+ o'reilly@example.com
+ ].each do |valid_email|
+ context "with a value of '#{valid_email}'" do
+ let(:email_value) { valid_email }
+
+ it 'is valid' do
+ subject.send("#{attribute}=", valid_email)
+
+ expect(subject).to be_valid
+ end
+ end
+ end
+
+ %w[
+ foobar
+ test@test@example.com
+ test.test.@example.com
+ .test.test@example.com
+ mailto:test@example.com
+ lol!'+=?><#$%^&*()@gmail.com
+ ].each do |invalid_email|
+ context "with a value of '#{invalid_email}'" do
+ let(:email_value) { invalid_email }
+
+ it 'is invalid' do
+ subject.send("#{attribute}=", invalid_email)
+
+ expect(subject).to be_invalid
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/models/issuable_hook_data_shared_examples.rb b/spec/support/shared_examples/models/issuable_hook_data_shared_examples.rb
index ecf1640ef5d..21ab9b06c33 100644
--- a/spec/support/shared_examples/models/issuable_hook_data_shared_examples.rb
+++ b/spec/support/shared_examples/models/issuable_hook_data_shared_examples.rb
@@ -7,6 +7,7 @@ RSpec.shared_examples 'issuable hook data' do |kind|
include_examples 'project hook data' do
let(:project) { builder.issuable.project }
end
+
include_examples 'deprecated repository hook data'
context "with a #{kind}" do
diff --git a/spec/support/shared_examples/models/mentionable_shared_examples.rb b/spec/support/shared_examples/models/mentionable_shared_examples.rb
index ba6aa4e1d89..d3e9035393f 100644
--- a/spec/support/shared_examples/models/mentionable_shared_examples.rb
+++ b/spec/support/shared_examples/models/mentionable_shared_examples.rb
@@ -210,6 +210,10 @@ RSpec.shared_examples 'mentions in description' do |mentionable_type|
it 'stores no mentions' do
expect(mentionable.user_mentions.count).to eq 0
end
+
+ it 'renders description_html correctly' do
+ expect(mentionable.description_html).to include("<a href=\"/#{user.username}\" data-user=\"#{user.id}\"")
+ end
end
end
diff --git a/spec/support/shared_examples/models/slack_mattermost_notifications_shared_examples.rb b/spec/support/shared_examples/models/slack_mattermost_notifications_shared_examples.rb
index 24ff57c8517..a5228c43f6f 100644
--- a/spec/support/shared_examples/models/slack_mattermost_notifications_shared_examples.rb
+++ b/spec/support/shared_examples/models/slack_mattermost_notifications_shared_examples.rb
@@ -112,7 +112,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do |service_name|
message: "user created page: Awesome wiki_page"
}
- @wiki_page = create(:wiki_page, wiki: project.wiki, attrs: opts)
+ @wiki_page = create(:wiki_page, wiki: project.wiki, **opts)
@wiki_page_sample_data = Gitlab::DataBuilder::WikiPage.build(@wiki_page, user, 'create')
end
diff --git a/spec/support/shared_examples/models/wiki_shared_examples.rb b/spec/support/shared_examples/models/wiki_shared_examples.rb
new file mode 100644
index 00000000000..84569e95e11
--- /dev/null
+++ b/spec/support/shared_examples/models/wiki_shared_examples.rb
@@ -0,0 +1,423 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'wiki model' do
+ let_it_be(:user) { create(:user, :commit_email) }
+ let(:wiki_container) { raise NotImplementedError }
+ let(:wiki_container_without_repo) { raise NotImplementedError }
+ let(:wiki) { described_class.new(wiki_container, user) }
+ let(:commit) { subject.repository.head_commit }
+
+ subject { wiki }
+
+ it_behaves_like 'model with repository' do
+ let(:container) { wiki }
+ let(:stubbed_container) { described_class.new(wiki_container_without_repo, user) }
+ let(:expected_full_path) { "#{container.container.full_path}.wiki" }
+ let(:expected_web_url_path) { "#{container.container.web_url(only_path: true).sub(%r{^/}, '')}/-/wikis/home" }
+ end
+
+ describe '#repository' do
+ it 'returns a wiki repository' do
+ expect(subject.repository.repo_type).to be_wiki
+ end
+ end
+
+ describe '#full_path' do
+ it 'returns the container path with the .wiki extension' do
+ expect(subject.full_path).to eq(wiki_container.full_path + '.wiki')
+ end
+ end
+
+ describe '#wiki_base_path' do
+ it 'returns the wiki base path' do
+ expect(subject.wiki_base_path).to eq("#{wiki_container.web_url(only_path: true)}/-/wikis")
+ end
+ end
+
+ describe '#wiki' do
+ it 'contains a Gitlab::Git::Wiki instance' do
+ expect(subject.wiki).to be_a Gitlab::Git::Wiki
+ end
+
+ it 'creates a new wiki repo if one does not yet exist' do
+ expect(subject.create_page('index', 'test content')).to be_truthy
+ end
+
+ it 'creates a new wiki repo with a default commit message' do
+ expect(subject.create_page('index', 'test content', :markdown, '')).to be_truthy
+
+ page = subject.find_page('index')
+
+ expect(page.last_version.message).to eq("#{user.username} created page: index")
+ end
+
+ context 'when the repository cannot be created' do
+ let(:wiki_container) { wiki_container_without_repo }
+
+ before do
+ expect(subject.repository).to receive(:create_if_not_exists) { false }
+ end
+
+ it 'raises CouldNotCreateWikiError' do
+ expect { subject.wiki }.to raise_exception(Wiki::CouldNotCreateWikiError)
+ end
+ end
+ end
+
+ describe '#empty?' do
+ context 'when the wiki repository is empty' do
+ it 'returns true' do
+ expect(subject.empty?).to be(true)
+ end
+ end
+
+ context 'when the wiki has pages' do
+ before do
+ subject.create_page('index', 'This is an awesome new Gollum Wiki')
+ subject.create_page('another-page', 'This is another page')
+ end
+
+ describe '#empty?' do
+ it 'returns false' do
+ expect(subject.empty?).to be(false)
+ end
+
+ it 'only instantiates a Wiki page once' do
+ expect(WikiPage).to receive(:new).once.and_call_original
+
+ subject.empty?
+ end
+ end
+ end
+ end
+
+ describe '#list_pages' do
+ let(:wiki_pages) { subject.list_pages }
+
+ before do
+ subject.create_page('index', 'This is an index')
+ subject.create_page('index2', 'This is an index2')
+ subject.create_page('an index3', 'This is an index3')
+ end
+
+ it 'returns an array of WikiPage instances' do
+ expect(wiki_pages).to be_present
+ expect(wiki_pages).to all(be_a(WikiPage))
+ end
+
+ it 'does not load WikiPage content by default' do
+ wiki_pages.each do |page|
+ expect(page.content).to be_empty
+ end
+ end
+
+ it 'returns all pages by default' do
+ expect(wiki_pages.count).to eq(3)
+ end
+
+ context 'with limit option' do
+ it 'returns limited set of pages' do
+ expect(subject.list_pages(limit: 1).count).to eq(1)
+ end
+ end
+
+ context 'with sorting options' do
+ it 'returns pages sorted by title by default' do
+ pages = ['an index3', 'index', 'index2']
+
+ expect(subject.list_pages.map(&:title)).to eq(pages)
+ expect(subject.list_pages(direction: 'desc').map(&:title)).to eq(pages.reverse)
+ end
+
+ it 'returns pages sorted by created_at' do
+ pages = ['index', 'index2', 'an index3']
+
+ expect(subject.list_pages(sort: 'created_at').map(&:title)).to eq(pages)
+ expect(subject.list_pages(sort: 'created_at', direction: 'desc').map(&:title)).to eq(pages.reverse)
+ end
+ end
+
+ context 'with load_content option' do
+ let(:pages) { subject.list_pages(load_content: true) }
+
+ it 'loads WikiPage content' do
+ expect(pages.first.content).to eq('This is an index3')
+ expect(pages.second.content).to eq('This is an index')
+ expect(pages.third.content).to eq('This is an index2')
+ end
+ end
+ end
+
+ describe '#sidebar_entries' do
+ before do
+ (1..5).each { |i| create(:wiki_page, wiki: wiki, title: "my page #{i}") }
+ (6..10).each { |i| create(:wiki_page, wiki: wiki, title: "parent/my page #{i}") }
+ (11..15).each { |i| create(:wiki_page, wiki: wiki, title: "grandparent/parent/my page #{i}") }
+ end
+
+ def total_pages(entries)
+ entries.sum do |entry|
+ entry.is_a?(WikiDirectory) ? entry.pages.size : 1
+ end
+ end
+
+ context 'when the number of pages does not exceed the limit' do
+ it 'returns all pages grouped by directory and limited is false' do
+ entries, limited = subject.sidebar_entries
+
+ expect(entries.size).to be(7)
+ expect(total_pages(entries)).to be(15)
+ expect(limited).to be(false)
+ end
+ end
+
+ context 'when the number of pages exceeds the limit' do
+ before do
+ create(:wiki_page, wiki: wiki, title: 'my page 16')
+ end
+
+ it 'returns 15 pages grouped by directory and limited is true' do
+ entries, limited = subject.sidebar_entries
+
+ expect(entries.size).to be(8)
+ expect(total_pages(entries)).to be(15)
+ expect(limited).to be(true)
+ end
+ end
+ end
+
+ describe '#find_page' do
+ before do
+ subject.create_page('index page', 'This is an awesome Gollum Wiki')
+ end
+
+ it 'returns the latest version of the page if it exists' do
+ page = subject.find_page('index page')
+
+ expect(page.title).to eq('index page')
+ end
+
+ it 'returns nil if the page does not exist' do
+ expect(subject.find_page('non-existent')).to eq(nil)
+ end
+
+ it 'can find a page by slug' do
+ page = subject.find_page('index-page')
+
+ expect(page.title).to eq('index page')
+ end
+
+ it 'returns a WikiPage instance' do
+ page = subject.find_page('index page')
+
+ expect(page).to be_a WikiPage
+ end
+
+ context 'pages with multibyte-character title' do
+ before do
+ subject.create_page('autre pagé', "C'est un génial Gollum Wiki")
+ end
+
+ it 'can find a page by slug' do
+ page = subject.find_page('autre pagé')
+
+ expect(page.title).to eq('autre pagé')
+ end
+ end
+
+ context 'pages with invalidly-encoded content' do
+ before do
+ subject.create_page('encoding is fun', "f\xFCr".b)
+ end
+
+ it 'can find the page' do
+ page = subject.find_page('encoding is fun')
+
+ expect(page.content).to eq('fr')
+ end
+ end
+ end
+
+ describe '#find_sidebar' do
+ before do
+ subject.create_page(described_class::SIDEBAR, 'This is an awesome Sidebar')
+ end
+
+ it 'finds the page defined as _sidebar' do
+ page = subject.find_sidebar
+
+ expect(page.content).to eq('This is an awesome Sidebar')
+ end
+ end
+
+ describe '#find_file' do
+ let(:image) { File.open(Rails.root.join('spec', 'fixtures', 'big-image.png')) }
+
+ before do
+ subject.wiki # Make sure the wiki repo exists
+
+ subject.repository.create_file(user, 'image.png', image, branch_name: subject.default_branch, message: 'add image')
+ end
+
+ it 'returns the latest version of the file if it exists' do
+ file = subject.find_file('image.png')
+
+ expect(file.mime_type).to eq('image/png')
+ end
+
+ it 'returns nil if the page does not exist' do
+ expect(subject.find_file('non-existent')).to eq(nil)
+ end
+
+ it 'returns a Gitlab::Git::WikiFile instance' do
+ file = subject.find_file('image.png')
+
+ expect(file).to be_a Gitlab::Git::WikiFile
+ end
+
+ it 'returns the whole file' do
+ file = subject.find_file('image.png')
+ image.rewind
+
+ expect(file.raw_data.b).to eq(image.read.b)
+ end
+ end
+
+ describe '#create_page' do
+ it 'creates a new wiki page' do
+ expect(subject.create_page('test page', 'this is content')).not_to eq(false)
+ expect(subject.list_pages.count).to eq(1)
+ end
+
+ it 'returns false when a duplicate page exists' do
+ subject.create_page('test page', 'content')
+
+ expect(subject.create_page('test page', 'content')).to eq(false)
+ end
+
+ it 'stores an error message when a duplicate page exists' do
+ 2.times { subject.create_page('test page', 'content') }
+
+ expect(subject.error_message).to match(/Duplicate page:/)
+ end
+
+ it 'sets the correct commit message' do
+ subject.create_page('test page', 'some content', :markdown, 'commit message')
+
+ expect(subject.list_pages.first.page.version.message).to eq('commit message')
+ end
+
+ it 'sets the correct commit email' do
+ subject.create_page('test page', 'content')
+
+ expect(user.commit_email).not_to eq(user.email)
+ expect(commit.author_email).to eq(user.commit_email)
+ expect(commit.committer_email).to eq(user.commit_email)
+ end
+
+ it 'updates container activity' do
+ expect(subject).to receive(:update_container_activity)
+
+ subject.create_page('Test Page', 'This is content')
+ end
+ end
+
+ describe '#update_page' do
+ let(:page) { create(:wiki_page, wiki: subject, title: 'update-page') }
+
+ def update_page
+ subject.update_page(
+ page.page,
+ content: 'some other content',
+ format: :markdown,
+ message: 'updated page'
+ )
+ end
+
+ it 'updates the content of the page' do
+ update_page
+ page = subject.find_page('update-page')
+
+ expect(page.raw_content).to eq('some other content')
+ end
+
+ it 'sets the correct commit message' do
+ update_page
+ page = subject.find_page('update-page')
+
+ expect(page.version.message).to eq('updated page')
+ end
+
+ it 'sets the correct commit email' do
+ update_page
+
+ expect(user.commit_email).not_to eq(user.email)
+ expect(commit.author_email).to eq(user.commit_email)
+ expect(commit.committer_email).to eq(user.commit_email)
+ end
+
+ it 'updates container activity' do
+ page
+
+ expect(subject).to receive(:update_container_activity)
+
+ update_page
+ end
+ end
+
+ describe '#delete_page' do
+ let(:page) { create(:wiki_page, wiki: wiki) }
+
+ it 'deletes the page' do
+ subject.delete_page(page)
+
+ expect(subject.list_pages.count).to eq(0)
+ end
+
+ it 'sets the correct commit email' do
+ subject.delete_page(page)
+
+ expect(user.commit_email).not_to eq(user.email)
+ expect(commit.author_email).to eq(user.commit_email)
+ expect(commit.committer_email).to eq(user.commit_email)
+ end
+
+ it 'updates container activity' do
+ page
+
+ expect(subject).to receive(:update_container_activity)
+
+ subject.delete_page(page)
+ end
+ end
+
+ describe '#ensure_repository' do
+ context 'if the repository exists' do
+ it 'does not create the repository' do
+ expect(subject.repository.exists?).to eq(true)
+ expect(subject.repository.raw).not_to receive(:create_repository)
+
+ subject.ensure_repository
+ end
+ end
+
+ context 'if the repository does not exist' do
+ let(:wiki_container) { wiki_container_without_repo }
+
+ it 'creates the repository' do
+ expect(subject.repository.exists?).to eq(false)
+
+ subject.ensure_repository
+
+ expect(subject.repository.exists?).to eq(true)
+ end
+ end
+ end
+
+ describe '#hook_attrs' do
+ it 'returns a hash with values' do
+ expect(subject.hook_attrs).to be_a Hash
+ expect(subject.hook_attrs.keys).to contain_exactly(:web_url, :git_ssh_url, :git_http_url, :path_with_namespace, :default_branch)
+ end
+ end
+end
diff --git a/spec/support/shared_examples/policies/project_policy_shared_examples.rb b/spec/support/shared_examples/policies/project_policy_shared_examples.rb
index 1831fc10628..4dd0152e3d1 100644
--- a/spec/support/shared_examples/policies/project_policy_shared_examples.rb
+++ b/spec/support/shared_examples/policies/project_policy_shared_examples.rb
@@ -212,8 +212,8 @@ RSpec.shared_examples 'project policies as owner' do
end
end
-RSpec.shared_examples 'project policies as admin' do
- context 'abilities for non-public projects' do
+RSpec.shared_examples 'project policies as admin with admin mode' do
+ context 'abilities for non-public projects', :enable_admin_mode do
let(:project) { create(:project, namespace: owner.namespace) }
subject { described_class.new(admin, project) }
@@ -232,3 +232,13 @@ RSpec.shared_examples 'project policies as admin' do
end
end
end
+
+RSpec.shared_examples 'project policies as admin without admin mode' do
+ context 'abilities for non-public projects' do
+ let(:project) { create(:project, namespace: owner.namespace) }
+
+ subject { described_class.new(admin, project) }
+
+ it { is_expected.to be_banned }
+ end
+end
diff --git a/spec/support/shared_examples/policies/wiki_policies_shared_examples.rb b/spec/support/shared_examples/policies/wiki_policies_shared_examples.rb
index b91500ffd9c..58822f4309b 100644
--- a/spec/support/shared_examples/policies/wiki_policies_shared_examples.rb
+++ b/spec/support/shared_examples/policies/wiki_policies_shared_examples.rb
@@ -1,152 +1,116 @@
# frozen_string_literal: true
RSpec.shared_examples 'model with wiki policies' do
- let(:container) { raise NotImplementedError }
- let(:permissions) { %i(read_wiki create_wiki update_wiki admin_wiki download_wiki_code) }
-
- # TODO: Remove this helper once we implement group features
- # https://gitlab.com/gitlab-org/gitlab/-/issues/208412
- def set_access_level(access_level)
- raise NotImplementedError
- end
-
- subject { described_class.new(owner, container) }
-
- context 'when the feature is disabled' do
- before do
- set_access_level(ProjectFeature::DISABLED)
- end
+ include ProjectHelpers
+ include AdminModeHelper
- it 'does not include the wiki permissions' do
- expect_disallowed(*permissions)
- end
+ let(:container) { raise NotImplementedError }
+ let(:user) { raise NotImplementedError }
- context 'when there is an external wiki' do
- it 'does not include the wiki permissions' do
- allow(container).to receive(:has_external_wiki?).and_return(true)
+ subject { described_class.new(user, container) }
- expect_disallowed(*permissions)
- end
+ let_it_be(:wiki_permissions) do
+ {}.tap do |permissions|
+ permissions[:guest] = %i[read_wiki]
+ permissions[:reporter] = permissions[:guest] + %i[download_wiki_code]
+ permissions[:developer] = permissions[:reporter] + %i[create_wiki]
+ permissions[:maintainer] = permissions[:developer] + %i[admin_wiki]
+ permissions[:all] = permissions[:maintainer]
end
end
- describe 'read_wiki' do
- subject { described_class.new(user, container) }
-
- member_roles = %i[guest developer]
- stranger_roles = %i[anonymous non_member]
-
- user_roles = stranger_roles + member_roles
+ using RSpec::Parameterized::TableSyntax
+
+ where(:container_level, :access_level, :membership, :access) do
+ :public | :enabled | :admin | :all
+ :public | :enabled | :maintainer | :maintainer
+ :public | :enabled | :developer | :developer
+ :public | :enabled | :reporter | :reporter
+ :public | :enabled | :guest | :guest
+ :public | :enabled | :non_member | :guest
+ :public | :enabled | :anonymous | :guest
+
+ :public | :private | :admin | :all
+ :public | :private | :maintainer | :maintainer
+ :public | :private | :developer | :developer
+ :public | :private | :reporter | :reporter
+ :public | :private | :guest | :guest
+ :public | :private | :non_member | nil
+ :public | :private | :anonymous | nil
+
+ :public | :disabled | :admin | nil
+ :public | :disabled | :maintainer | nil
+ :public | :disabled | :developer | nil
+ :public | :disabled | :reporter | nil
+ :public | :disabled | :guest | nil
+ :public | :disabled | :non_member | nil
+ :public | :disabled | :anonymous | nil
+
+ :internal | :enabled | :admin | :all
+ :internal | :enabled | :maintainer | :maintainer
+ :internal | :enabled | :developer | :developer
+ :internal | :enabled | :reporter | :reporter
+ :internal | :enabled | :guest | :guest
+ :internal | :enabled | :non_member | :guest
+ :internal | :enabled | :anonymous | nil
+
+ :internal | :private | :admin | :all
+ :internal | :private | :maintainer | :maintainer
+ :internal | :private | :developer | :developer
+ :internal | :private | :reporter | :reporter
+ :internal | :private | :guest | :guest
+ :internal | :private | :non_member | nil
+ :internal | :private | :anonymous | nil
+
+ :internal | :disabled | :admin | nil
+ :internal | :disabled | :maintainer | nil
+ :internal | :disabled | :developer | nil
+ :internal | :disabled | :reporter | nil
+ :internal | :disabled | :guest | nil
+ :internal | :disabled | :non_member | nil
+ :internal | :disabled | :anonymous | nil
+
+ :private | :private | :admin | :all
+ :private | :private | :maintainer | :maintainer
+ :private | :private | :developer | :developer
+ :private | :private | :reporter | :reporter
+ :private | :private | :guest | :guest
+ :private | :private | :non_member | nil
+ :private | :private | :anonymous | nil
+
+ :private | :disabled | :admin | nil
+ :private | :disabled | :maintainer | nil
+ :private | :disabled | :developer | nil
+ :private | :disabled | :reporter | nil
+ :private | :disabled | :guest | nil
+ :private | :disabled | :non_member | nil
+ :private | :disabled | :anonymous | nil
+ end
- # When a user is anonymous, their `current_user == nil`
- let(:user) { create(:user) unless user_role == :anonymous }
+ with_them do
+ let(:user) { create_user_from_membership(container, membership) }
+ let(:allowed_permissions) { wiki_permissions[access].dup || [] }
+ let(:disallowed_permissions) { wiki_permissions[:all] - allowed_permissions }
before do
- container.visibility = container_visibility
- set_access_level(wiki_access_level)
- container.add_user(user, user_role) if member_roles.include?(user_role)
- end
-
- title = ->(container_visibility, wiki_access_level, user_role) do
- [
- "container is #{Gitlab::VisibilityLevel.level_name container_visibility}",
- "wiki is #{ProjectFeature.str_from_access_level wiki_access_level}",
- "user is #{user_role}"
- ].join(', ')
- end
-
- describe 'Situations where :read_wiki is always false' do
- where(case_names: title,
- container_visibility: Gitlab::VisibilityLevel.options.values,
- wiki_access_level: [ProjectFeature::DISABLED],
- user_role: user_roles)
-
- with_them do
- it { is_expected.to be_disallowed(:read_wiki) }
- end
- end
-
- describe 'Situations where :read_wiki is always true' do
- where(case_names: title,
- container_visibility: [Gitlab::VisibilityLevel::PUBLIC],
- wiki_access_level: [ProjectFeature::ENABLED],
- user_role: user_roles)
+ container.visibility = container_level.to_s
+ set_access_level(ProjectFeature.access_level_from_str(access_level.to_s))
+ enable_admin_mode!(user) if user&.admin?
- with_them do
- it { is_expected.to be_allowed(:read_wiki) }
+ if allowed_permissions.any? && [container_level, access_level, membership] != [:private, :private, :guest]
+ allowed_permissions << :download_wiki_code
end
end
- describe 'Situations where :read_wiki requires membership' do
- context 'the wiki is private, and the user is a member' do
- where(case_names: title,
- container_visibility: [Gitlab::VisibilityLevel::PUBLIC,
- Gitlab::VisibilityLevel::INTERNAL],
- wiki_access_level: [ProjectFeature::PRIVATE],
- user_role: member_roles)
-
- with_them do
- it { is_expected.to be_allowed(:read_wiki) }
- end
- end
-
- context 'the wiki is private, and the user is not member' do
- where(case_names: title,
- container_visibility: [Gitlab::VisibilityLevel::PUBLIC,
- Gitlab::VisibilityLevel::INTERNAL],
- wiki_access_level: [ProjectFeature::PRIVATE],
- user_role: stranger_roles)
-
- with_them do
- it { is_expected.to be_disallowed(:read_wiki) }
- end
- end
-
- context 'the wiki is enabled, and the user is a member' do
- where(case_names: title,
- container_visibility: [Gitlab::VisibilityLevel::PRIVATE],
- wiki_access_level: [ProjectFeature::ENABLED],
- user_role: member_roles)
-
- with_them do
- it { is_expected.to be_allowed(:read_wiki) }
- end
- end
-
- context 'the wiki is enabled, and the user is not a member' do
- where(case_names: title,
- container_visibility: [Gitlab::VisibilityLevel::PRIVATE],
- wiki_access_level: [ProjectFeature::ENABLED],
- user_role: stranger_roles)
-
- with_them do
- it { is_expected.to be_disallowed(:read_wiki) }
- end
- end
+ it 'allows actions based on membership' do
+ expect_allowed(*allowed_permissions)
+ expect_disallowed(*disallowed_permissions)
end
+ end
- describe 'Situations where :read_wiki prohibits anonymous access' do
- context 'the user is not anonymous' do
- where(case_names: title,
- container_visibility: [Gitlab::VisibilityLevel::INTERNAL],
- wiki_access_level: [ProjectFeature::ENABLED, ProjectFeature::PUBLIC],
- user_role: user_roles.reject { |u| u == :anonymous })
-
- with_them do
- it { is_expected.to be_allowed(:read_wiki) }
- end
- end
-
- context 'the user is anonymous' do
- where(case_names: title,
- container_visibility: [Gitlab::VisibilityLevel::INTERNAL],
- wiki_access_level: [ProjectFeature::ENABLED, ProjectFeature::PUBLIC],
- user_role: %i[anonymous])
-
- with_them do
- it { is_expected.to be_disallowed(:read_wiki) }
- end
- end
- end
+ # TODO: Remove this helper once we implement group features
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/208412
+ def set_access_level(access_level)
+ raise NotImplementedError
end
end
diff --git a/spec/support/shared_examples/quick_actions/issuable/issuable_quick_actions_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/issuable_quick_actions_shared_examples.rb
index b03da4471bc..50a8b81b518 100644
--- a/spec/support/shared_examples/quick_actions/issuable/issuable_quick_actions_shared_examples.rb
+++ b/spec/support/shared_examples/quick_actions/issuable/issuable_quick_actions_shared_examples.rb
@@ -18,6 +18,16 @@ RSpec.shared_examples 'issuable quick actions' do
end
end
+ let(:unlabel_expectation) do
+ ->(noteable, can_use_quick_action) {
+ if can_use_quick_action
+ expect(noteable.labels).to be_empty
+ else
+ expect(noteable.labels).not_to be_empty
+ end
+ }
+ end
+
# Quick actions shared by issues and merge requests
let(:issuable_quick_actions) do
[
@@ -136,13 +146,11 @@ RSpec.shared_examples 'issuable quick actions' do
),
QuickAction.new(
action_text: "/unlabel",
- expectation: ->(noteable, can_use_quick_action) {
- if can_use_quick_action
- expect(noteable.labels).to be_empty
- else
- expect(noteable.labels).not_to be_empty
- end
- }
+ expectation: unlabel_expectation
+ ),
+ QuickAction.new(
+ action_text: "/remove_label",
+ expectation: unlabel_expectation
),
QuickAction.new(
action_text: "/award :100:",
diff --git a/spec/support/shared_examples/requests/api/award_emoji_todo_shared_examples.rb b/spec/support/shared_examples/requests/api/award_emoji_todo_shared_examples.rb
index 88ad37d232f..9bfd1e6faa0 100644
--- a/spec/support/shared_examples/requests/api/award_emoji_todo_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/award_emoji_todo_shared_examples.rb
@@ -25,7 +25,7 @@ RSpec.shared_examples 'creating award emojis marks Todos as done' do
let(:awardable) { create(type) }
let!(:todo) { create(:todo, target: awardable, project: project, user: user) }
- it do
+ specify do
subject
expect(todo.reload.done?).to eq(expectation)
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 90ac60a6fe7..feb3ba46353 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
@@ -80,7 +80,7 @@ RSpec.shared_examples 'group and project boards query' do
cursored_query = query("after: \"#{end_cursor}\"")
post_graphql(cursored_query, current_user: current_user)
- response_data = JSON.parse(response.body)['data'][board_parent_type]['boards']['edges']
+ 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)
end
diff --git a/spec/support/shared_examples/requests/api/graphql/mutations/snippets_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/mutations/snippets_shared_examples.rb
new file mode 100644
index 00000000000..48824a4b0d2
--- /dev/null
+++ b/spec/support/shared_examples/requests/api/graphql/mutations/snippets_shared_examples.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'when the snippet is not found' do
+ let(:snippet_gid) do
+ "gid://gitlab/#{snippet.class.name}/#{non_existing_record_id}"
+ end
+
+ it_behaves_like 'a mutation that returns top-level errors',
+ errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR]
+end
diff --git a/spec/support/shared_examples/requests/snippet_shared_examples.rb b/spec/support/shared_examples/requests/snippet_shared_examples.rb
index aa7f57ae903..f830f957174 100644
--- a/spec/support/shared_examples/requests/snippet_shared_examples.rb
+++ b/spec/support/shared_examples/requests/snippet_shared_examples.rb
@@ -23,21 +23,53 @@ RSpec.shared_examples 'update with repository actions' do
context 'when the repository does not exist' do
let(:snippet) { snippet_without_repo }
- it 'creates the repository' do
- update_snippet(snippet_id: snippet.id, params: { title: 'foo' })
+ context 'when update attributes does not include file_name or content' do
+ it 'does not create the repository' do
+ update_snippet(snippet_id: snippet.id, params: { title: 'foo' })
- expect(snippet.repository).to exist
+ expect(snippet.repository).not_to exist
+ end
end
- it 'commits the file to the repository' do
- content = 'New Content'
- file_name = 'file_name.rb'
+ context 'when update attributes include file_name or content' do
+ it 'creates the repository' do
+ update_snippet(snippet_id: snippet.id, params: { title: 'foo', file_name: 'foo' })
+
+ expect(snippet.repository).to exist
+ end
+
+ it 'commits the file to the repository' do
+ content = 'New Content'
+ file_name = 'file_name.rb'
+
+ update_snippet(snippet_id: snippet.id, params: { content: content, file_name: file_name })
+
+ blob = snippet.repository.blob_at('master', file_name)
+ expect(blob).not_to be_nil
+ expect(blob.data).to eq content
+ end
+
+ context 'when save fails due to a repository creation error' do
+ let(:content) { 'File content' }
+ let(:file_name) { 'test.md' }
+
+ before do
+ allow_next_instance_of(Snippets::UpdateService) do |instance|
+ allow(instance).to receive(:create_repository_for).with(snippet).and_raise(Snippets::UpdateService::CreateRepositoryError)
+ end
+
+ update_snippet(snippet_id: snippet.id, params: { content: content, file_name: file_name })
+ end
- update_snippet(snippet_id: snippet.id, params: { content: content, file_name: file_name })
+ it 'returns 400' do
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
- blob = snippet.repository.blob_at('master', file_name)
- expect(blob).not_to be_nil
- expect(blob.data).to eq content
+ it 'does not save the changes to the snippet object' do
+ expect(snippet.content).not_to eq(content)
+ expect(snippet.file_name).not_to eq(file_name)
+ end
+ end
end
end
end
@@ -48,3 +80,21 @@ RSpec.shared_examples 'snippet response without repository URLs' do
expect(json_response).not_to have_key('http_url_to_repo')
end
end
+
+RSpec.shared_examples 'snippet blob content' do
+ it 'returns content from repository' do
+ subject
+
+ expect(response.body).to eq(snippet.blobs.first.data)
+ end
+
+ context 'when snippet repository is empty' do
+ let(:snippet) { snippet_with_empty_repo }
+
+ it 'returns content from database' do
+ subject
+
+ expect(response.body).to eq(snippet.content)
+ end
+ end
+end
diff --git a/spec/support/shared_examples/requires_variables_shared_example.rb b/spec/support/shared_examples/requires_variables_shared_example.rb
new file mode 100644
index 00000000000..2921fccf87a
--- /dev/null
+++ b/spec/support/shared_examples/requires_variables_shared_example.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'requires variables' do
+ it 'shared example requires variables to be set', :aggregate_failures do
+ variables = Array.wrap(required_variables)
+
+ variables.each do |variable_name|
+ expect { send(variable_name) }.not_to(
+ raise_error, "The following variable must be set to use this shared example: #{variable_name}"
+ )
+ end
+ end
+end
diff --git a/spec/support/shared_examples/resource_events.rb b/spec/support/shared_examples/resource_events.rb
index 963453666c9..66f5e760c37 100644
--- a/spec/support/shared_examples/resource_events.rb
+++ b/spec/support/shared_examples/resource_events.rb
@@ -83,6 +83,24 @@ shared_examples 'a resource event for issues' do
expect(events).to be_empty
end
end
+
+ describe '.by_issue_ids_and_created_at_earlier_or_equal_to' do
+ let_it_be(:event1) { create(described_class.name.underscore.to_sym, issue: issue1, created_at: '2020-03-10') }
+ let_it_be(:event2) { create(described_class.name.underscore.to_sym, issue: issue2, created_at: '2020-03-10') }
+ let_it_be(:event3) { create(described_class.name.underscore.to_sym, issue: issue1, created_at: '2020-03-12') }
+
+ it 'returns the expected records for an issue with events' do
+ events = described_class.by_issue_ids_and_created_at_earlier_or_equal_to([issue1.id, issue2.id], '2020-03-11 23:59:59')
+
+ expect(events).to contain_exactly(event1, event2)
+ end
+
+ it 'returns the expected records for an issue with no events' do
+ events = described_class.by_issue_ids_and_created_at_earlier_or_equal_to(issue3, '2020-03-12')
+
+ expect(events).to be_empty
+ end
+ end
end
shared_examples 'a resource event for merge requests' do
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 b6c4841dbd4..db5c4b45b70 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
@@ -60,7 +60,7 @@ RSpec.shared_examples 'diff file entity' do
context 'when the `single_mr_diff_view` feature is disabled' do
before do
- stub_feature_flags(single_mr_diff_view: { enabled: false, thing: project })
+ stub_feature_flags(single_mr_diff_view: false)
end
it 'contains both kinds of diffs' do
diff --git a/spec/support/shared_examples/services/boards/lists_list_service_shared_examples.rb b/spec/support/shared_examples/services/boards/lists_list_service_shared_examples.rb
index 1b7fe626aea..07a6353296d 100644
--- a/spec/support/shared_examples/services/boards/lists_list_service_shared_examples.rb
+++ b/spec/support/shared_examples/services/boards/lists_list_service_shared_examples.rb
@@ -18,6 +18,10 @@ RSpec.shared_examples 'lists list service' do
expect { service.execute(board) }.to change(board.lists, :count).by(1)
end
+ it 'does not create a backlog list when create_default_lists is false' do
+ expect { service.execute(board, create_default_lists: false) }.not_to change(board.lists, :count)
+ end
+
it "returns board's lists" do
expect(service.execute(board)).to eq [board.backlog_list, list, board.closed_list]
end
diff --git a/spec/support/shared_examples/services/measurable_service_shared_examples.rb b/spec/support/shared_examples/services/measurable_service_shared_examples.rb
new file mode 100644
index 00000000000..206c25e49af
--- /dev/null
+++ b/spec/support/shared_examples/services/measurable_service_shared_examples.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'measurable service' do
+ context 'when measurement is enabled' do
+ let!(:measuring) { Gitlab::Utils::Measuring.new(base_log_data) }
+
+ before do
+ stub_feature_flags(feature_flag => true)
+ end
+
+ it 'measure service execution with Gitlab::Utils::Measuring', :aggregate_failures do
+ expect(Gitlab::Utils::Measuring).to receive(:new).with(base_log_data).and_return(measuring)
+ expect(measuring).to receive(:with_measuring).and_call_original
+ end
+ end
+
+ context 'when measurement is disabled' do
+ it 'does not measure service execution' do
+ stub_feature_flags(feature_flag => false)
+
+ expect(Gitlab::Utils::Measuring).not_to receive(:new)
+ end
+ end
+
+ def feature_flag
+ "gitlab_service_measuring_#{described_class_name}"
+ end
+
+ def described_class_name
+ described_class.name.underscore.tr('/', '_')
+ end
+end
diff --git a/spec/support/shared_examples/services/metrics/dashboard_shared_examples.rb b/spec/support/shared_examples/services/metrics/dashboard_shared_examples.rb
index 90fcac0e55c..5dd1badbefc 100644
--- a/spec/support/shared_examples/services/metrics/dashboard_shared_examples.rb
+++ b/spec/support/shared_examples/services/metrics/dashboard_shared_examples.rb
@@ -23,7 +23,7 @@ RSpec.shared_examples 'valid dashboard service response for schema' do
end
RSpec.shared_examples 'valid dashboard service response' do
- let(:dashboard_schema) { JSON.parse(fixture_file('lib/gitlab/metrics/dashboard/schemas/dashboard.json')) }
+ let(:dashboard_schema) { Gitlab::Json.parse(fixture_file('lib/gitlab/metrics/dashboard/schemas/dashboard.json')) }
it_behaves_like 'valid dashboard service response for schema'
end
@@ -38,7 +38,7 @@ RSpec.shared_examples 'caches the unprocessed dashboard for subsequent calls' do
end
RSpec.shared_examples 'valid embedded dashboard service response' do
- let(:dashboard_schema) { JSON.parse(fixture_file('lib/gitlab/metrics/dashboard/schemas/embedded_dashboard.json')) }
+ let(:dashboard_schema) { Gitlab::Json.parse(fixture_file('lib/gitlab/metrics/dashboard/schemas/embedded_dashboard.json')) }
it_behaves_like 'valid dashboard service response for schema'
end
diff --git a/spec/support/shared_examples/services/projects/update_repository_storage_service_shared_examples.rb b/spec/support/shared_examples/services/projects/update_repository_storage_service_shared_examples.rb
index d6166ac8188..0e6ecf49cd0 100644
--- a/spec/support/shared_examples/services/projects/update_repository_storage_service_shared_examples.rb
+++ b/spec/support/shared_examples/services/projects/update_repository_storage_service_shared_examples.rb
@@ -47,9 +47,9 @@ RSpec.shared_examples 'moves repository to another storage' do |repository_type|
old_repository_path = repository.full_path
- result = subject.execute('test_second_storage')
+ result = subject.execute
- expect(result[:status]).to eq(:success)
+ expect(result).to be_success
expect(project).not_to be_repository_read_only
expect(project.repository_storage).to eq('test_second_storage')
expect(gitlab_shell.repository_exists?('default', old_project_repository_path)).to be(false)
@@ -62,7 +62,7 @@ RSpec.shared_examples 'moves repository to another storage' do |repository_type|
end
it 'does not enqueue a GC run' do
- expect { subject.execute('test_second_storage') }
+ expect { subject.execute }
.not_to change(GitGarbageCollectWorker.jobs, :count)
end
end
@@ -75,23 +75,25 @@ RSpec.shared_examples 'moves repository to another storage' do |repository_type|
it 'does not enqueue a GC run if housekeeping is disabled' do
stub_application_setting(housekeeping_enabled: false)
- expect { subject.execute('test_second_storage') }
+ expect { subject.execute }
.not_to change(GitGarbageCollectWorker.jobs, :count)
end
it 'enqueues a GC run' do
- expect { subject.execute('test_second_storage') }
+ expect { subject.execute }
.to change(GitGarbageCollectWorker.jobs, :count).by(1)
end
end
end
context 'when the filesystems are the same' do
+ let(:destination) { project.repository_storage }
+
it 'bails out and does nothing' do
- result = subject.execute(project.repository_storage)
+ result = subject.execute
- expect(result[:status]).to eq(:error)
- expect(result[:message]).to match(/SameFilesystemError/)
+ expect(result).to be_error
+ expect(result.message).to match(/SameFilesystemError/)
end
end
@@ -114,9 +116,9 @@ RSpec.shared_examples 'moves repository to another storage' do |repository_type|
expect(GitlabShellWorker).not_to receive(:perform_async)
- result = subject.execute('test_second_storage')
+ result = subject.execute
- expect(result[:status]).to eq(:error)
+ expect(result).to be_error
expect(project).not_to be_repository_read_only
expect(project.repository_storage).to eq('default')
end
@@ -142,9 +144,9 @@ RSpec.shared_examples 'moves repository to another storage' do |repository_type|
expect(GitlabShellWorker).not_to receive(:perform_async)
- result = subject.execute('test_second_storage')
+ result = subject.execute
- expect(result[:status]).to eq(:error)
+ expect(result).to be_error
expect(project).not_to be_repository_read_only
expect(project.repository_storage).to eq('default')
end
diff --git a/spec/support/shared_examples/services/resource_events/change_milestone_service_shared_examples.rb b/spec/support/shared_examples/services/resource_events/change_milestone_service_shared_examples.rb
index 77f64e5e8f8..c5f84e205cf 100644
--- a/spec/support/shared_examples/services/resource_events/change_milestone_service_shared_examples.rb
+++ b/spec/support/shared_examples/services/resource_events/change_milestone_service_shared_examples.rb
@@ -4,7 +4,7 @@ shared_examples 'a milestone events creator' do
let_it_be(:user) { create(:user) }
let(:created_at_time) { Time.utc(2019, 12, 30) }
- let(:service) { described_class.new(resource, user, created_at: created_at_time) }
+ let(:service) { described_class.new(resource, user, created_at: created_at_time, old_milestone: nil) }
context 'when milestone is present' do
let_it_be(:milestone) { create(:milestone) }
@@ -25,10 +25,13 @@ shared_examples 'a milestone events creator' do
resource.milestone = nil
end
+ let(:old_milestone) { create(:milestone, project: resource.project) }
+ let(:service) { described_class.new(resource, user, created_at: created_at_time, old_milestone: old_milestone) }
+
it 'creates the expected event records' do
expect { service.execute }.to change { ResourceMilestoneEvent.count }.by(1)
- expect_event_record(ResourceMilestoneEvent.last, action: 'remove', milestone: nil, state: 'opened')
+ expect_event_record(ResourceMilestoneEvent.last, action: 'remove', milestone: old_milestone, state: 'opened')
end
end
diff --git a/spec/support/shared_examples/services/snippets_shared_examples.rb b/spec/support/shared_examples/services/snippets_shared_examples.rb
new file mode 100644
index 00000000000..51a4a8b1cd9
--- /dev/null
+++ b/spec/support/shared_examples/services/snippets_shared_examples.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'snippets spam check is performed' do
+ shared_examples 'marked as spam' do
+ it 'marks a snippet as spam' do
+ expect(snippet).to be_spam
+ end
+
+ it 'invalidates the snippet' do
+ expect(snippet).to be_invalid
+ end
+
+ it 'creates a new spam_log' do
+ expect { snippet }
+ .to have_spam_log(title: snippet.title, noteable_type: snippet.class.name)
+ end
+
+ it 'assigns a spam_log to an issue' do
+ expect(snippet.spam_log).to eq(SpamLog.last)
+ end
+ end
+
+ let(:extra_opts) do
+ { visibility_level: Gitlab::VisibilityLevel::PUBLIC, request: double(:request, env: {}) }
+ end
+
+ before do
+ expect_next_instance_of(Spam::AkismetService) do |akismet_service|
+ expect(akismet_service).to receive_messages(spam?: true)
+ end
+ end
+
+ [true, false, nil].each do |allow_possible_spam|
+ context "when allow_possible_spam flag is #{allow_possible_spam.inspect}" do
+ before do
+ stub_feature_flags(allow_possible_spam: allow_possible_spam) unless allow_possible_spam.nil?
+ end
+
+ it_behaves_like 'marked as spam'
+ end
+ end
+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
new file mode 100644
index 00000000000..71bdd46572f
--- /dev/null
+++ b/spec/support/shared_examples/services/wiki_pages/create_service_shared_examples.rb
@@ -0,0 +1,94 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'WikiPages::CreateService#execute' do |container_type|
+ let(:container) { create(container_type, :wiki_repo) }
+ let(:user) { create(:user) }
+ let(:page_title) { 'Title' }
+
+ let(:opts) do
+ {
+ title: page_title,
+ content: 'Content for wiki page',
+ format: 'markdown'
+ }
+ end
+
+ subject(:service) { described_class.new(container: container, current_user: user, params: opts) }
+
+ it 'creates wiki page with valid attributes' do
+ page = service.execute
+
+ expect(page).to be_valid
+ expect(page).to be_persisted
+ expect(page.title).to eq(opts[:title])
+ expect(page.content).to eq(opts[:content])
+ expect(page.format).to eq(opts[:format].to_sym)
+ end
+
+ it 'executes webhooks' do
+ expect(service).to receive(:execute_hooks).once.with(WikiPage)
+
+ service.execute
+ end
+
+ it 'counts wiki page creation' do
+ counter = Gitlab::UsageDataCounters::WikiPageCounter
+
+ expect { service.execute }.to change { counter.read(:create) }.by 1
+ end
+
+ shared_examples 'correct event created' do
+ it 'creates appropriate events' do
+ # TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/216904
+ pending('group wiki support') if container_type == :group
+
+ expect { service.execute }.to change { Event.count }.by 1
+
+ expect(Event.recent.first).to have_attributes(
+ action: Event::CREATED,
+ target: have_attributes(canonical_slug: page_title)
+ )
+ end
+ end
+
+ context 'the new page is at the top level' do
+ let(:page_title) { 'root-level-page' }
+
+ include_examples 'correct event created'
+ end
+
+ context 'the new page is in a subsection' do
+ let(:page_title) { 'subsection/page' }
+
+ include_examples 'correct event created'
+ end
+
+ context 'the feature is disabled' do
+ before do
+ stub_feature_flags(wiki_events: false)
+ end
+
+ it 'does not record the activity' do
+ expect { service.execute }.not_to change(Event, :count)
+ end
+ end
+
+ context 'when the options are bad' do
+ let(:page_title) { '' }
+
+ it 'does not count a creation event' do
+ counter = Gitlab::UsageDataCounters::WikiPageCounter
+
+ expect { service.execute }.not_to change { counter.read(:create) }
+ end
+
+ it 'does not record the activity' do
+ expect { service.execute }.not_to change(Event, :count)
+ end
+
+ it 'reports the error' do
+ expect(service.execute).to be_invalid
+ .and have_attributes(errors: be_present)
+ end
+ end
+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
new file mode 100644
index 00000000000..62541eb3da9
--- /dev/null
+++ b/spec/support/shared_examples/services/wiki_pages/destroy_service_shared_examples.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'WikiPages::DestroyService#execute' do |container_type|
+ let(:container) { create(container_type) }
+
+ let(:user) { create(:user) }
+ let(:page) { create(:wiki_page) }
+
+ subject(:service) { described_class.new(container: container, current_user: user) }
+
+ it 'executes webhooks' do
+ expect(service).to receive(:execute_hooks).once.with(page)
+
+ service.execute(page)
+ end
+
+ it 'increments the delete count' do
+ counter = Gitlab::UsageDataCounters::WikiPageCounter
+
+ expect { service.execute(page) }.to change { counter.read(:delete) }.by 1
+ end
+
+ it 'creates a new wiki page deletion event' do
+ # TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/216904
+ pending('group wiki support') if container_type == :group
+
+ expect { service.execute(page) }.to change { Event.count }.by 1
+
+ expect(Event.recent.first).to have_attributes(
+ action: Event::DESTROYED,
+ target: have_attributes(canonical_slug: page.slug)
+ )
+ end
+
+ it 'does not increment the delete count if the deletion failed' do
+ counter = Gitlab::UsageDataCounters::WikiPageCounter
+
+ expect { service.execute(nil) }.not_to change { counter.read(:delete) }
+ end
+
+ context 'the feature is disabled' do
+ before do
+ stub_feature_flags(wiki_events: false)
+ end
+
+ it 'does not record the activity' do
+ expect { service.execute(page) }.not_to change(Event, :count)
+ end
+ end
+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
new file mode 100644
index 00000000000..0dfc99d043b
--- /dev/null
+++ b/spec/support/shared_examples/services/wiki_pages/update_service_shared_examples.rb
@@ -0,0 +1,98 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'WikiPages::UpdateService#execute' do |container_type|
+ let(:container) { create(container_type, :wiki_repo) }
+
+ let(:user) { create(:user) }
+ let(:page) { create(:wiki_page) }
+ let(:page_title) { 'New Title' }
+
+ let(:opts) do
+ {
+ content: 'New content for wiki page',
+ format: 'markdown',
+ message: 'New wiki message',
+ title: page_title
+ }
+ end
+
+ subject(:service) { described_class.new(container: container, current_user: user, params: opts) }
+
+ it 'updates the wiki page' do
+ updated_page = service.execute(page)
+
+ expect(updated_page).to be_valid
+ expect(updated_page.message).to eq(opts[:message])
+ expect(updated_page.content).to eq(opts[:content])
+ expect(updated_page.format).to eq(opts[:format].to_sym)
+ expect(updated_page.title).to eq(page_title)
+ end
+
+ it 'executes webhooks' do
+ expect(service).to receive(:execute_hooks).once.with(WikiPage)
+
+ service.execute(page)
+ end
+
+ it 'counts edit events' do
+ counter = Gitlab::UsageDataCounters::WikiPageCounter
+
+ expect { service.execute page }.to change { counter.read(:update) }.by 1
+ end
+
+ shared_examples 'adds activity event' do
+ it 'adds a new wiki page activity event' do
+ # TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/216904
+ pending('group wiki support') if container_type == :group
+
+ expect { service.execute(page) }.to change { Event.count }.by 1
+
+ expect(Event.recent.first).to have_attributes(
+ action: Event::UPDATED,
+ wiki_page: page,
+ target_title: page.title
+ )
+ end
+ end
+
+ context 'the page is at the top level' do
+ let(:page_title) { 'Top level page' }
+
+ include_examples 'adds activity event'
+ end
+
+ context 'the page is in a subsection' do
+ let(:page_title) { 'Subsection / secondary page' }
+
+ include_examples 'adds activity event'
+ end
+
+ context 'the feature is disabled' do
+ before do
+ stub_feature_flags(wiki_events: false)
+ end
+
+ it 'does not record the activity' do
+ expect { service.execute(page) }.not_to change(Event, :count)
+ end
+ end
+
+ context 'when the options are bad' do
+ let(:page_title) { '' }
+
+ it 'does not count an edit event' do
+ counter = Gitlab::UsageDataCounters::WikiPageCounter
+
+ expect { service.execute page }.not_to change { counter.read(:update) }
+ end
+
+ it 'does not record the activity' do
+ expect { service.execute page }.not_to change(Event, :count)
+ end
+
+ it 'reports the error' do
+ expect(service.execute(page)).to be_invalid
+ .and have_attributes(errors: be_present)
+ end
+ end
+end
diff --git a/spec/support/shared_examples/services/wikis/create_attachment_service_shared_examples.rb b/spec/support/shared_examples/services/wikis/create_attachment_service_shared_examples.rb
new file mode 100644
index 00000000000..541e332e3a1
--- /dev/null
+++ b/spec/support/shared_examples/services/wikis/create_attachment_service_shared_examples.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'Wikis::CreateAttachmentService#execute' do |container_type|
+ let(:container) { create(container_type, :wiki_repo) }
+ let(:wiki) { container.wiki }
+
+ let(:user) { create(:user) }
+ let(:file_name) { 'filename.txt' }
+ let(:file_path_regex) { %r{#{described_class::ATTACHMENT_PATH}/\h{32}/#{file_name}} }
+
+ let(:file_opts) do
+ {
+ file_name: file_name,
+ file_content: 'Content of attachment'
+ }
+ end
+ let(:opts) { file_opts }
+
+ let(:service) { Wikis::CreateAttachmentService.new(container: container, current_user: user, params: opts) }
+
+ subject(:service_execute) { service.execute[:result] }
+
+ before do
+ container.add_developer(user)
+ end
+
+ context 'creates branch if it does not exists' do
+ let(:branch_name) { 'new_branch' }
+ let(:opts) { file_opts.merge(branch_name: branch_name) }
+
+ it do
+ expect(wiki.repository.branches).to be_empty
+ expect { service.execute }.to change { wiki.repository.branches.count }.by(1)
+ expect(wiki.repository.branches.first.name).to eq branch_name
+ end
+ end
+
+ it 'adds file to the repository' do
+ expect(wiki.repository.ls_files('HEAD')).to be_empty
+
+ service.execute
+
+ files = wiki.repository.ls_files('HEAD')
+ expect(files.count).to eq 1
+ expect(files.first).to match(file_path_regex)
+ end
+
+ context 'returns' do
+ before do
+ allow(SecureRandom).to receive(:hex).and_return('fixed_hex')
+
+ service_execute
+ end
+
+ it 'returns related information', :aggregate_failures do
+ expect(service_execute[:file_name]).to eq file_name
+ expect(service_execute[:file_path]).to eq 'uploads/fixed_hex/filename.txt'
+ expect(service_execute[:branch]).to eq wiki.default_branch
+ expect(service_execute[:commit]).not_to be_empty
+ end
+ end
+end
diff --git a/spec/support/shared_examples/tasks/gitlab/import_export/measurable_shared_examples.rb b/spec/support/shared_examples/tasks/gitlab/import_export/measurable_shared_examples.rb
deleted file mode 100644
index 5950a1a53e2..00000000000
--- a/spec/support/shared_examples/tasks/gitlab/import_export/measurable_shared_examples.rb
+++ /dev/null
@@ -1,31 +0,0 @@
-# frozen_string_literal: true
-
-RSpec.shared_examples 'measurable' do
- context 'when measurement is enabled' do
- let(:measurement_enabled) { true }
-
- it 'prints measurement results' do
- expect { subject }.to output(including('Measuring enabled...', 'Number of sql calls:', 'GC stats:')).to_stdout
- end
- end
-
- context 'when measurement is not enabled' do
- let(:measurement_enabled) { false }
-
- it 'does not output measurement results' do
- expect { subject }.not_to output(/Measuring enabled.../).to_stdout
- end
- end
-
- context 'when measurement is not provided' do
- let(:measurement_enabled) { nil }
-
- it 'does not output measurement results' do
- expect { subject }.not_to output(/Measuring enabled.../).to_stdout
- end
-
- it 'does not raise any exception' do
- expect { subject }.not_to raise_error
- end
- end
-end
diff --git a/spec/support/shared_examples/workers/authorized_projects_worker_shared_example.rb b/spec/support/shared_examples/workers/authorized_projects_worker_shared_example.rb
new file mode 100644
index 00000000000..fba8b4aadbb
--- /dev/null
+++ b/spec/support/shared_examples/workers/authorized_projects_worker_shared_example.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.shared_examples "refreshes user's project authorizations" do
+ describe '#perform' do
+ let(:user) { create(:user) }
+
+ subject(:job) { described_class.new }
+
+ it "refreshes user's authorized projects" do
+ expect_any_instance_of(User).to receive(:refresh_authorized_projects)
+
+ job.perform(user.id)
+ end
+
+ context "when the user is not found" do
+ it "does nothing" do
+ expect_any_instance_of(User).not_to receive(:refresh_authorized_projects)
+
+ job.perform(-1)
+ end
+ end
+
+ it_behaves_like "an idempotent worker" do
+ let(:job_args) { user.id }
+
+ it "does not change authorizations when run twice" do
+ group = create(:group)
+ create(:project, namespace: group)
+ group.add_developer(user)
+
+ # Delete the authorization created by the after save hook of the member
+ # created above.
+ user.project_authorizations.delete_all
+
+ expect { job.perform(user.id) }.to change { user.project_authorizations.reload.size }.by(1)
+ expect { job.perform(user.id) }.not_to change { user.project_authorizations.reload.size }
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/workers/gitlab/jira_import/jira_import_workers_shared_examples.rb b/spec/support/shared_examples/workers/gitlab/jira_import/jira_import_workers_shared_examples.rb
index c0d17d6853d..ae8c82cb67c 100644
--- a/spec/support/shared_examples/workers/gitlab/jira_import/jira_import_workers_shared_examples.rb
+++ b/spec/support/shared_examples/workers/gitlab/jira_import/jira_import_workers_shared_examples.rb
@@ -20,7 +20,7 @@ shared_examples 'does not advance to next stage' do
end
end
-shared_examples 'cannot do jira import' do
+shared_examples 'cannot do Jira import' do
it 'does not advance to next stage' do
worker = described_class.new
expect(worker).not_to receive(:import)
diff --git a/spec/support/shared_examples/workers/pages_domain_cron_worker_shared_examples.rb b/spec/support/shared_examples/workers/pages_domain_cron_worker_shared_examples.rb
index 9e8102aea53..c79e3ed7d21 100644
--- a/spec/support/shared_examples/workers/pages_domain_cron_worker_shared_examples.rb
+++ b/spec/support/shared_examples/workers/pages_domain_cron_worker_shared_examples.rb
@@ -3,12 +3,14 @@
RSpec.shared_examples 'a pages cronjob scheduling jobs with context' do |scheduled_worker_class|
let(:worker) { described_class.new }
- it 'does not cause extra queries for multiple domains' do
- control = ActiveRecord::QueryRecorder.new { worker.perform }
+ context 'with RequestStore enabled', :request_store do
+ it 'does not cause extra queries for multiple domains' do
+ control = ActiveRecord::QueryRecorder.new { worker.perform }
- extra_domain
+ extra_domain
- expect { worker.perform }.not_to exceed_query_limit(control)
+ expect { worker.perform }.not_to exceed_query_limit(control)
+ end
end
it 'schedules the renewal with a context' do
diff --git a/spec/support/shared_examples/workers/reactive_cacheable_shared_examples.rb b/spec/support/shared_examples/workers/reactive_cacheable_shared_examples.rb
new file mode 100644
index 00000000000..0bbd0e2a90d
--- /dev/null
+++ b/spec/support/shared_examples/workers/reactive_cacheable_shared_examples.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'reactive cacheable worker' do
+ describe '#perform' do
+ context 'when reactive cache worker class is found' do
+ let!(:cluster) { create(:cluster, :project, :provided_by_gcp) }
+ let(:project) { cluster.project }
+ let!(:environment) { create(:environment, project: project) }
+
+ it 'calls #exclusively_update_reactive_cache!' do
+ expect_any_instance_of(Environment).to receive(:exclusively_update_reactive_cache!)
+
+ described_class.new.perform("Environment", environment.id)
+ end
+
+ context 'when ReactiveCaching::ExceededReactiveCacheLimit is raised' do
+ it 'avoids failing the job and tracks via Gitlab::ErrorTracking' do
+ allow_any_instance_of(Environment).to receive(:exclusively_update_reactive_cache!)
+ .and_raise(ReactiveCaching::ExceededReactiveCacheLimit)
+
+ expect(Gitlab::ErrorTracking).to receive(:track_exception)
+ .with(kind_of(ReactiveCaching::ExceededReactiveCacheLimit))
+
+ described_class.new.perform("Environment", environment.id)
+ end
+ end
+ end
+
+ context 'when reactive cache worker class is not found' do
+ it 'raises no error' do
+ expect { described_class.new.perform("Environment", -1) }.not_to raise_error
+ end
+ end
+
+ context 'when reactive cache worker class is invalid' do
+ it 'raises no error' do
+ expect { described_class.new.perform("FooBarKux", -1) }.not_to raise_error
+ end
+ end
+ end
+
+ describe 'worker context' do
+ it 'sets the related class on the job' do
+ described_class.perform_async('Environment', 1, 'other', 'argument')
+
+ scheduled_job = described_class.jobs.first
+
+ expect(scheduled_job).to include('meta.related_class' => 'Environment')
+ end
+
+ it 'sets the related class on the job when it was passed as a class' do
+ described_class.perform_async(Project, 1, 'other', 'argument')
+
+ scheduled_job = described_class.jobs.first
+
+ expect(scheduled_job).to include('meta.related_class' => 'Project')
+ end
+ end
+end
diff --git a/spec/support/sidekiq.rb b/spec/support/sidekiq.rb
index 9fa8df39019..374997af1ec 100644
--- a/spec/support/sidekiq.rb
+++ b/spec/support/sidekiq.rb
@@ -1,14 +1,23 @@
# frozen_string_literal: true
RSpec.configure do |config|
+ def gitlab_sidekiq_inline(&block)
+ # We need to cleanup the queues before running jobs in specs because the
+ # middleware might have written to redis
+ redis_queues_cleanup!
+ Sidekiq::Testing.inline!(&block)
+ ensure
+ redis_queues_cleanup!
+ end
+
# As we'll review the examples with this tag, we should either:
# - fix the example to not require Sidekiq inline mode (and remove this tag)
# - explicitly keep the inline mode and change the tag for `:sidekiq_inline` instead
config.around(:example, :sidekiq_might_not_need_inline) do |example|
- Sidekiq::Testing.inline! { example.run }
+ gitlab_sidekiq_inline { example.run }
end
config.around(:example, :sidekiq_inline) do |example|
- Sidekiq::Testing.inline! { example.run }
+ gitlab_sidekiq_inline { example.run }
end
end
diff --git a/spec/support/sidekiq_middleware.rb b/spec/support/sidekiq_middleware.rb
index 1380f4394d8..62f81ef1669 100644
--- a/spec/support/sidekiq_middleware.rb
+++ b/spec/support/sidekiq_middleware.rb
@@ -31,3 +31,16 @@ class DisableQueryLimit
end
end
end
+
+# When running `Sidekiq::Testing.inline!` each job is using a request-store.
+# This middleware makes sure the values don't leak into eachother.
+class IsolatedRequestStore
+ def call(_worker, msg, queue)
+ old_store = RequestStore.store.dup
+ RequestStore.clear!
+
+ yield
+
+ RequestStore.store = old_store
+ end
+end
diff --git a/spec/support/unicorn.rb b/spec/support/unicorn.rb
new file mode 100644
index 00000000000..0b01fc9e26c
--- /dev/null
+++ b/spec/support/unicorn.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+REQUEST_CLASSES = [
+ ::Grape::Request,
+ ::Rack::Request
+].freeze
+
+def request_body_class
+ return ::Unicorn::TeeInput if defined?(::Unicorn)
+
+ Class.new(StringIO) do
+ def string
+ raise NotImplementedError, '#string is only valid under Puma which uses StringIO, use #read instead'
+ end
+ end
+end
+
+RSpec.configure do |config|
+ config.before(:each, :unicorn) do
+ REQUEST_CLASSES.each do |request_class|
+ allow_any_instance_of(request_class)
+ .to receive(:body).and_wrap_original do |m, *args|
+ request_body_class.new(m.call(*args).read)
+ end
+ end
+ end
+end
diff --git a/spec/support/webmock.rb b/spec/support/webmock.rb
index 57acc3b63b1..f952f7f0985 100644
--- a/spec/support/webmock.rb
+++ b/spec/support/webmock.rb
@@ -15,4 +15,20 @@ def webmock_allowed_hosts
end.compact.uniq
end
-WebMock.disable_net_connect!(allow_localhost: true, allow: webmock_allowed_hosts)
+# This prevents Selenium/WebMock from spawning thousands of connections
+# while waiting for an element to appear via Capybara's find:
+# https://github.com/teamcapybara/capybara/issues/2322#issuecomment-619321520
+def webmock_enable_with_http_connect_on_start!
+ webmock_enable!(net_http_connect_on_start: true)
+end
+
+def webmock_enable!(options = {})
+ WebMock.disable_net_connect!(
+ {
+ allow_localhost: true,
+ allow: webmock_allowed_hosts
+ }.merge(options)
+ )
+end
+
+webmock_enable!