diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-05-20 14:34:42 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-05-20 14:34:42 +0000 |
commit | 9f46488805e86b1bc341ea1620b866016c2ce5ed (patch) | |
tree | f9748c7e287041e37d6da49e0a29c9511dc34768 /spec/support | |
parent | dfc92d081ea0332d69c8aca2f0e745cb48ae5e6d (diff) | |
download | gitlab-ce-9f46488805e86b1bc341ea1620b866016c2ce5ed.tar.gz |
Add latest changes from gitlab-org/gitlab@13-0-stable-ee
Diffstat (limited to 'spec/support')
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! |