summaryrefslogtreecommitdiff
path: root/spec/support
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-09-19 01:45:44 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2020-09-19 01:45:44 +0000
commit85dc423f7090da0a52c73eb66faf22ddb20efff9 (patch)
tree9160f299afd8c80c038f08e1545be119f5e3f1e1 /spec/support
parent15c2c8c66dbe422588e5411eee7e68f1fa440bb8 (diff)
downloadgitlab-ce-85dc423f7090da0a52c73eb66faf22ddb20efff9.tar.gz
Add latest changes from gitlab-org/gitlab@13-4-stable-ee
Diffstat (limited to 'spec/support')
-rw-r--r--spec/support/factory_default.rb11
-rw-r--r--spec/support/forgery_protection.rb2
-rw-r--r--spec/support/gitlab_stubs/gitlab_ci_for_sast.yml2
-rw-r--r--spec/support/helpers/ci/source_pipeline_helpers.rb13
-rw-r--r--spec/support/helpers/dns_helpers.rb2
-rw-r--r--spec/support/helpers/docs_screenshot_helpers.rb39
-rw-r--r--spec/support/helpers/fake_u2f_device.rb3
-rw-r--r--spec/support/helpers/fake_webauthn_device.rb74
-rw-r--r--spec/support/helpers/feature_flag_helpers.rb95
-rw-r--r--spec/support/helpers/features/editor_lite_spec_helpers.rb29
-rw-r--r--spec/support/helpers/features/releases_helpers.rb117
-rw-r--r--spec/support/helpers/features/snippet_helpers.rb51
-rw-r--r--spec/support/helpers/features/two_factor_helpers.rb74
-rw-r--r--spec/support/helpers/graphql_helpers.rb33
-rw-r--r--spec/support/helpers/jira_service_helper.rb3
-rw-r--r--spec/support/helpers/login_helpers.rb5
-rw-r--r--spec/support/helpers/markdown_feature.rb8
-rw-r--r--spec/support/helpers/metrics_dashboard_helpers.rb2
-rw-r--r--spec/support/helpers/multipart_helpers.rb91
-rw-r--r--spec/support/helpers/navbar_structure_helper.rb4
-rw-r--r--spec/support/helpers/next_found_instance_of.rb33
-rw-r--r--spec/support/helpers/project_forks_helper.rb16
-rw-r--r--spec/support/helpers/repo_helpers.rb2
-rw-r--r--spec/support/helpers/snowplow_helpers.rb53
-rw-r--r--spec/support/helpers/stub_object_storage.rb2
-rw-r--r--spec/support/helpers/stubbed_feature.rb5
-rw-r--r--spec/support/helpers/test_env.rb19
-rw-r--r--spec/support/helpers/usage_data_helpers.rb17
-rw-r--r--spec/support/helpers/wiki_helpers.rb5
-rw-r--r--spec/support/helpers/workhorse_helpers.rb18
-rw-r--r--spec/support/import_export/configuration_helper.rb4
-rw-r--r--spec/support/matchers/markdown_matchers.rb9
-rw-r--r--spec/support/shared_contexts/features/file_uploads_shared_context.rb7
-rw-r--r--spec/support/shared_contexts/finders/users_finder_shared_contexts.rb1
-rw-r--r--spec/support/shared_contexts/lib/gitlab/middleware/multipart_shared_contexts.rb106
-rw-r--r--spec/support/shared_contexts/navbar_structure_context.rb10
-rw-r--r--spec/support/shared_contexts/policies/project_policy_shared_context.rb75
-rw-r--r--spec/support/shared_contexts/project_service_jira_context.rb2
-rw-r--r--spec/support/shared_contexts/project_service_shared_context.rb20
-rw-r--r--spec/support/shared_contexts/requests/api/conan_packages_shared_context.rb75
-rw-r--r--spec/support/shared_contexts/serializers/group_group_link_shared_context.rb17
-rw-r--r--spec/support/shared_contexts/services_shared_context.rb3
-rw-r--r--spec/support/shared_examples/ci/jobs_shared_examples.rb27
-rw-r--r--spec/support/shared_examples/controllers/binary_blob_shared_examples.rb86
-rw-r--r--spec/support/shared_examples/controllers/instance_statistics_controllers_shared_examples.rb51
-rw-r--r--spec/support/shared_examples/controllers/issuables_list_metadata_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/controllers/unique_hll_events_examples.rb47
-rw-r--r--spec/support/shared_examples/controllers/unique_visits_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb49
-rw-r--r--spec/support/shared_examples/features/2fa_shared_examples.rb108
-rw-r--r--spec/support/shared_examples/features/editable_merge_request_shared_examples.rb13
-rw-r--r--spec/support/shared_examples/features/error_tracking_shared_example.rb4
-rw-r--r--spec/support/shared_examples/features/file_uploads_shared_examples.rb29
-rw-r--r--spec/support/shared_examples/features/packages_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/graphql/members_shared_examples.rb61
-rw-r--r--spec/support/shared_examples/graphql/mutation_shared_examples.rb7
-rw-r--r--spec/support/shared_examples/graphql/sorted_paginated_query_shared_examples.rb3
-rw-r--r--spec/support/shared_examples/graphql/types/gitlab_style_deprecations_shared_examples.rb52
-rw-r--r--spec/support/shared_examples/lib/banzai/reference_parser_shared_examples.rb6
-rw-r--r--spec/support/shared_examples/lib/gitlab/alert_management/payload.rb34
-rw-r--r--spec/support/shared_examples/lib/gitlab/auth/atlassian_identity_shared_examples.rb10
-rw-r--r--spec/support/shared_examples/lib/gitlab/background_migration/mentions_migration_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/lib/gitlab/kubernetes/network_policy_common_shared_examples.rb13
-rw-r--r--spec/support/shared_examples/lib/gitlab/middleware/multipart_shared_examples.rb145
-rw-r--r--spec/support/shared_examples/lib/gitlab/project_search_results_shared_examples.rb79
-rw-r--r--spec/support/shared_examples/lib/gitlab/repo_type_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/lib/gitlab/search/recent_items.rb87
-rw-r--r--spec/support/shared_examples/lib/gitlab/search_issue_state_filter_shared_examples.rb48
-rw-r--r--spec/support/shared_examples/lib/gitlab/sql/set_operator_shared_examples.rb46
-rw-r--r--spec/support/shared_examples/lib/gitlab/usage_data_counters/incident_management_activity_shared_examples.rb32
-rw-r--r--spec/support/shared_examples/models/chat_slash_commands_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/models/cluster_application_helm_cert_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/models/concerns/from_set_operator_shared_examples.rb42
-rw-r--r--spec/support/shared_examples/models/concerns/limitable_shared_examples.rb13
-rw-r--r--spec/support/shared_examples/models/concerns/timebox_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/models/diff_note_after_commit_shared_examples.rb8
-rw-r--r--spec/support/shared_examples/models/diff_positionable_note_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/models/member_shared_examples.rb10
-rw-r--r--spec/support/shared_examples/models/members_notifications_shared_example.rb2
-rw-r--r--spec/support/shared_examples/models/mentionable_shared_examples.rb10
-rw-r--r--spec/support/shared_examples/models/project_latest_successful_build_for_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/models/relative_positioning_shared_examples.rb419
-rw-r--r--spec/support/shared_examples/models/slack_mattermost_notifications_shared_examples.rb18
-rw-r--r--spec/support/shared_examples/models/throttled_touch_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/models/update_project_statistics_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/models/wiki_shared_examples.rb12
-rw-r--r--spec/support/shared_examples/models/with_uploads_shared_examples.rb10
-rw-r--r--spec/support/shared_examples/path_extraction_shared_examples.rb14
-rw-r--r--spec/support/shared_examples/policies/project_policy_shared_examples.rb44
-rw-r--r--spec/support/shared_examples/requests/api/award_emoji_todo_shared_examples.rb7
-rw-r--r--spec/support/shared_examples/requests/api/boards_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/requests/api/composer_packages_shared_examples.rb24
-rw-r--r--spec/support/shared_examples/requests/api/conan_packages_shared_examples.rb843
-rw-r--r--spec/support/shared_examples/requests/api/custom_attributes_shared_examples.rb16
-rw-r--r--spec/support/shared_examples/requests/api/graphql/mutations/snippets_shared_examples.rb39
-rw-r--r--spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/requests/api/packages_shared_examples.rb85
-rw-r--r--spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/requests/api/snippets_shared_examples.rb139
-rw-r--r--spec/support/shared_examples/requests/snippet_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/serializers/diff_file_entity_shared_examples.rb10
-rw-r--r--spec/support/shared_examples/services/alert_management_shared_examples.rb38
-rw-r--r--spec/support/shared_examples/services/common_system_notes_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/services/incident_shared_examples.rb47
-rw-r--r--spec/support/shared_examples/services/issuable_shared_examples.rb31
-rw-r--r--spec/support/shared_examples/services/merge_request_shared_examples.rb62
-rw-r--r--spec/support/shared_examples/services/packages_shared_examples.rb9
-rw-r--r--spec/support/shared_examples/services/snippets_shared_examples.rb17
-rw-r--r--spec/support/shared_examples/services/wiki_pages/destroy_service_shared_examples.rb18
-rw-r--r--spec/support/shared_examples/services/wiki_pages/update_service_shared_examples.rb10
-rw-r--r--spec/support/shared_examples/uploaders/workers/object_storage/migrate_uploads_shared_examples.rb4
-rw-r--r--spec/support/snowplow.rb22
-rw-r--r--spec/support/test_reports/test_reports_helper.rb4
113 files changed, 3565 insertions, 700 deletions
diff --git a/spec/support/factory_default.rb b/spec/support/factory_default.rb
new file mode 100644
index 00000000000..e116c28f132
--- /dev/null
+++ b/spec/support/factory_default.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+RSpec.configure do |config|
+ config.after do |ex|
+ TestProf::FactoryDefault.reset unless ex.metadata[:factory_default] == :keep
+ end
+
+ config.after(:all) do
+ TestProf::FactoryDefault.reset
+ end
+end
diff --git a/spec/support/forgery_protection.rb b/spec/support/forgery_protection.rb
index 1d6ea013292..d12e99b17c4 100644
--- a/spec/support/forgery_protection.rb
+++ b/spec/support/forgery_protection.rb
@@ -8,7 +8,7 @@ module ForgeryProtection
ActionController::Base.allow_forgery_protection = false
end
- module_function :with_forgery_protection
+ module_function :with_forgery_protection # rubocop: disable Style/AccessModifierDeclarations
end
RSpec.configure do |config|
diff --git a/spec/support/gitlab_stubs/gitlab_ci_for_sast.yml b/spec/support/gitlab_stubs/gitlab_ci_for_sast.yml
index 4134660e4b9..c4f3c3aace2 100644
--- a/spec/support/gitlab_stubs/gitlab_ci_for_sast.yml
+++ b/spec/support/gitlab_stubs/gitlab_ci_for_sast.yml
@@ -4,6 +4,7 @@ include:
variables:
SECURE_ANALYZERS_PREFIX: "registry.gitlab.com/gitlab-org/security-products/analyzers2"
SAST_EXCLUDED_PATHS: "spec, executables"
+ SAST_DEFAULT_ANALYZERS: "bandit, gosec"
stages:
- our_custom_security_stage
@@ -11,3 +12,4 @@ sast:
stage: our_custom_security_stage
variables:
SEARCH_MAX_DEPTH: 8
+ SAST_BRAKEMAN_LEVEL: 2
diff --git a/spec/support/helpers/ci/source_pipeline_helpers.rb b/spec/support/helpers/ci/source_pipeline_helpers.rb
new file mode 100644
index 00000000000..b99f499cc16
--- /dev/null
+++ b/spec/support/helpers/ci/source_pipeline_helpers.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Ci
+ module SourcePipelineHelpers
+ def create_source_pipeline(upstream, downstream)
+ create(:ci_sources_pipeline,
+ source_job: create(:ci_build, pipeline: upstream),
+ source_project: upstream.project,
+ pipeline: downstream,
+ project: downstream.project)
+ end
+ end
+end
diff --git a/spec/support/helpers/dns_helpers.rb b/spec/support/helpers/dns_helpers.rb
index 29be4da6902..1795b0a9ac3 100644
--- a/spec/support/helpers/dns_helpers.rb
+++ b/spec/support/helpers/dns_helpers.rb
@@ -25,6 +25,6 @@ module DnsHelpers
def permit_local_dns!
local_addresses = /\A(127|10)\.0\.0\.\d{1,3}|(192\.168|172\.16)\.\d{1,3}\.\d{1,3}|0\.0\.0\.0|localhost\z/i
allow(Addrinfo).to receive(:getaddrinfo).with(local_addresses, anything, nil, :STREAM).and_call_original
- allow(Addrinfo).to receive(:getaddrinfo).with(local_addresses, anything, nil, :STREAM, anything, anything).and_call_original
+ allow(Addrinfo).to receive(:getaddrinfo).with(local_addresses, anything, nil, :STREAM, anything, anything, any_args).and_call_original
end
end
diff --git a/spec/support/helpers/docs_screenshot_helpers.rb b/spec/support/helpers/docs_screenshot_helpers.rb
new file mode 100644
index 00000000000..aa3aad0a740
--- /dev/null
+++ b/spec/support/helpers/docs_screenshot_helpers.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+require 'fileutils'
+require 'mini_magick'
+
+module DocsScreenshotHelpers
+ extend ActiveSupport::Concern
+
+ def set_crop_data(element, padding)
+ @crop_element = element
+ @crop_padding = padding
+ end
+
+ def crop_image_screenshot(path)
+ element_rect = @crop_element.evaluate_script("this.getBoundingClientRect()")
+
+ width = element_rect['width'] + (@crop_padding * 2)
+ height = element_rect['height'] + (@crop_padding * 2)
+
+ x = element_rect['x'] - @crop_padding
+ y = element_rect['y'] - @crop_padding
+
+ image = MiniMagick::Image.new(path)
+ image.crop "#{width}x#{height}+#{x}+#{y}"
+ end
+
+ included do |base|
+ after do |example|
+ filename = "#{example.description}.png"
+ path = File.expand_path(filename, 'doc/')
+ page.save_screenshot(path)
+
+ if @crop_element
+ crop_image_screenshot(path)
+ set_crop_data(nil, nil)
+ end
+ end
+ end
+end
diff --git a/spec/support/helpers/fake_u2f_device.rb b/spec/support/helpers/fake_u2f_device.rb
index f765b277175..2ed1222ebd3 100644
--- a/spec/support/helpers/fake_u2f_device.rb
+++ b/spec/support/helpers/fake_u2f_device.rb
@@ -3,9 +3,10 @@
class FakeU2fDevice
attr_reader :name
- def initialize(page, name)
+ def initialize(page, name, device = nil)
@page = page
@name = name
+ @u2f_device = device
end
def respond_to_u2f_registration
diff --git a/spec/support/helpers/fake_webauthn_device.rb b/spec/support/helpers/fake_webauthn_device.rb
new file mode 100644
index 00000000000..d2c2f7d6bf3
--- /dev/null
+++ b/spec/support/helpers/fake_webauthn_device.rb
@@ -0,0 +1,74 @@
+# frozen_string_literal: true
+require 'webauthn/fake_client'
+
+class FakeWebauthnDevice
+ attr_reader :name
+
+ def initialize(page, name, device = nil)
+ @page = page
+ @name = name
+ @webauthn_device = device
+ end
+
+ def respond_to_webauthn_registration
+ app_id = @page.evaluate_script('gon.webauthn.app_id')
+ challenge = @page.evaluate_script('gon.webauthn.options.challenge')
+
+ json_response = webauthn_device(app_id).create(challenge: challenge).to_json # rubocop:disable Rails/SaveBang
+ @page.execute_script <<~JS
+ var result = #{json_response};
+ result.getClientExtensionResults = () => ({});
+ navigator.credentials.create = function(_) {
+ return Promise.resolve(result);
+ };
+ JS
+ end
+
+ def respond_to_webauthn_authentication
+ app_id = @page.evaluate_script('JSON.parse(gon.webauthn.options).extensions.appid')
+ challenge = @page.evaluate_script('JSON.parse(gon.webauthn.options).challenge')
+
+ begin
+ json_response = webauthn_device(app_id).get(challenge: challenge).to_json
+
+ rescue RuntimeError
+ # A runtime error is raised from fake webauthn if no credentials have been registered yet.
+ # To be able to test non registered devices, credentials are created ad-hoc
+ webauthn_device(app_id).create # rubocop:disable Rails/SaveBang
+ json_response = webauthn_device(app_id).get(challenge: challenge).to_json
+ end
+
+ @page.execute_script <<~JS
+ var result = #{json_response};
+ result.getClientExtensionResults = () => ({});
+ navigator.credentials.get = function(_) {
+ return Promise.resolve(result);
+ };
+ JS
+ @page.click_link('Try again?', href: false)
+ end
+
+ def fake_webauthn_authentication
+ @page.execute_script <<~JS
+ const mockResponse = {
+ type: 'public-key',
+ id: '',
+ rawId: '',
+ response: { clientDataJSON: '', authenticatorData: '', signature: '', userHandle: '' },
+ getClientExtensionResults: () => {},
+ };
+ window.gl.resolveWebauthn(mockResponse);
+ JS
+ end
+
+ def add_credential(app_id, credential_id, credential_key)
+ credentials = { URI.parse(app_id).host => { credential_id => { credential_key: credential_key, sign_count: 0 } } }
+ webauthn_device(app_id).send(:authenticator).instance_variable_set(:@credentials, credentials)
+ end
+
+ private
+
+ def webauthn_device(app_id)
+ @webauthn_device ||= WebAuthn::FakeClient.new(app_id)
+ end
+end
diff --git a/spec/support/helpers/feature_flag_helpers.rb b/spec/support/helpers/feature_flag_helpers.rb
new file mode 100644
index 00000000000..93cd915879b
--- /dev/null
+++ b/spec/support/helpers/feature_flag_helpers.rb
@@ -0,0 +1,95 @@
+# frozen_string_literal: true
+
+module FeatureFlagHelpers
+ def create_flag(project, name, active = true, description: nil, version: Operations::FeatureFlag.versions['legacy_flag'])
+ create(:operations_feature_flag, name: name, active: active, version: version,
+ description: description, project: project)
+ end
+
+ def create_scope(feature_flag, environment_scope, active = true, strategies = [{ name: "default", parameters: {} }])
+ create(:operations_feature_flag_scope,
+ feature_flag: feature_flag,
+ environment_scope: environment_scope,
+ active: active,
+ strategies: strategies)
+ end
+
+ def within_feature_flag_row(index)
+ within ".gl-responsive-table-row:nth-child(#{index + 1})" do
+ yield
+ end
+ end
+
+ def within_feature_flag_scopes
+ within '.js-feature-flag-environments' do
+ yield
+ end
+ end
+
+ def within_scope_row(index)
+ within ".gl-responsive-table-row:nth-child(#{index + 1})" do
+ yield
+ end
+ end
+
+ def within_strategy_row(index)
+ within ".feature-flags-form > fieldset > div[data-testid='feature-flag-strategies'] > div:nth-child(#{index})" do
+ yield
+ end
+ end
+
+ def within_environment_spec
+ within '.table-section:nth-child(1)' do
+ yield
+ end
+ end
+
+ def within_status
+ within '.table-section:nth-child(2)' do
+ yield
+ end
+ end
+
+ def within_delete
+ within '.table-section:nth-child(4)' do
+ yield
+ end
+ end
+
+ def edit_feature_flag_button
+ find('.js-feature-flag-edit-button')
+ end
+
+ def delete_strategy_button
+ find("button[data-testid='delete-strategy-button']")
+ end
+
+ def add_linked_issue_button
+ find('.js-issue-count-badge-add-button')
+ end
+
+ def remove_linked_issue_button
+ find('.js-issue-item-remove-button')
+ end
+
+ def status_toggle_button
+ find('[data-testid="feature-flag-status-toggle"] button')
+ end
+
+ def expect_status_toggle_button_to_be_checked
+ expect(page).to have_css('[data-testid="feature-flag-status-toggle"] button.is-checked')
+ end
+
+ def expect_status_toggle_button_not_to_be_checked
+ expect(page).to have_css('[data-testid="feature-flag-status-toggle"] button:not(.is-checked)')
+ end
+
+ def expect_status_toggle_button_to_be_disabled
+ expect(page).to have_css('[data-testid="feature-flag-status-toggle"] button.is-disabled')
+ end
+
+ def expect_user_to_see_feature_flags_index_page
+ expect(page).to have_text('Feature Flags')
+ expect(page).to have_text('Lists')
+ end
+end
diff --git a/spec/support/helpers/features/editor_lite_spec_helpers.rb b/spec/support/helpers/features/editor_lite_spec_helpers.rb
new file mode 100644
index 00000000000..0a67e753379
--- /dev/null
+++ b/spec/support/helpers/features/editor_lite_spec_helpers.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+# These helpers help you interact within the Editor Lite (single-file editor, snippets, etc.).
+#
+module Spec
+ module Support
+ module Helpers
+ module Features
+ module EditorLiteSpecHelpers
+ include ActionView::Helpers::JavaScriptHelper
+
+ def editor_set_value(value)
+ editor = find('.monaco-editor')
+ uri = editor['data-uri']
+
+ execute_script("monaco.editor.getModel('#{uri}').setValue('#{escape_javascript(value)}')")
+ end
+
+ def editor_get_value
+ editor = find('.monaco-editor')
+ uri = editor['data-uri']
+
+ evaluate_script("monaco.editor.getModel('#{uri}').getValue()")
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/support/helpers/features/releases_helpers.rb b/spec/support/helpers/features/releases_helpers.rb
new file mode 100644
index 00000000000..0d46918b05c
--- /dev/null
+++ b/spec/support/helpers/features/releases_helpers.rb
@@ -0,0 +1,117 @@
+# frozen_string_literal: true
+
+# These helpers fill fields on the "New Release" and
+# "Edit Release" pages. They use the keyboard to navigate
+# from one field to the next and assume that when
+# they are called, the field to be filled out is already focused.
+#
+# Usage:
+# describe "..." do
+# include Spec::Support::Helpers::Features::ReleasesHelpers
+# ...
+#
+# fill_tag_name("v1.0")
+# select_create_from("my-feature-branch")
+#
+module Spec
+ module Support
+ module Helpers
+ module Features
+ module ReleasesHelpers
+ # Returns the element that currently has keyboard focus.
+ # Reminder that this returns a Selenium::WebDriver::Element
+ # _not_ a Capybara::Node::Element
+ def focused_element
+ page.driver.browser.switch_to.active_element
+ end
+
+ def fill_tag_name(tag_name, and_tab: true)
+ expect(focused_element).to eq(find_field('Tag name').native)
+
+ focused_element.send_keys(tag_name)
+
+ focused_element.send_keys(:tab) if and_tab
+ end
+
+ def select_create_from(branch_name, and_tab: true)
+ expect(focused_element).to eq(find('[data-testid="create-from-field"] button').native)
+
+ focused_element.send_keys(:enter)
+
+ # Wait for the dropdown to be rendered
+ page.find('.ref-selector .dropdown-menu')
+
+ # Pressing Enter in the search box shouldn't submit the form
+ focused_element.send_keys(branch_name, :enter)
+
+ # Wait for the search to return
+ page.find('.ref-selector .dropdown-item', text: branch_name, match: :first)
+
+ focused_element.send_keys(:arrow_down, :enter)
+
+ focused_element.send_keys(:tab) if and_tab
+ end
+
+ def fill_release_title(release_title, and_tab: true)
+ expect(focused_element).to eq(find_field('Release title').native)
+
+ focused_element.send_keys(release_title)
+
+ focused_element.send_keys(:tab) if and_tab
+ end
+
+ def select_milestone(milestone_title, and_tab: true)
+ expect(focused_element).to eq(find('[data-testid="milestones-field"] button').native)
+
+ focused_element.send_keys(:enter)
+
+ # Wait for the dropdown to be rendered
+ page.find('.project-milestone-combobox .dropdown-menu')
+
+ # Clear any existing input
+ focused_element.attribute('value').length.times { focused_element.send_keys(:backspace) }
+
+ # Pressing Enter in the search box shouldn't submit the form
+ focused_element.send_keys(milestone_title, :enter)
+
+ # Wait for the search to return
+ page.find('.project-milestone-combobox .dropdown-item', text: milestone_title, match: :first)
+
+ focused_element.send_keys(:arrow_down, :arrow_down, :enter)
+
+ focused_element.send_keys(:tab) if and_tab
+ end
+
+ def fill_release_notes(release_notes, and_tab: true)
+ expect(focused_element).to eq(find_field('Release notes').native)
+
+ focused_element.send_keys(release_notes)
+
+ # Tab past the links at the bottom of the editor
+ focused_element.send_keys(:tab, :tab, :tab) if and_tab
+ end
+
+ def fill_asset_link(link, and_tab: true)
+ expect(focused_element['id']).to start_with('asset-url-')
+
+ focused_element.send_keys(link[:url], :tab, link[:title], :tab, link[:type])
+
+ # Tab past the "Remove asset link" button
+ focused_element.send_keys(:tab, :tab) if and_tab
+ end
+
+ # Click "Add another link" and tab back to the beginning of the new row
+ def add_another_asset_link
+ expect(focused_element).to eq(find_button('Add another link').native)
+
+ focused_element.send_keys(:enter,
+ [:shift, :tab],
+ [:shift, :tab],
+ [:shift, :tab],
+ [:shift, :tab])
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/support/helpers/features/snippet_helpers.rb b/spec/support/helpers/features/snippet_helpers.rb
new file mode 100644
index 00000000000..c01d179770c
--- /dev/null
+++ b/spec/support/helpers/features/snippet_helpers.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+# These helpers help you interact within the Editor Lite (single-file editor, snippets, etc.).
+#
+module Spec
+ module Support
+ module Helpers
+ module Features
+ module SnippetSpecHelpers
+ include ActionView::Helpers::JavaScriptHelper
+ include Spec::Support::Helpers::Features::EditorLiteSpecHelpers
+
+ def snippet_get_first_blob_path
+ page.find_field(snippet_blob_path_field, match: :first).value
+ end
+
+ def snippet_get_first_blob_value
+ page.find(snippet_blob_content_selector, match: :first)
+ end
+
+ def snippet_description_value
+ page.find_field(snippet_description_field).value
+ end
+
+ def snippet_fill_in_form(title:, content:, description: '')
+ # fill_in snippet_title_field, with: title
+ # editor_set_value(content)
+ fill_in snippet_title_field, with: title
+
+ if description
+ # Click placeholder first to expand full description field
+ description_field.click
+ fill_in snippet_description_field, with: description
+ end
+
+ page.within('.file-editor') do
+ el = find('.inputarea')
+ el.send_keys content
+ end
+ end
+
+ private
+
+ def description_field
+ find('.js-description-input').find('input,textarea')
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/support/helpers/features/two_factor_helpers.rb b/spec/support/helpers/features/two_factor_helpers.rb
new file mode 100644
index 00000000000..08a7665201f
--- /dev/null
+++ b/spec/support/helpers/features/two_factor_helpers.rb
@@ -0,0 +1,74 @@
+# frozen_string_literal: true
+# These helpers allow you to manage and register
+# U2F and WebAuthn devices
+#
+# Usage:
+# describe "..." do
+# include Spec::Support::Helpers::Features::TwoFactorHelpers
+# ...
+#
+# manage_two_factor_authentication
+#
+module Spec
+ module Support
+ module Helpers
+ module Features
+ module TwoFactorHelpers
+ def manage_two_factor_authentication
+ click_on 'Manage two-factor authentication'
+ expect(page).to have_content("Set up new device")
+ wait_for_requests
+ end
+
+ def register_u2f_device(u2f_device = nil, name: 'My device')
+ u2f_device ||= FakeU2fDevice.new(page, name)
+ u2f_device.respond_to_u2f_registration
+ click_on 'Set up new device'
+ expect(page).to have_content('Your device was successfully set up')
+ fill_in "Pick a name", with: name
+ click_on 'Register device'
+ u2f_device
+ end
+
+ # Registers webauthn device via UI
+ def register_webauthn_device(webauthn_device = nil, name: 'My device')
+ webauthn_device ||= FakeWebauthnDevice.new(page, name)
+ webauthn_device.respond_to_webauthn_registration
+ click_on 'Set up new device'
+ expect(page).to have_content('Your device was successfully set up')
+ fill_in 'Pick a name', with: name
+ click_on 'Register device'
+ webauthn_device
+ end
+
+ # Adds webauthn device directly via database
+ def add_webauthn_device(app_id, user, fake_device = nil, name: 'My device')
+ fake_device ||= WebAuthn::FakeClient.new(app_id)
+
+ options_for_create = WebAuthn::Credential.options_for_create(
+ user: { id: user.webauthn_xid, name: user.username },
+ authenticator_selection: { user_verification: 'discouraged' },
+ rp: { name: 'GitLab' }
+ )
+ challenge = options_for_create.challenge
+
+ device_response = fake_device.create(challenge: challenge).to_json # rubocop:disable Rails/SaveBang
+ device_registration_params = { device_response: device_response,
+ name: name }
+
+ Webauthn::RegisterService.new(
+ user, device_registration_params, challenge).execute
+ FakeWebauthnDevice.new(page, name, fake_device)
+ end
+
+ def assert_fallback_ui(page)
+ expect(page).to have_button('Verify code')
+ expect(page).to have_css('#user_otp_attempt')
+ expect(page).not_to have_link('Sign in via 2FA code')
+ expect(page).not_to have_css("#js-authenticate-token-2fa")
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/support/helpers/graphql_helpers.rb b/spec/support/helpers/graphql_helpers.rb
index 87525734490..5635ba3df05 100644
--- a/spec/support/helpers/graphql_helpers.rb
+++ b/spec/support/helpers/graphql_helpers.rb
@@ -241,6 +241,39 @@ module GraphqlHelpers
post_graphql(mutation.query, current_user: current_user, variables: mutation.variables)
end
+ def post_graphql_mutation_with_uploads(mutation, current_user: nil)
+ file_paths = file_paths_in_mutation(mutation)
+ params = mutation_to_apollo_uploads_param(mutation, files: file_paths)
+
+ workhorse_post_with_file(api('/', current_user, version: 'graphql'),
+ params: params,
+ file_key: '1'
+ )
+ end
+
+ def file_paths_in_mutation(mutation)
+ paths = []
+ find_uploads(paths, [], mutation.variables)
+
+ paths
+ end
+
+ # Depth first search for UploadedFile values
+ def find_uploads(paths, path, value)
+ case value
+ when Rack::Test::UploadedFile
+ paths << path
+ when Hash
+ value.each do |k, v|
+ find_uploads(paths, path + [k], v)
+ end
+ when Array
+ value.each_with_index do |v, i|
+ find_uploads(paths, path + [i], v)
+ end
+ end
+ end
+
# this implements GraphQL multipart request v2
# https://github.com/jaydenseric/graphql-multipart-request-spec/tree/v2.0.0-alpha.2
# this is simplified and do not support file deduplication
diff --git a/spec/support/helpers/jira_service_helper.rb b/spec/support/helpers/jira_service_helper.rb
index 4895bc3ba15..698490c8c92 100644
--- a/spec/support/helpers/jira_service_helper.rb
+++ b/spec/support/helpers/jira_service_helper.rb
@@ -78,8 +78,7 @@ module JiraServiceHelper
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)
+ WebMock.stub_request(:get, /serverInfo/).to_return(body: { url: 'http://url' }.to_json)
end
def stub_jira_urls(issue_id)
diff --git a/spec/support/helpers/login_helpers.rb b/spec/support/helpers/login_helpers.rb
index 1118cfcf7ac..e21d4497cda 100644
--- a/spec/support/helpers/login_helpers.rb
+++ b/spec/support/helpers/login_helpers.rb
@@ -111,6 +111,11 @@ module LoginHelpers
FakeU2fDevice.new(page, nil).fake_u2f_authentication
end
+ def fake_successful_webauthn_authentication
+ allow_any_instance_of(Webauthn::AuthenticateService).to receive(:execute).and_return(true)
+ FakeWebauthnDevice.new(page, nil).fake_webauthn_authentication
+ end
+
def mock_auth_hash_with_saml_xml(provider, uid, email, saml_response)
response_object = { document: saml_xml(saml_response) }
mock_auth_hash(provider, uid, email, response_object: response_object)
diff --git a/spec/support/helpers/markdown_feature.rb b/spec/support/helpers/markdown_feature.rb
index 40e0d4413e2..0cb2863dc2c 100644
--- a/spec/support/helpers/markdown_feature.rb
+++ b/spec/support/helpers/markdown_feature.rb
@@ -87,6 +87,10 @@ class MarkdownFeature
@group_milestone ||= create(:milestone, name: 'group-milestone', group: group)
end
+ def alert
+ @alert ||= create(:alert_management_alert, project: project)
+ end
+
# Cross-references -----------------------------------------------------------
def xproject
@@ -125,6 +129,10 @@ class MarkdownFeature
@xmilestone ||= create(:milestone, project: xproject)
end
+ def xalert
+ @xalert ||= create(:alert_management_alert, project: xproject)
+ end
+
def urls
Gitlab::Routing.url_helpers
end
diff --git a/spec/support/helpers/metrics_dashboard_helpers.rb b/spec/support/helpers/metrics_dashboard_helpers.rb
index 7168079fead..a384e44f428 100644
--- a/spec/support/helpers/metrics_dashboard_helpers.rb
+++ b/spec/support/helpers/metrics_dashboard_helpers.rb
@@ -47,7 +47,7 @@ module MetricsDashboardHelpers
end
def business_metric_title
- PrometheusMetricEnums.group_details[:business][:group_title]
+ Enums::PrometheusMetric.group_details[:business][:group_title]
end
def self_monitoring_dashboard_path
diff --git a/spec/support/helpers/multipart_helpers.rb b/spec/support/helpers/multipart_helpers.rb
new file mode 100644
index 00000000000..043cb6e1420
--- /dev/null
+++ b/spec/support/helpers/multipart_helpers.rb
@@ -0,0 +1,91 @@
+# frozen_string_literal: true
+
+module MultipartHelpers
+ include WorkhorseHelpers
+
+ def post_env(rewritten_fields:, params:, secret:, issuer:)
+ token = JWT.encode({ 'iss' => issuer, 'rewritten_fields' => rewritten_fields }, secret, 'HS256')
+ Rack::MockRequest.env_for(
+ '/',
+ method: 'post',
+ params: params,
+ described_class::RACK_ENV_KEY => token
+ )
+ end
+
+ # This function assumes a `mode` variable to be set
+ def upload_parameters_for(filepath: nil, key: nil, filename: 'filename', remote_id: 'remote_id')
+ result = {
+ "#{key}.name" => filename,
+ "#{key}.type" => "application/octet-stream",
+ "#{key}.sha256" => "1234567890"
+ }
+
+ case mode
+ when :local
+ result["#{key}.path"] = filepath
+ when :remote
+ result["#{key}.remote_id"] = remote_id
+ result["#{key}.size"] = 3.megabytes
+ else
+ raise ArgumentError, "can't handle #{mode} mode"
+ end
+
+ return result if ::Feature.disabled?(:upload_middleware_jwt_params_handler)
+
+ # the HandlerForJWTParams expects a jwt token with the upload parameters
+ # *without* the "#{key}." prefix
+ result.deep_transform_keys! { |k| k.remove("#{key}.") }
+ {
+ "#{key}.gitlab-workhorse-upload" => jwt_token('upload' => result)
+ }
+ end
+
+ # This function assumes a `mode` variable to be set
+ def rewritten_fields_hash(hash)
+ if mode == :remote
+ # For remote uploads, workhorse still submits rewritten_fields,
+ # but all the values are empty strings.
+ hash.keys.each { |k| hash[k] = '' }
+ end
+
+ hash
+ end
+
+ def expect_uploaded_files(uploaded_file_expectations)
+ expect(app).to receive(:call) do |env|
+ Array.wrap(uploaded_file_expectations).each do |expectation|
+ file = get_params(env).dig(*expectation[:params_path])
+ expect_uploaded_file(file, expectation)
+ end
+ end
+ end
+
+ # This function assumes a `mode` variable to be set
+ def expect_uploaded_file(file, expectation)
+ expect(file).to be_a(::UploadedFile)
+ expect(file.original_filename).to eq(expectation[:original_filename])
+ expect(file.sha256).to eq('1234567890')
+
+ case mode
+ when :local
+ expect(file.path).to eq(File.realpath(expectation[:filepath]))
+ expect(file.remote_id).to be_nil
+ expect(file.size).to eq(expectation[:size])
+ when :remote
+ expect(file.remote_id).to eq(expectation[:remote_id])
+ expect(file.path).to be_nil
+ expect(file.size).to eq(3.megabytes)
+ else
+ raise ArgumentError, "can't handle #{mode} mode"
+ end
+ end
+
+ # Rails doesn't combine the GET/POST parameters in
+ # ActionDispatch::HTTP::Parameters if action_dispatch.request.parameters is set:
+ # https://github.com/rails/rails/blob/aea6423f013ca48f7704c70deadf2cd6ac7d70a1/actionpack/lib/action_dispatch/http/parameters.rb#L41
+ def get_params(env)
+ req = ActionDispatch::Request.new(env)
+ req.GET.merge(req.POST)
+ end
+end
diff --git a/spec/support/helpers/navbar_structure_helper.rb b/spec/support/helpers/navbar_structure_helper.rb
index c7aa2ffe536..11e67ba394c 100644
--- a/spec/support/helpers/navbar_structure_helper.rb
+++ b/spec/support/helpers/navbar_structure_helper.rb
@@ -4,13 +4,13 @@ module NavbarStructureHelper
def insert_after_nav_item(before_nav_item_name, new_nav_item:)
expect(structure).to include(a_hash_including(nav_item: before_nav_item_name))
- index = structure.find_index { |h| h[:nav_item] == before_nav_item_name }
+ index = structure.find_index { |h| h[:nav_item] == before_nav_item_name if h }
structure.insert(index + 1, new_nav_item)
end
def insert_after_sub_nav_item(before_sub_nav_item_name, within:, new_sub_nav_item_name:)
expect(structure).to include(a_hash_including(nav_item: within))
- hash = structure.find { |h| h[:nav_item] == within }
+ hash = structure.find { |h| h[:nav_item] == within if h }
expect(hash).to have_key(:nav_sub_items)
expect(hash[:nav_sub_items]).to include(before_sub_nav_item_name)
diff --git a/spec/support/helpers/next_found_instance_of.rb b/spec/support/helpers/next_found_instance_of.rb
new file mode 100644
index 00000000000..ff34fcdd1d3
--- /dev/null
+++ b/spec/support/helpers/next_found_instance_of.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+module NextFoundInstanceOf
+ ERROR_MESSAGE = 'NextFoundInstanceOf mock helpers can only be used with ActiveRecord targets'
+
+ def expect_next_found_instance_of(klass)
+ check_if_active_record!(klass)
+
+ stub_allocate(expect(klass)) do |expectation|
+ yield(expectation)
+ end
+ end
+
+ def allow_next_found_instance_of(klass)
+ check_if_active_record!(klass)
+
+ stub_allocate(allow(klass)) do |allowance|
+ yield(allowance)
+ end
+ end
+
+ private
+
+ def check_if_active_record!(klass)
+ raise ArgumentError.new(ERROR_MESSAGE) unless klass < ActiveRecord::Base
+ end
+
+ def stub_allocate(target)
+ target.to receive(:allocate).and_wrap_original do |method|
+ method.call.tap { |allocation| yield(allocation) }
+ end
+ end
+end
diff --git a/spec/support/helpers/project_forks_helper.rb b/spec/support/helpers/project_forks_helper.rb
index a32e39e52c8..4b4285f251e 100644
--- a/spec/support/helpers/project_forks_helper.rb
+++ b/spec/support/helpers/project_forks_helper.rb
@@ -17,14 +17,26 @@ module ProjectForksHelper
project.add_developer(user)
end
- unless params[:namespace] || params[:namespace_id]
+ unless params[:namespace]
params[:namespace] = create(:group)
params[:namespace].add_owner(user)
end
+ namespace = params[:namespace]
+ create_repository = params.delete(:repository)
+
+ unless params[:target_project] || params[:using_service]
+ target_level = [project.visibility_level, namespace.visibility_level].min
+ visibility_level = Gitlab::VisibilityLevel.closest_allowed_level(target_level)
+
+ params[:target_project] =
+ create(:project,
+ (:repository if create_repository),
+ visibility_level: visibility_level, creator: user, namespace: namespace)
+ end
+
service = Projects::ForkService.new(project, user, params)
- create_repository = params.delete(:repository)
# Avoid creating a repository
unless create_repository
allow(RepositoryForkWorker).to receive(:perform_async).and_return(true)
diff --git a/spec/support/helpers/repo_helpers.rb b/spec/support/helpers/repo_helpers.rb
index 7741c805b37..bbba58d60d6 100644
--- a/spec/support/helpers/repo_helpers.rb
+++ b/spec/support/helpers/repo_helpers.rb
@@ -125,7 +125,7 @@ eos
end
def create_file_in_repo(
- project, start_branch, branch_name, filename, content,
+ project, start_branch, branch_name, filename, content,
commit_message: 'Add new content')
Files::CreateService.new(
project,
diff --git a/spec/support/helpers/snowplow_helpers.rb b/spec/support/helpers/snowplow_helpers.rb
new file mode 100644
index 00000000000..83a5b7e48bc
--- /dev/null
+++ b/spec/support/helpers/snowplow_helpers.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+module SnowplowHelpers
+ # Asserts call for one snowplow event from `Gitlab::Tracking#event`.
+ #
+ # @param [Hash]
+ #
+ # Examples:
+ #
+ # describe '#show', :snowplow do
+ # it 'tracks snowplow events' do
+ # get :show
+ #
+ # expect_snowplow_event(category: 'Experiment', action: 'start')
+ # end
+ # end
+ #
+ # describe '#create', :snowplow do
+ # it 'tracks snowplow events' do
+ # post :create
+ #
+ # expect_snowplow_event(
+ # category: 'Experiment',
+ # action: 'created',
+ # )
+ # expect_snowplow_event(
+ # category: 'Experiment',
+ # action: 'accepted',
+ # property: 'property',
+ # label: 'label'
+ # )
+ # end
+ # end
+ def expect_snowplow_event(category:, action:, **kwargs)
+ expect(Gitlab::Tracking).to have_received(:event)
+ .with(category, action, **kwargs).at_least(:once)
+ end
+
+ # Asserts that no call to `Gitlab::Tracking#event` was made.
+ #
+ # Example:
+ #
+ # describe '#show', :snowplow do
+ # it 'does not track any snowplow events' do
+ # get :show
+ #
+ # expect_no_snowplow_event
+ # end
+ # end
+ def expect_no_snowplow_event
+ expect(Gitlab::Tracking).not_to have_received(:event)
+ end
+end
diff --git a/spec/support/helpers/stub_object_storage.rb b/spec/support/helpers/stub_object_storage.rb
index 8a52a614821..476b7d34ee5 100644
--- a/spec/support/helpers/stub_object_storage.rb
+++ b/spec/support/helpers/stub_object_storage.rb
@@ -14,7 +14,7 @@ module StubObjectStorage
end
def stub_object_storage_uploader(
- config:,
+ config:,
uploader:,
remote_directory:,
enabled: true,
diff --git a/spec/support/helpers/stubbed_feature.rb b/spec/support/helpers/stubbed_feature.rb
index e78efcf6b75..d4e9af7a031 100644
--- a/spec/support/helpers/stubbed_feature.rb
+++ b/spec/support/helpers/stubbed_feature.rb
@@ -37,10 +37,7 @@ module StubbedFeature
# We do `m.call` as we want to validate the execution of method arguments
# and a feature flag state if it is not persisted
unless Feature.persisted_name?(args.first)
- # TODO: this is hack to support `promo_feature_available?`
- # We enable all feature flags by default unless they are `promo_`
- # Issue: https://gitlab.com/gitlab-org/gitlab/-/issues/218667
- feature_flag = true unless args.first.to_s.start_with?('promo_')
+ feature_flag = true
end
feature_flag
diff --git a/spec/support/helpers/test_env.rb b/spec/support/helpers/test_env.rb
index 7dae960410d..641ed24207e 100644
--- a/spec/support/helpers/test_env.rb
+++ b/spec/support/helpers/test_env.rb
@@ -247,8 +247,9 @@ module TestEnv
'GitLab Workhorse',
install_dir: workhorse_dir,
version: Gitlab::Workhorse.version,
- task: "gitlab:workhorse:install[#{install_workhorse_args}]"
- )
+ task: "gitlab:workhorse:install[#{install_workhorse_args}]") do
+ Gitlab::SetupHelper::Workhorse.create_configuration(workhorse_dir, nil)
+ end
end
def workhorse_dir
@@ -259,16 +260,22 @@ module TestEnv
host = "[#{host}]" if host.include?(':')
listen_addr = [host, port].join(':')
+ config_path = Gitlab::SetupHelper::Workhorse.get_config_path(workhorse_dir)
+
+ # This should be set up in setup_workhorse, but since
+ # component_needs_update? only checks that versions are consistent,
+ # we need to ensure the config file exists. This line can be removed
+ # later after a new Workhorse version is updated.
+ Gitlab::SetupHelper::Workhorse.create_configuration(workhorse_dir, nil) unless File.exist?(config_path)
+
workhorse_pid = spawn(
+ { 'PATH' => "#{ENV['PATH']}:#{workhorse_dir}" },
File.join(workhorse_dir, 'gitlab-workhorse'),
'-authSocket', upstream,
'-documentRoot', Rails.root.join('public').to_s,
'-listenAddr', listen_addr,
'-secretPath', Gitlab::Workhorse.secret_path.to_s,
- # TODO: Needed for workhorse + redis features.
- # https://gitlab.com/gitlab-org/gitlab/-/issues/209245
- #
- # '-config', '',
+ '-config', config_path,
'-logFile', 'log/workhorse-test.log',
'-logFormat', 'structured',
'-developmentMode' # to serve assets and rich error messages
diff --git a/spec/support/helpers/usage_data_helpers.rb b/spec/support/helpers/usage_data_helpers.rb
index fab775dd404..d92fcdc2d4a 100644
--- a/spec/support/helpers/usage_data_helpers.rb
+++ b/spec/support/helpers/usage_data_helpers.rb
@@ -88,6 +88,8 @@ module UsageDataHelpers
projects_jira_active
projects_jira_server_active
projects_jira_cloud_active
+ projects_jira_dvcs_cloud_active
+ projects_jira_dvcs_server_active
projects_slack_active
projects_slack_slash_commands_active
projects_custom_issue_tracker_active
@@ -136,6 +138,7 @@ module UsageDataHelpers
USAGE_DATA_KEYS = %i(
active_user_count
counts
+ counts_monthly
recorded_at
edition
version
@@ -160,6 +163,7 @@ module UsageDataHelpers
web_ide_clientside_preview_enabled
ingress_modsecurity_enabled
object_store
+ topology
).freeze
def stub_usage_data_connections
@@ -220,17 +224,8 @@ module UsageDataHelpers
)
end
- def expect_prometheus_api_to(*receive_matchers)
- expect_next_instance_of(Gitlab::PrometheusClient) do |client|
- receive_matchers.each { |m| expect(client).to m }
- end
- end
-
- def allow_prometheus_queries
- allow_next_instance_of(Gitlab::PrometheusClient) do |client|
- allow(client).to receive(:aggregate).and_return({})
- allow(client).to receive(:query).and_return({})
- end
+ def expect_prometheus_client_to(*receive_matchers)
+ receive_matchers.each { |m| expect(prometheus_client).to m }
end
def for_defined_days_back(days: [29, 2])
diff --git a/spec/support/helpers/wiki_helpers.rb b/spec/support/helpers/wiki_helpers.rb
index ae0d53d1297..e59c6bde264 100644
--- a/spec/support/helpers/wiki_helpers.rb
+++ b/spec/support/helpers/wiki_helpers.rb
@@ -3,6 +3,11 @@
module WikiHelpers
extend self
+ def stub_group_wikis(enabled)
+ stub_feature_flags(group_wikis: enabled)
+ stub_licensed_features(group_wikis: enabled)
+ end
+
def wait_for_svg_to_be_loaded(example = nil)
# Ensure the SVG is loaded first before clicking the button
find('.svg-content .js-lazy-loaded') if example.nil? || example.metadata.key?(:js)
diff --git a/spec/support/helpers/workhorse_helpers.rb b/spec/support/helpers/workhorse_helpers.rb
index f16b6c1e910..7e95f49aea2 100644
--- a/spec/support/helpers/workhorse_helpers.rb
+++ b/spec/support/helpers/workhorse_helpers.rb
@@ -3,6 +3,8 @@
module WorkhorseHelpers
extend self
+ UPLOAD_PARAM_NAMES = %w[name size path remote_id sha256 type].freeze
+
def workhorse_send_data
@_workhorse_send_data ||= begin
header = response.headers[Gitlab::Workhorse::SEND_DATA_HEADER]
@@ -59,6 +61,7 @@ module WorkhorseHelpers
file = workhorse_params.delete(key)
rewritten_fields[key] = file.path if file
workhorse_params = workhorse_disk_accelerated_file_params(key, file).merge(workhorse_params)
+ workhorse_params = workhorse_params.merge(jwt_file_upload_param(key: key, params: workhorse_params))
end
headers = if send_rewritten_field
@@ -74,8 +77,19 @@ module WorkhorseHelpers
private
- def jwt_token(data = {})
- JWT.encode({ 'iss' => 'gitlab-workhorse' }.merge(data), Gitlab::Workhorse.secret, 'HS256')
+ def jwt_file_upload_param(key:, params:)
+ upload_params = UPLOAD_PARAM_NAMES.map do |file_upload_param|
+ [file_upload_param, params["#{key}.#{file_upload_param}"]]
+ end
+ upload_params = upload_params.to_h.compact
+
+ return {} if upload_params.empty?
+
+ { "#{key}.gitlab-workhorse-upload" => jwt_token('upload' => upload_params) }
+ end
+
+ def jwt_token(data = {}, issuer: 'gitlab-workhorse', secret: Gitlab::Workhorse.secret, algorithm: 'HS256')
+ JWT.encode({ 'iss' => issuer }.merge(data), secret, algorithm)
end
def workhorse_rewritten_fields_header(fields)
diff --git a/spec/support/import_export/configuration_helper.rb b/spec/support/import_export/configuration_helper.rb
index 6f67b0f3dd7..b731ee626c0 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 - Gitlab::Json.parse(excluded_attributes.to_json) if excluded_attributes
- attributes = attributes & Gitlab::Json.parse(included_attributes.to_json) if included_attributes
+ attributes -= Gitlab::Json.parse(excluded_attributes.to_json) if excluded_attributes
+ attributes &= Gitlab::Json.parse(included_attributes.to_json) if included_attributes
attributes
end
diff --git a/spec/support/matchers/markdown_matchers.rb b/spec/support/matchers/markdown_matchers.rb
index 103019d8dd8..6e75fa58700 100644
--- a/spec/support/matchers/markdown_matchers.rb
+++ b/spec/support/matchers/markdown_matchers.rb
@@ -174,6 +174,15 @@ module MarkdownMatchers
end
end
+ # AlertReferenceFilter
+ matcher :reference_alerts do
+ set_default_markdown_messages
+
+ match do |actual|
+ expect(actual).to have_selector('a.gfm.gfm-alert', count: 5)
+ end
+ end
+
# TaskListFilter
matcher :parse_task_lists do
set_default_markdown_messages
diff --git a/spec/support/shared_contexts/features/file_uploads_shared_context.rb b/spec/support/shared_contexts/features/file_uploads_shared_context.rb
new file mode 100644
index 00000000000..972d25e81d2
--- /dev/null
+++ b/spec/support/shared_contexts/features/file_uploads_shared_context.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+RSpec.shared_context 'file upload requests helpers' do
+ def capybara_url(path)
+ "http://#{Capybara.current_session.server.host}:#{Capybara.current_session.server.port}#{path}"
+ end
+end
diff --git a/spec/support/shared_contexts/finders/users_finder_shared_contexts.rb b/spec/support/shared_contexts/finders/users_finder_shared_contexts.rb
index fc8f9d2f407..54022aeb494 100644
--- a/spec/support/shared_contexts/finders/users_finder_shared_contexts.rb
+++ b/spec/support/shared_contexts/finders/users_finder_shared_contexts.rb
@@ -5,4 +5,5 @@ RSpec.shared_context 'UsersFinder#execute filter by project context' do
let_it_be(:blocked_user) { create(:user, :blocked, username: 'notsorandom') }
let_it_be(:external_user) { create(:user, :external) }
let_it_be(:omniauth_user) { create(:omniauth_user, provider: 'twitter', extern_uid: '123456') }
+ let_it_be(:internal_user) { User.alert_bot }
end
diff --git a/spec/support/shared_contexts/lib/gitlab/middleware/multipart_shared_contexts.rb b/spec/support/shared_contexts/lib/gitlab/middleware/multipart_shared_contexts.rb
index f1554ea8e9f..ec5bea34e8b 100644
--- a/spec/support/shared_contexts/lib/gitlab/middleware/multipart_shared_contexts.rb
+++ b/spec/support/shared_contexts/lib/gitlab/middleware/multipart_shared_contexts.rb
@@ -1,42 +1,88 @@
# frozen_string_literal: true
-RSpec.shared_context 'multipart middleware context' do
- let(:app) { double(:app) }
- let(:middleware) { described_class.new(app) }
- let(:original_filename) { 'filename' }
-
- # Rails 5 doesn't combine the GET/POST parameters in
- # ActionDispatch::HTTP::Parameters if action_dispatch.request.parameters is set:
- # https://github.com/rails/rails/blob/aea6423f013ca48f7704c70deadf2cd6ac7d70a1/actionpack/lib/action_dispatch/http/parameters.rb#L41
- def get_params(env)
- req = ActionDispatch::Request.new(env)
- req.GET.merge(req.POST)
+# This context provides one temporary file for the multipart spec
+#
+# Here are the available variables:
+# - uploaded_file
+# - uploaded_filepath
+# - filename
+# - remote_id
+RSpec.shared_context 'with one temporary file for multipart' do |within_tmp_sub_dir: false|
+ let(:uploaded_filepath) { uploaded_file.path }
+
+ around do |example|
+ Tempfile.open('uploaded_file2') do |tempfile|
+ @uploaded_file = tempfile
+ @filename = 'test_file.png'
+ @remote_id = 'remote_id'
+
+ example.run
+ end
end
- def post_env(rewritten_fields, params, secret, issuer)
- token = JWT.encode({ 'iss' => issuer, 'rewritten_fields' => rewritten_fields }, secret, 'HS256')
- Rack::MockRequest.env_for(
- '/',
- method: 'post',
- params: params,
- described_class::RACK_ENV_KEY => token
- )
+ attr_reader :uploaded_file, :filename, :remote_id
+end
+
+# This context provides two temporary files for the multipart spec
+#
+# Here are the available variables:
+# - uploaded_file
+# - uploaded_filepath
+# - filename
+# - remote_id
+# - tmp_sub_dir (only when using within_tmp_sub_dir: true)
+# - uploaded_file2
+# - uploaded_filepath2
+# - filename2
+# - remote_id2
+RSpec.shared_context 'with two temporary files for multipart' do
+ include_context 'with one temporary file for multipart'
+
+ let(:uploaded_filepath2) { uploaded_file2.path }
+
+ around do |example|
+ Tempfile.open('uploaded_file2') do |tempfile|
+ @uploaded_file2 = tempfile
+ @filename2 = 'test_file2.png'
+ @remote_id2 = 'remote_id2'
+
+ example.run
+ end
end
- def with_tmp_dir(uploads_sub_dir, storage_path = '')
- Dir.mktmpdir do |dir|
- upload_dir = File.join(dir, storage_path, uploads_sub_dir)
- FileUtils.mkdir_p(upload_dir)
+ attr_reader :uploaded_file2, :filename2, :remote_id2
+end
+
+# This context provides three temporary files for the multipart spec
+#
+# Here are the available variables:
+# - uploaded_file
+# - uploaded_filepath
+# - filename
+# - remote_id
+# - tmp_sub_dir (only when using within_tmp_sub_dir: true)
+# - uploaded_file2
+# - uploaded_filepath2
+# - filename2
+# - remote_id2
+# - uploaded_file3
+# - uploaded_filepath3
+# - filename3
+# - remote_id3
+RSpec.shared_context 'with three temporary files for multipart' do
+ include_context 'with two temporary files for multipart'
- allow(Rails).to receive(:root).and_return(dir)
- allow(Dir).to receive(:tmpdir).and_return(File.join(Dir.tmpdir, 'tmpsubdir'))
- allow(GitlabUploader).to receive(:root).and_return(File.join(dir, storage_path))
+ let(:uploaded_filepath3) { uploaded_file3.path }
- Tempfile.open('top-level', upload_dir) do |tempfile|
- env = post_env({ 'file' => tempfile.path }, { 'file.name' => original_filename, 'file.path' => tempfile.path }, Gitlab::Workhorse.secret, 'gitlab-workhorse')
+ around do |example|
+ Tempfile.open('uploaded_file3') do |tempfile|
+ @uploaded_file3 = tempfile
+ @filename3 = 'test_file3.png'
+ @remote_id3 = 'remote_id3'
- yield dir, env
- end
+ example.run
end
end
+
+ attr_reader :uploaded_file3, :filename3, :remote_id3
end
diff --git a/spec/support/shared_contexts/navbar_structure_context.rb b/spec/support/shared_contexts/navbar_structure_context.rb
index e276a54224b..747358fc1e0 100644
--- a/spec/support/shared_contexts/navbar_structure_context.rb
+++ b/spec/support/shared_contexts/navbar_structure_context.rb
@@ -119,10 +119,10 @@ RSpec.shared_context 'group navbar structure' do
nav_item: _('Settings'),
nav_sub_items: [
_('General'),
+ _('Integrations'),
_('Projects'),
_('Repository'),
_('CI / CD'),
- _('Integrations'),
_('Webhooks'),
_('Audit Events')
]
@@ -138,6 +138,13 @@ RSpec.shared_context 'group navbar structure' do
}
end
+ let(:push_rules_nav_item) do
+ {
+ nav_item: _('Push Rules'),
+ nav_sub_items: []
+ }
+ end
+
let(:structure) do
[
{
@@ -160,6 +167,7 @@ RSpec.shared_context 'group navbar structure' do
nav_item: _('Merge Requests'),
nav_sub_items: []
},
+ (push_rules_nav_item if Gitlab.ee?),
{
nav_item: _('Kubernetes'),
nav_sub_items: []
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 5339fa003b9..3016494ac8d 100644
--- a/spec/support/shared_contexts/policies/project_policy_shared_context.rb
+++ b/spec/support/shared_contexts/policies/project_policy_shared_context.rb
@@ -1,30 +1,36 @@
# frozen_string_literal: true
RSpec.shared_context 'ProjectPolicy context' do
+ let_it_be(:anonymous) { nil }
let_it_be(:guest) { create(:user) }
let_it_be(:reporter) { create(:user) }
let_it_be(:developer) { create(:user) }
let_it_be(:maintainer) { create(:user) }
let_it_be(:owner) { create(:user) }
let_it_be(:admin) { create(:admin) }
- let(:project) { create(:project, :public, namespace: owner.namespace) }
+ let_it_be(:non_member) { create(:user) }
+ let_it_be_with_refind(:private_project) { create(:project, :private, namespace: owner.namespace) }
+ let_it_be_with_refind(:internal_project) { create(:project, :internal, namespace: owner.namespace) }
+ let_it_be_with_refind(:public_project) { create(:project, :public, namespace: owner.namespace) }
let(:base_guest_permissions) do
%i[
- read_project read_board read_list read_wiki read_issue
- read_project_for_iids read_issue_iid read_label
- read_milestone read_snippet read_project_member read_note
- create_project create_issue create_note upload_file create_merge_request_in
- award_emoji
+ award_emoji create_issue create_merge_request_in create_note
+ create_project read_board read_issue read_issue_iid read_issue_link
+ read_label read_list read_milestone read_note read_project
+ read_project_for_iids read_project_member read_release read_snippet
+ read_wiki upload_file
]
end
let(:base_reporter_permissions) do
%i[
- download_code fork_project create_snippet update_issue
- admin_issue admin_label admin_list read_commit_status read_build
- read_container_image read_pipeline read_environment read_deployment
- read_merge_request download_wiki_code read_sentry_issue read_prometheus
+ admin_issue admin_issue_link admin_label admin_list create_snippet
+ download_code download_wiki_code fork_project metrics_dashboard
+ read_build read_commit_status read_confidential_issues
+ read_container_image read_deployment read_environment read_merge_request
+ read_metrics_dashboard_annotation read_pipeline read_prometheus
+ read_sentry_issue update_issue
]
end
@@ -34,37 +40,42 @@ RSpec.shared_context 'ProjectPolicy context' do
let(:developer_permissions) do
%i[
- admin_milestone admin_merge_request update_merge_request create_commit_status
- update_commit_status create_build update_build create_pipeline
- 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 daily_statistics
+ admin_merge_request admin_milestone admin_tag create_build
+ create_commit_status create_container_image create_deployment
+ create_environment create_merge_request_from
+ create_metrics_dashboard_annotation create_pipeline create_release
+ create_wiki daily_statistics delete_metrics_dashboard_annotation
+ destroy_container_image push_code read_pod_logs read_terraform_state
+ resolve_note update_build update_commit_status update_container_image
+ update_deployment update_environment update_merge_request
+ update_metrics_dashboard_annotation update_pipeline update_release
]
end
let(:base_maintainer_permissions) do
%i[
- push_to_delete_protected_branch update_snippet
- 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
+ add_cluster admin_build admin_commit_status admin_container_image
+ admin_deployment admin_environment admin_note admin_pipeline
+ admin_project admin_project_member admin_snippet admin_terraform_state
+ admin_wiki create_deploy_token destroy_deploy_token destroy_release
+ push_to_delete_protected_branch read_deploy_token update_snippet
]
end
let(:public_permissions) do
%i[
- download_code fork_project read_commit_status read_pipeline
- read_container_image build_download_code build_read_container_image
- download_wiki_code read_release
+ build_download_code build_read_container_image download_code
+ download_wiki_code fork_project read_commit_status read_container_image
+ read_pipeline read_release
]
end
let(:base_owner_permissions) do
%i[
- change_namespace change_visibility_level rename_project remove_project
- archive_project remove_fork_project destroy_merge_request destroy_issue
- set_issue_iid set_issue_created_at set_issue_updated_at set_note_created_at
+ archive_project change_namespace change_visibility_level destroy_issue
+ destroy_merge_request remove_fork_project remove_project rename_project
+ set_issue_created_at set_issue_iid set_issue_updated_at
+ set_note_created_at
]
end
@@ -79,10 +90,12 @@ RSpec.shared_context 'ProjectPolicy context' do
let(:maintainer_permissions) { base_maintainer_permissions + additional_maintainer_permissions }
let(:owner_permissions) { base_owner_permissions + additional_owner_permissions }
- before do
- project.add_guest(guest)
- project.add_maintainer(maintainer)
- project.add_developer(developer)
- project.add_reporter(reporter)
+ before_all do
+ [private_project, internal_project, public_project].each do |project|
+ project.add_guest(guest)
+ project.add_reporter(reporter)
+ project.add_developer(developer)
+ project.add_maintainer(maintainer)
+ end
end
end
diff --git a/spec/support/shared_contexts/project_service_jira_context.rb b/spec/support/shared_contexts/project_service_jira_context.rb
index 4ca5c99323a..8e01de70846 100644
--- a/spec/support/shared_contexts/project_service_jira_context.rb
+++ b/spec/support/shared_contexts/project_service_jira_context.rb
@@ -5,7 +5,7 @@ RSpec.shared_context 'project service Jira context' do
let(:test_url) { 'http://jira.example.com/rest/api/2/serverInfo' }
def fill_form(disable: false)
- click_active_toggle if disable
+ click_active_checkbox if disable
fill_in 'service_url', with: url
fill_in 'service_username', with: 'username'
diff --git a/spec/support/shared_contexts/project_service_shared_context.rb b/spec/support/shared_contexts/project_service_shared_context.rb
index a72d4901b72..b4b9ab456e0 100644
--- a/spec/support/shared_contexts/project_service_shared_context.rb
+++ b/spec/support/shared_contexts/project_service_shared_context.rb
@@ -18,19 +18,27 @@ RSpec.shared_context 'project service activation' do
click_link(name)
end
- def click_active_toggle
- find('input[name="service[active]"] + button').click
+ def click_active_checkbox
+ find('input[name="service[active]"]').click
+ end
+
+ def click_save_integration
+ click_button('Save changes')
end
def click_test_integration
- click_button('Test settings and save changes')
+ click_link('Test settings')
end
- def click_test_then_save_integration
+ def click_test_then_save_integration(expect_test_to_fail: true)
click_test_integration
- expect(page).to have_content('Test failed.')
+ if expect_test_to_fail
+ expect(page).to have_content('Connection failed.')
+ else
+ expect(page).to have_content('Connection successful.')
+ end
- click_link('Save anyway')
+ click_save_integration
end
end
diff --git a/spec/support/shared_contexts/requests/api/conan_packages_shared_context.rb b/spec/support/shared_contexts/requests/api/conan_packages_shared_context.rb
new file mode 100644
index 00000000000..580ebf00dcb
--- /dev/null
+++ b/spec/support/shared_contexts/requests/api/conan_packages_shared_context.rb
@@ -0,0 +1,75 @@
+# frozen_string_literal: true
+
+RSpec.shared_context 'conan api setup' do
+ include PackagesManagerApiSpecHelpers
+ include HttpBasicAuthHelpers
+
+ let(:package) { create(:conan_package) }
+ let_it_be(:personal_access_token) { create(:personal_access_token) }
+ let_it_be(:user) { personal_access_token.user }
+ let_it_be(:base_secret) { SecureRandom.base64(64) }
+ let_it_be(:job) { create(:ci_build, :running, user: user) }
+ let_it_be(:job_token) { job.token }
+ let_it_be(:deploy_token) { create(:deploy_token, read_package_registry: true, write_package_registry: true) }
+
+ let(:project) { package.project }
+ let(:auth_token) { personal_access_token.token }
+ let(:project_deploy_token) { create(:project_deploy_token, deploy_token: deploy_token, project: project) }
+
+ let(:headers) do
+ { 'HTTP_AUTHORIZATION' => ActionController::HttpAuthentication::Basic.encode_credentials('foo', auth_token) }
+ end
+
+ let(:jwt_secret) do
+ OpenSSL::HMAC.hexdigest(
+ OpenSSL::Digest::SHA256.new,
+ base_secret,
+ Gitlab::ConanToken::HMAC_KEY
+ )
+ end
+
+ before do
+ project.add_developer(user)
+ allow(Settings).to receive(:attr_encrypted_db_key_base).and_return(base_secret)
+ end
+end
+
+RSpec.shared_context 'conan recipe endpoints' do
+ include PackagesManagerApiSpecHelpers
+ include HttpBasicAuthHelpers
+
+ let(:jwt) { build_jwt(personal_access_token) }
+ let(:headers) { build_token_auth_header(jwt.encoded) }
+ let(:conan_package_reference) { '123456789' }
+ let(:presenter) { double('::Packages::Conan::PackagePresenter') }
+
+ before do
+ allow(::Packages::Conan::PackagePresenter).to receive(:new)
+ .with(package, user, package.project, any_args)
+ .and_return(presenter)
+ end
+end
+
+RSpec.shared_context 'conan file download endpoints' do
+ include PackagesManagerApiSpecHelpers
+ include HttpBasicAuthHelpers
+
+ let(:jwt) { build_jwt(personal_access_token) }
+ let(:headers) { build_token_auth_header(jwt.encoded) }
+ let(:recipe_path) { package.conan_recipe_path }
+ let(:package_file) { package.package_files.find_by(file_name: 'conaninfo.txt') }
+ let(:recipe_file) { package.package_files.find_by(file_name: 'conanfile.py') }
+ let(:metadata) { package_file.conan_file_metadatum }
+end
+
+RSpec.shared_context 'conan file upload endpoints' do
+ include PackagesManagerApiSpecHelpers
+ include WorkhorseHelpers
+ include HttpBasicAuthHelpers
+
+ let(:jwt) { build_jwt(personal_access_token) }
+ let(:workhorse_token) { JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256') }
+ let(:workhorse_header) { { 'GitLab-Workhorse' => '1.0', Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER => workhorse_token } }
+ let(:headers_with_token) { build_token_auth_header(jwt.encoded).merge(workhorse_header) }
+ let(:recipe_path) { "foo/bar/#{project.full_path.tr('/', '+')}/baz"}
+end
diff --git a/spec/support/shared_contexts/serializers/group_group_link_shared_context.rb b/spec/support/shared_contexts/serializers/group_group_link_shared_context.rb
new file mode 100644
index 00000000000..fce78957eba
--- /dev/null
+++ b/spec/support/shared_contexts/serializers/group_group_link_shared_context.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+RSpec.shared_context 'group_group_link' do
+ let(:shared_with_group) { create(:group) }
+ let(:shared_group) { create(:group) }
+
+ let!(:group_group_link) do
+ create(
+ :group_group_link,
+ {
+ shared_group: shared_group,
+ shared_with_group: shared_with_group,
+ expires_at: '2020-05-12'
+ }
+ )
+ end
+end
diff --git a/spec/support/shared_contexts/services_shared_context.rb b/spec/support/shared_contexts/services_shared_context.rb
index 899b43ade01..2bd516a2339 100644
--- a/spec/support/shared_contexts/services_shared_context.rb
+++ b/spec/support/shared_contexts/services_shared_context.rb
@@ -2,6 +2,8 @@
Service.available_services_names.each do |service|
RSpec.shared_context service do
+ include JiraServiceHelper if service == 'jira'
+
let(:dashed_service) { service.dasherize }
let(:service_method) { "#{service}_service".to_sym }
let(:service_klass) { "#{service}_service".classify.constantize }
@@ -39,6 +41,7 @@ Service.available_services_names.each do |service|
before do
enable_license_for_service(service)
+ stub_jira_service_test if service == 'jira'
end
def initialize_service(service)
diff --git a/spec/support/shared_examples/ci/jobs_shared_examples.rb b/spec/support/shared_examples/ci/jobs_shared_examples.rb
new file mode 100644
index 00000000000..d952d4a98eb
--- /dev/null
+++ b/spec/support/shared_examples/ci/jobs_shared_examples.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'a job with artifacts and trace' do |result_is_array: true|
+ context 'with artifacts and trace' do
+ let!(:second_job) { create(:ci_build, :trace_artifact, :artifacts, :test_reports, pipeline: pipeline) }
+
+ it 'returns artifacts and trace data', :skip_before_request do
+ get api(api_endpoint, api_user)
+ json_job = json_response.is_a?(Array) ? json_response.find { |job| job['id'] == second_job.id } : json_response
+
+ expect(json_job['artifacts_file']).not_to be_nil
+ expect(json_job['artifacts_file']).not_to be_empty
+ expect(json_job['artifacts_file']['filename']).to eq(second_job.artifacts_file.filename)
+ expect(json_job['artifacts_file']['size']).to eq(second_job.artifacts_file.size)
+ expect(json_job['artifacts']).not_to be_nil
+ expect(json_job['artifacts']).to be_an Array
+ expect(json_job['artifacts'].size).to eq(second_job.job_artifacts.length)
+ json_job['artifacts'].each do |artifact|
+ expect(artifact).not_to be_nil
+ file_type = Ci::JobArtifact.file_types[artifact['file_type']]
+ expect(artifact['size']).to eq(second_job.job_artifacts.find_by(file_type: file_type).size)
+ expect(artifact['filename']).to eq(second_job.job_artifacts.find_by(file_type: file_type).filename)
+ expect(artifact['file_format']).to eq(second_job.job_artifacts.find_by(file_type: file_type).file_format)
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/controllers/binary_blob_shared_examples.rb b/spec/support/shared_examples/controllers/binary_blob_shared_examples.rb
deleted file mode 100644
index acce7642cfe..00000000000
--- a/spec/support/shared_examples/controllers/binary_blob_shared_examples.rb
+++ /dev/null
@@ -1,86 +0,0 @@
-# frozen_string_literal: true
-
-RSpec.shared_examples 'editing snippet checks blob is binary' do
- let(:snippets_binary_blob_value) { true }
-
- before do
- sign_in(user)
-
- allow_next_instance_of(Blob) do |blob|
- allow(blob).to receive(:binary?).and_return(binary)
- end
-
- stub_feature_flags(snippets_binary_blob: snippets_binary_blob_value)
-
- subject
- end
-
- context 'when blob is text' do
- let(:binary) { false }
-
- it 'responds with status 200' do
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to render_template(:edit)
- end
- end
-
- context 'when blob is binary' do
- let(:binary) { true }
-
- it 'responds with status 200' do
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to render_template(:edit)
- end
-
- context 'when feature flag :snippets_binary_blob is disabled' do
- let(:snippets_binary_blob_value) { false }
-
- it 'redirects away' do
- expect(response).to redirect_to(gitlab_snippet_path(snippet))
- end
- end
- end
-end
-
-RSpec.shared_examples 'updating snippet checks blob is binary' do
- let(:snippets_binary_blob_value) { true }
-
- before do
- sign_in(user)
-
- allow_next_instance_of(Blob) do |blob|
- allow(blob).to receive(:binary?).and_return(binary)
- end
-
- stub_feature_flags(snippets_binary_blob: snippets_binary_blob_value)
-
- subject
- end
-
- context 'when blob is text' do
- let(:binary) { false }
-
- it 'updates successfully' do
- expect(snippet.reload.title).to eq title
- expect(response).to redirect_to(gitlab_snippet_path(snippet))
- end
- end
-
- context 'when blob is binary' do
- let(:binary) { true }
-
- it 'updates successfully' do
- expect(snippet.reload.title).to eq title
- expect(response).to redirect_to(gitlab_snippet_path(snippet))
- end
-
- context 'when feature flag :snippets_binary_blob is disabled' do
- let(:snippets_binary_blob_value) { false }
-
- it 'redirects away without updating' do
- expect(response).to redirect_to(gitlab_snippet_path(snippet))
- expect(snippet.reload.title).not_to eq title
- end
- end
- end
-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
deleted file mode 100644
index 17087456720..00000000000
--- a/spec/support/shared_examples/controllers/instance_statistics_controllers_shared_examples.rb
+++ /dev/null
@@ -1,51 +0,0 @@
-# frozen_string_literal: true
-
-RSpec.shared_examples 'instance statistics availability' do
- let(:user) { create(:user) }
-
- before do
- sign_in(user)
-
- stub_application_setting(usage_ping_enabled: true)
- end
-
- describe 'GET #index' do
- it 'is available when the feature is available publicly' do
- get :index
-
- expect(response).to have_gitlab_http_status(:success)
- end
-
- it 'renders a 404 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(:not_found)
- end
-
- context 'for admins' do
- let(:user) { create(:admin) }
-
- 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
-
- 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
-end
diff --git a/spec/support/shared_examples/controllers/issuables_list_metadata_shared_examples.rb b/spec/support/shared_examples/controllers/issuables_list_metadata_shared_examples.rb
index 62a1a07b6c1..02915206cc5 100644
--- a/spec/support/shared_examples/controllers/issuables_list_metadata_shared_examples.rb
+++ b/spec/support/shared_examples/controllers/issuables_list_metadata_shared_examples.rb
@@ -42,10 +42,6 @@ RSpec.shared_examples 'issuables list meta-data' do |issuable_type, action = nil
let(:result_issuable) { issuables.first }
let(:search) { result_issuable.title }
- before do
- stub_feature_flags(attempt_project_search_optimizations: true)
- end
-
# .simple_sorts is the same across all Sortable classes
sorts = ::Issue.simple_sorts.keys + %w[popularity priority label_priority]
sorts.each do |sort|
diff --git a/spec/support/shared_examples/controllers/unique_hll_events_examples.rb b/spec/support/shared_examples/controllers/unique_hll_events_examples.rb
new file mode 100644
index 00000000000..7e5a225f020
--- /dev/null
+++ b/spec/support/shared_examples/controllers/unique_hll_events_examples.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'tracking unique hll events' do |feature_flag|
+ context 'when format is HTML' do
+ let(:format) { :html }
+
+ it 'tracks unique event' do
+ expect(Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:track_event).with(expected_type, target_id)
+
+ subject
+ end
+
+ it 'tracks unique event if DNT is not enabled' do
+ expect(Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:track_event).with(expected_type, target_id)
+ request.headers['DNT'] = '0'
+
+ subject
+ end
+
+ it 'does not track unique event if DNT is enabled' do
+ expect(Gitlab::UsageDataCounters::HLLRedisCounter).not_to receive(:track_event).with(expected_type, target_id)
+ request.headers['DNT'] = '1'
+
+ subject
+ end
+
+ context 'when feature flag is disabled' do
+ it 'does not track unique event' do
+ stub_feature_flags(feature_flag => false)
+
+ expect(Gitlab::UsageDataCounters::HLLRedisCounter).not_to receive(:track_event).with(expected_type, target_id)
+
+ subject
+ end
+ end
+ end
+
+ context 'when format is JSON' do
+ let(:format) { :json }
+
+ it 'does not track unique event if the format is JSON' do
+ expect(Gitlab::UsageDataCounters::HLLRedisCounter).not_to receive(:track_event).with(expected_type, target_id)
+
+ subject
+ end
+ end
+end
diff --git a/spec/support/shared_examples/controllers/unique_visits_shared_examples.rb b/spec/support/shared_examples/controllers/unique_visits_shared_examples.rb
index 90588756eb0..428389a9a01 100644
--- a/spec/support/shared_examples/controllers/unique_visits_shared_examples.rb
+++ b/spec/support/shared_examples/controllers/unique_visits_shared_examples.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
RSpec.shared_examples 'tracking unique visits' do |method|
+ let(:request_params) { {} }
+
it 'tracks unique visit if the format is HTML' do
expect_any_instance_of(Gitlab::Analytics::UniqueVisits).to receive(:track_visit).with(instance_of(String), target_id)
diff --git a/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb b/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb
index c89ee0d25ae..4ca400dd87b 100644
--- a/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb
+++ b/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb
@@ -388,7 +388,54 @@ RSpec.shared_examples 'wiki controller actions' do
end.not_to change { wiki.list_pages.size }
expect(response).to render_template('shared/wikis/edit')
- expect(flash[:alert]).to eq('Could not create wiki page')
+ end
+ end
+ end
+
+ describe 'DELETE #destroy' do
+ let(:id_param) { wiki_title }
+
+ subject do
+ delete(:destroy,
+ params: routing_params.merge(
+ id: id_param
+ ))
+ end
+
+ context 'when page exists' do
+ it 'deletes the page' do
+ expect do
+ subject
+ end.to change { wiki.list_pages.size }.by(-1)
+ end
+
+ context 'but page cannot be deleted' do
+ before do
+ allow_next_instance_of(WikiPage) do |page|
+ allow(page).to receive(:delete).and_return(false)
+ end
+ end
+
+ it 'renders the edit state' do
+ expect do
+ subject
+ end.not_to change { wiki.list_pages.size }
+
+ expect(response).to render_template('shared/wikis/edit')
+ expect(assigns(:error).message).to eq('Could not delete wiki page')
+ end
+ end
+ end
+
+ context 'when page does not exist' do
+ let(:id_param) { 'nil' }
+
+ it 'renders 404' do
+ expect do
+ subject
+ end.not_to change { wiki.list_pages.size }
+
+ expect(response).to have_gitlab_http_status(:not_found)
end
end
end
diff --git a/spec/support/shared_examples/features/2fa_shared_examples.rb b/spec/support/shared_examples/features/2fa_shared_examples.rb
new file mode 100644
index 00000000000..ddc03e178ba
--- /dev/null
+++ b/spec/support/shared_examples/features/2fa_shared_examples.rb
@@ -0,0 +1,108 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'hardware device for 2fa' do |device_type|
+ include Spec::Support::Helpers::Features::TwoFactorHelpers
+
+ def register_device(device_type, **kwargs)
+ case device_type.downcase
+ when "u2f"
+ register_u2f_device(**kwargs)
+ when "webauthn"
+ register_webauthn_device(**kwargs)
+ else
+ raise "Unknown device type #{device_type}"
+ end
+ end
+
+ describe "registration" do
+ let(:user) { create(:user) }
+
+ before do
+ gitlab_sign_in(user)
+ user.update_attribute(:otp_required_for_login, true)
+ end
+
+ describe 'when 2FA via OTP is disabled' do
+ before do
+ user.update_attribute(:otp_required_for_login, false)
+ end
+
+ it 'does not allow registering a new device' do
+ visit profile_account_path
+ click_on 'Enable two-factor authentication'
+
+ expect(page).to have_button("Set up new device", disabled: true)
+ end
+ end
+
+ describe 'when 2FA via OTP is enabled' do
+ it 'allows registering a new device with a name' do
+ visit profile_account_path
+ manage_two_factor_authentication
+ expect(page).to have_content("You've already enabled two-factor authentication using one time password authenticators")
+
+ device = register_device(device_type)
+
+ expect(page).to have_content(device.name)
+ expect(page).to have_content("Your #{device_type} device was registered")
+ end
+
+ it 'allows deleting a device' do
+ visit profile_account_path
+ manage_two_factor_authentication
+ expect(page).to have_content("You've already enabled two-factor authentication using one time password authenticators")
+
+ first_device = register_device(device_type)
+ second_device = register_device(device_type, name: 'My other device')
+
+ expect(page).to have_content(first_device.name)
+ expect(page).to have_content(second_device.name)
+
+ accept_confirm { click_on 'Delete', match: :first }
+
+ expect(page).to have_content('Successfully deleted')
+ expect(page.body).not_to have_content(first_device.name)
+ expect(page.body).to have_content(second_device.name)
+ end
+ end
+ end
+
+ describe 'fallback code authentication' do
+ let(:user) { create(:user) }
+
+ before do
+ # Register and logout
+ gitlab_sign_in(user)
+ user.update_attribute(:otp_required_for_login, true)
+ visit profile_account_path
+ end
+
+ describe 'when no device is registered' do
+ before do
+ gitlab_sign_out
+ gitlab_sign_in(user)
+ end
+
+ it 'shows the fallback otp code UI' do
+ assert_fallback_ui(page)
+ end
+ end
+
+ describe 'when a device is registered' do
+ before do
+ manage_two_factor_authentication
+ register_device(device_type)
+ gitlab_sign_out
+ gitlab_sign_in(user)
+ end
+
+ it 'provides a button that shows the fallback otp code UI' do
+ expect(page).to have_link('Sign in via 2FA code')
+
+ click_link('Sign in via 2FA code')
+
+ assert_fallback_ui(page)
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/features/editable_merge_request_shared_examples.rb b/spec/support/shared_examples/features/editable_merge_request_shared_examples.rb
index 487c38da7da..c9910487798 100644
--- a/spec/support/shared_examples/features/editable_merge_request_shared_examples.rb
+++ b/spec/support/shared_examples/features/editable_merge_request_shared_examples.rb
@@ -124,3 +124,16 @@ end
def get_textarea_height
page.evaluate_script('document.getElementById("merge_request_description").offsetHeight')
end
+
+RSpec.shared_examples 'an editable merge request with reviewers' do
+ it 'updates merge request', :js do
+ find('.js-reviewer-search').click
+ page.within '.dropdown-menu-user' do
+ click_link user.name
+ end
+ expect(find('input[name="merge_request[reviewer_ids][]"]', visible: false).value).to match(user.id.to_s)
+ page.within '.js-reviewer-search' do
+ expect(page).to have_content user.name
+ 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 ae7d62f31a2..92fc54ce0b0 100644
--- a/spec/support/shared_examples/features/error_tracking_shared_example.rb
+++ b/spec/support/shared_examples/features/error_tracking_shared_example.rb
@@ -36,10 +36,10 @@ end
RSpec.shared_examples 'expanded stack trace context' do |selected_line: nil, expected_line: 1|
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
+ find("div.file-holder:nth-child(#{selected_line}) svg[data-testid='chevron-right-icon']").click if selected_line
expanded_line = find("div.file-holder:nth-child(#{expected_line})")
- expect(expanded_line).to have_css('svg.ic-chevron-down')
+ expect(expanded_line).to have_css('svg[data-testid="chevron-down-icon"]')
event_response['entries'][0]['data']['values'][0]['stacktrace']['frames'][-expected_line]['context'].each do |context|
expect(page).to have_content(context[0])
diff --git a/spec/support/shared_examples/features/file_uploads_shared_examples.rb b/spec/support/shared_examples/features/file_uploads_shared_examples.rb
new file mode 100644
index 00000000000..ea8c8d44501
--- /dev/null
+++ b/spec/support/shared_examples/features/file_uploads_shared_examples.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'handling file uploads' do |shared_examples_name|
+ context 'with object storage disabled' do
+ context 'with upload_middleware_jwt_params_handler disabled' do
+ before do
+ stub_feature_flags(upload_middleware_jwt_params_handler: false)
+
+ expect_next_instance_of(Gitlab::Middleware::Multipart::Handler) do |handler|
+ expect(handler).to receive(:with_open_files).and_call_original
+ end
+ end
+
+ it_behaves_like shared_examples_name
+ end
+
+ context 'with upload_middleware_jwt_params_handler enabled' do
+ before do
+ stub_feature_flags(upload_middleware_jwt_params_handler: true)
+
+ expect_next_instance_of(Gitlab::Middleware::Multipart::HandlerForJWTParams) do |handler|
+ expect(handler).to receive(:with_open_files).and_call_original
+ end
+ end
+
+ it_behaves_like shared_examples_name
+ end
+ end
+end
diff --git a/spec/support/shared_examples/features/packages_shared_examples.rb b/spec/support/shared_examples/features/packages_shared_examples.rb
index 6debbf81fc0..f201421e827 100644
--- a/spec/support/shared_examples/features/packages_shared_examples.rb
+++ b/spec/support/shared_examples/features/packages_shared_examples.rb
@@ -14,7 +14,7 @@ RSpec.shared_examples 'packages list' do |check_project_name: false|
end
def package_table_row(index)
- page.all("#{packages_table_selector} > [data-qa-selector=\"packages-row\"]")[index].text
+ page.all("#{packages_table_selector} > [data-qa-selector=\"package_row\"]")[index].text
end
end
@@ -32,7 +32,7 @@ RSpec.shared_examples 'package details link' do |property|
expect(page).to have_current_path(project_package_path(package.project, package))
- page.within('.detail-page-header') do
+ page.within('[data-qa-selector="package_title"]') do
expect(page).to have_content(package.name)
end
diff --git a/spec/support/shared_examples/graphql/members_shared_examples.rb b/spec/support/shared_examples/graphql/members_shared_examples.rb
index a58e716efd2..3a046c3feec 100644
--- a/spec/support/shared_examples/graphql/members_shared_examples.rb
+++ b/spec/support/shared_examples/graphql/members_shared_examples.rb
@@ -20,3 +20,64 @@ RSpec.shared_examples 'a working membership object query' do |model_option|
).to eq('DEVELOPER')
end
end
+
+RSpec.shared_examples 'querying members with a group' do
+ let_it_be(:root_group) { create(:group, :private) }
+ let_it_be(:group_1) { create(:group, :private, parent: root_group, name: 'Main Group') }
+ let_it_be(:group_2) { create(:group, :private, parent: root_group) }
+
+ let_it_be(:user_1) { create(:user, name: 'test user') }
+ let_it_be(:user_2) { create(:user, name: 'test user 2') }
+ let_it_be(:user_3) { create(:user, name: 'another user 1') }
+ let_it_be(:user_4) { create(:user, name: 'another user 2') }
+
+ let_it_be(:root_group_member) { create(:group_member, user: user_4, group: root_group) }
+ let_it_be(:group_1_member) { create(:group_member, user: user_2, group: group_1) }
+ let_it_be(:group_2_member) { create(:group_member, user: user_3, group: group_2) }
+
+ let(:args) { {} }
+
+ subject do
+ resolve(described_class, obj: resource, args: args, ctx: { current_user: user_4 })
+ end
+
+ describe '#resolve' do
+ before do
+ group_1.add_maintainer(user_4)
+ end
+
+ it 'finds all resource members' do
+ expect(subject).to contain_exactly(resource_member, group_1_member, root_group_member)
+ end
+
+ context 'with search' do
+ context 'when the search term matches a user' do
+ let(:args) { { search: 'test' } }
+
+ it 'searches users by user name' do
+ expect(subject).to contain_exactly(resource_member, group_1_member)
+ end
+ end
+
+ context 'when the search term does not match any user' do
+ let(:args) { { search: 'nothing' } }
+
+ it 'is empty' do
+ expect(subject).to be_empty
+ end
+ end
+ end
+
+ context 'when user can not see resource members' do
+ let_it_be(:other_user) { create(:user) }
+
+ subject do
+ resolve(described_class, obj: resource, args: args, ctx: { current_user: other_user })
+ end
+
+ it 'raises an error' do
+ expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/graphql/mutation_shared_examples.rb b/spec/support/shared_examples/graphql/mutation_shared_examples.rb
index 86d2bb6c747..b67cac94547 100644
--- a/spec/support/shared_examples/graphql/mutation_shared_examples.rb
+++ b/spec/support/shared_examples/graphql/mutation_shared_examples.rb
@@ -19,6 +19,13 @@ RSpec.shared_examples 'a mutation that returns top-level errors' do |errors: []|
end
end
+# There must be a method or let called `mutation` defined that executes
+# the mutation.
+RSpec.shared_examples 'a mutation that returns a top-level access error' do
+ include_examples 'a mutation that returns top-level errors',
+ errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR]
+end
+
RSpec.shared_examples 'an invalid argument to the mutation' do |argument_name:|
it_behaves_like 'a mutation that returns top-level errors' do
let(:match_errors) do
diff --git a/spec/support/shared_examples/graphql/sorted_paginated_query_shared_examples.rb b/spec/support/shared_examples/graphql/sorted_paginated_query_shared_examples.rb
index 2ef71d275a2..7627a7b4d59 100644
--- a/spec/support/shared_examples/graphql/sorted_paginated_query_shared_examples.rb
+++ b/spec/support/shared_examples/graphql/sorted_paginated_query_shared_examples.rb
@@ -84,6 +84,9 @@ RSpec.shared_examples 'sorted paginated query' do
cursored_query = pagination_query([sort_argument, "after: \"#{end_cursor}\""].compact.join(','), page_info)
post_graphql(cursored_query, current_user: current_user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+
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)
diff --git a/spec/support/shared_examples/graphql/types/gitlab_style_deprecations_shared_examples.rb b/spec/support/shared_examples/graphql/types/gitlab_style_deprecations_shared_examples.rb
new file mode 100644
index 00000000000..ed139e638bf
--- /dev/null
+++ b/spec/support/shared_examples/graphql/types/gitlab_style_deprecations_shared_examples.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'Gitlab-style deprecations' do
+ describe 'validations' do
+ it 'raises an informative error if `deprecation_reason` is used' do
+ expect { subject(deprecation_reason: 'foo') }.to raise_error(
+ ArgumentError,
+ 'Use `deprecated` property instead of `deprecation_reason`. ' \
+ 'See https://docs.gitlab.com/ee/development/api_graphql_styleguide.html#deprecating-fields-and-enum-values'
+ )
+ end
+
+ it 'raises an error if a required property is missing', :aggregate_failures do
+ expect { subject(deprecated: { milestone: '1.10' }) }.to raise_error(
+ ArgumentError,
+ 'Please provide a `reason` within `deprecated`'
+ )
+ expect { subject(deprecated: { reason: 'Deprecation reason' }) }.to raise_error(
+ ArgumentError,
+ 'Please provide a `milestone` within `deprecated`'
+ )
+ end
+
+ it 'raises an error if milestone is not a String', :aggregate_failures do
+ expect { subject(deprecated: { milestone: 1.10, reason: 'Deprecation reason' }) }.to raise_error(
+ ArgumentError,
+ '`milestone` must be a `String`'
+ )
+ end
+ end
+
+ it 'adds a formatted `deprecated_reason` to the subject' do
+ deprecable = subject(deprecated: { milestone: '1.10', reason: 'Deprecation reason' })
+
+ expect(deprecable.deprecation_reason).to eq('Deprecation reason. Deprecated in 1.10')
+ end
+
+ it 'appends to the description if given' do
+ deprecable = subject(
+ deprecated: { milestone: '1.10', reason: 'Deprecation reason' },
+ description: 'Deprecable description'
+ )
+
+ expect(deprecable.description).to eq('Deprecable description. Deprecated in 1.10: Deprecation reason')
+ end
+
+ it 'does not append to the description if it is absent' do
+ deprecable = subject(deprecated: { milestone: '1.10', reason: 'Deprecation reason' })
+
+ expect(deprecable.description).to be_nil
+ end
+end
diff --git a/spec/support/shared_examples/lib/banzai/reference_parser_shared_examples.rb b/spec/support/shared_examples/lib/banzai/reference_parser_shared_examples.rb
index d903c0f10e0..479b26977e2 100644
--- a/spec/support/shared_examples/lib/banzai/reference_parser_shared_examples.rb
+++ b/spec/support/shared_examples/lib/banzai/reference_parser_shared_examples.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
RSpec.shared_examples "referenced feature visibility" do |*related_features|
+ let(:enable_user?) { false }
let(:feature_fields) do
related_features.map { |feature| (feature + "_access_level").to_sym }
end
@@ -35,8 +36,11 @@ RSpec.shared_examples "referenced feature visibility" do |*related_features|
end
context "when feature is enabled" do
- # The project is public
+ # Allows implementing specs to enable finer-tuned permissions
+ let(:enable_user?) { true }
+
it "creates reference" do
+ # The project is public
set_features_fields_to(ProjectFeature::ENABLED)
expect(subject.nodes_visible_to_user(user, [link])).to eq([link])
diff --git a/spec/support/shared_examples/lib/gitlab/alert_management/payload.rb b/spec/support/shared_examples/lib/gitlab/alert_management/payload.rb
new file mode 100644
index 00000000000..54b021e8371
--- /dev/null
+++ b/spec/support/shared_examples/lib/gitlab/alert_management/payload.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'parsable alert payload field with fallback' do |fallback, *paths|
+ context 'without payload' do
+ it { is_expected.to eq(fallback) }
+ end
+
+ paths.each do |path|
+ context "with #{path}" do
+ let(:value) { 'some value' }
+
+ before do
+ section, name = path.split('/')
+ raw_payload[section] = name ? { name => value } : value
+ end
+
+ it { is_expected.to eq(value) }
+ end
+ end
+end
+
+RSpec.shared_examples 'parsable alert payload field' do |*paths|
+ it_behaves_like 'parsable alert payload field with fallback', nil, *paths
+end
+
+RSpec.shared_examples 'subclass has expected api' do
+ it 'defines all public methods in the base class' do
+ default_methods = Gitlab::AlertManagement::Payload::Base.public_instance_methods
+ subclass_methods = described_class.public_instance_methods
+ missing_methods = subclass_methods - default_methods
+
+ expect(missing_methods).to be_empty
+ end
+end
diff --git a/spec/support/shared_examples/lib/gitlab/auth/atlassian_identity_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/auth/atlassian_identity_shared_examples.rb
new file mode 100644
index 00000000000..18a5087da3b
--- /dev/null
+++ b/spec/support/shared_examples/lib/gitlab/auth/atlassian_identity_shared_examples.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'an atlassian identity' do
+ it 'sets the proper values' do
+ expect(identity.extern_uid).to eq(extern_uid)
+ expect(identity.token).to eq(credentials[:token])
+ expect(identity.refresh_token).to eq(credentials[:refresh_token])
+ expect(identity.expires_at.to_i).to eq(credentials[:expires_at])
+ end
+end
diff --git a/spec/support/shared_examples/lib/gitlab/background_migration/mentions_migration_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/background_migration/mentions_migration_shared_examples.rb
index 8cf6babe146..e93077c42e1 100644
--- a/spec/support/shared_examples/lib/gitlab/background_migration/mentions_migration_shared_examples.rb
+++ b/spec/support/shared_examples/lib/gitlab/background_migration/mentions_migration_shared_examples.rb
@@ -63,7 +63,7 @@ RSpec.shared_examples 'schedules resource mentions migration' do |resource_class
it 'schedules background migrations' do
Sidekiq::Testing.fake! do
- Timecop.freeze do
+ freeze_time do
resource_count = is_for_notes ? Note.count : resource_class.count
expect(resource_count).to eq 5
diff --git a/spec/support/shared_examples/lib/gitlab/kubernetes/network_policy_common_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/kubernetes/network_policy_common_shared_examples.rb
index a3800f050bb..f018ece0d46 100644
--- a/spec/support/shared_examples/lib/gitlab/kubernetes/network_policy_common_shared_examples.rb
+++ b/spec/support/shared_examples/lib/gitlab/kubernetes/network_policy_common_shared_examples.rb
@@ -5,18 +5,19 @@ RSpec.shared_examples 'network policy common specs' do
let(:namespace) { 'example-namespace' }
let(:labels) { nil }
+ describe '#generate' do
+ subject { policy.generate }
+
+ it { is_expected.to eq(Kubeclient::Resource.new(policy.resource)) }
+ end
+
describe 'as_json' do
let(:json_policy) do
{
name: name,
namespace: namespace,
creation_timestamp: nil,
- manifest: YAML.dump(
- {
- metadata: metadata,
- spec: spec
- }.deep_stringify_keys
- ),
+ manifest: YAML.dump(policy.resource.deep_stringify_keys),
is_autodevops: false,
is_enabled: true
}
diff --git a/spec/support/shared_examples/lib/gitlab/middleware/multipart_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/middleware/multipart_shared_examples.rb
new file mode 100644
index 00000000000..6327367fcc2
--- /dev/null
+++ b/spec/support/shared_examples/lib/gitlab/middleware/multipart_shared_examples.rb
@@ -0,0 +1,145 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'handling all upload parameters conditions' do
+ context 'one root parameter' do
+ include_context 'with one temporary file for multipart'
+
+ let(:rewritten_fields) { rewritten_fields_hash('file' => uploaded_filepath) }
+ let(:params) { upload_parameters_for(filepath: uploaded_filepath, key: 'file', filename: filename, remote_id: remote_id) }
+
+ it 'builds an UploadedFile' do
+ expect_uploaded_files(filepath: uploaded_filepath, original_filename: filename, remote_id: remote_id, size: uploaded_file.size, params_path: %w(file))
+
+ subject
+ end
+ end
+
+ context 'two root parameters' do
+ include_context 'with two temporary files for multipart'
+
+ let(:rewritten_fields) { rewritten_fields_hash('file1' => uploaded_filepath, 'file2' => uploaded_filepath2) }
+ let(:params) do
+ upload_parameters_for(filepath: uploaded_filepath, key: 'file1', filename: filename, remote_id: remote_id).merge(
+ upload_parameters_for(filepath: uploaded_filepath2, key: 'file2', filename: filename2, remote_id: remote_id2)
+ )
+ end
+
+ it 'builds UploadedFiles' do
+ expect_uploaded_files([
+ { filepath: uploaded_filepath, original_filename: filename, remote_id: remote_id, size: uploaded_file.size, params_path: %w(file1) },
+ { filepath: uploaded_filepath2, original_filename: filename2, remote_id: remote_id2, size: uploaded_file2.size, params_path: %w(file2) }
+ ])
+
+ subject
+ end
+ end
+
+ context 'one nested parameter' do
+ include_context 'with one temporary file for multipart'
+
+ let(:rewritten_fields) { rewritten_fields_hash('user[avatar]' => uploaded_filepath) }
+ let(:params) { { 'user' => { 'avatar' => upload_parameters_for(filepath: uploaded_filepath, filename: filename, remote_id: remote_id) } } }
+
+ it 'builds an UploadedFile' do
+ expect_uploaded_files(filepath: uploaded_filepath, original_filename: filename, remote_id: remote_id, size: uploaded_file.size, params_path: %w(user avatar))
+
+ subject
+ end
+ end
+
+ context 'two nested parameters' do
+ include_context 'with two temporary files for multipart'
+
+ let(:rewritten_fields) { rewritten_fields_hash('user[avatar]' => uploaded_filepath, 'user[screenshot]' => uploaded_filepath2) }
+ let(:params) do
+ {
+ 'user' => {
+ 'avatar' => upload_parameters_for(filepath: uploaded_filepath, filename: filename, remote_id: remote_id),
+ 'screenshot' => upload_parameters_for(filepath: uploaded_filepath2, filename: filename2, remote_id: remote_id2)
+ }
+ }
+ end
+
+ it 'builds UploadedFiles' do
+ expect_uploaded_files([
+ { filepath: uploaded_filepath, original_filename: filename, remote_id: remote_id, size: uploaded_file.size, params_path: %w(user avatar) },
+ { filepath: uploaded_filepath2, original_filename: filename2, remote_id: remote_id2, size: uploaded_file2.size, params_path: %w(user screenshot) }
+ ])
+
+ subject
+ end
+ end
+
+ context 'one deeply nested parameter' do
+ include_context 'with one temporary file for multipart'
+
+ let(:rewritten_fields) { rewritten_fields_hash('user[avatar][bananas]' => uploaded_filepath) }
+ let(:params) { { 'user' => { 'avatar' => { 'bananas' => upload_parameters_for(filepath: uploaded_filepath, filename: filename, remote_id: remote_id) } } } }
+
+ it 'builds an UploadedFile' do
+ expect_uploaded_files(filepath: uploaded_file, original_filename: filename, remote_id: remote_id, size: uploaded_file.size, params_path: %w(user avatar bananas))
+
+ subject
+ end
+ end
+
+ context 'two deeply nested parameters' do
+ include_context 'with two temporary files for multipart'
+
+ let(:rewritten_fields) { rewritten_fields_hash('user[avatar][bananas]' => uploaded_filepath, 'user[friend][ananas]' => uploaded_filepath2) }
+ let(:params) do
+ {
+ 'user' => {
+ 'avatar' => {
+ 'bananas' => upload_parameters_for(filepath: uploaded_filepath, filename: filename, remote_id: remote_id)
+ },
+ 'friend' => {
+ 'ananas' => upload_parameters_for(filepath: uploaded_filepath2, filename: filename2, remote_id: remote_id2)
+ }
+ }
+ }
+ end
+
+ it 'builds UploadedFiles' do
+ expect_uploaded_files([
+ { filepath: uploaded_file, original_filename: filename, remote_id: remote_id, size: uploaded_file.size, params_path: %w(user avatar bananas) },
+ { filepath: uploaded_file2, original_filename: filename2, remote_id: remote_id2, size: uploaded_file2.size, params_path: %w(user friend ananas) }
+ ])
+
+ subject
+ end
+ end
+
+ context 'three parameters nested at different levels' do
+ include_context 'with three temporary files for multipart'
+
+ let(:rewritten_fields) do
+ rewritten_fields_hash(
+ 'file' => uploaded_filepath,
+ 'user[avatar]' => uploaded_filepath2,
+ 'user[friend][avatar]' => uploaded_filepath3
+ )
+ end
+
+ let(:params) do
+ upload_parameters_for(filepath: uploaded_filepath, filename: filename, key: 'file', remote_id: remote_id).merge(
+ 'user' => {
+ 'avatar' => upload_parameters_for(filepath: uploaded_filepath2, filename: filename2, remote_id: remote_id2),
+ 'friend' => {
+ 'avatar' => upload_parameters_for(filepath: uploaded_filepath3, filename: filename3, remote_id: remote_id3)
+ }
+ }
+ )
+ end
+
+ it 'builds UploadedFiles' do
+ expect_uploaded_files([
+ { filepath: uploaded_filepath, original_filename: filename, remote_id: remote_id, size: uploaded_file.size, params_path: %w(file) },
+ { filepath: uploaded_filepath2, original_filename: filename2, remote_id: remote_id2, size: uploaded_file2.size, params_path: %w(user avatar) },
+ { filepath: uploaded_filepath3, original_filename: filename3, remote_id: remote_id3, size: uploaded_file3.size, params_path: %w(user friend avatar) }
+ ])
+
+ subject
+ end
+ end
+end
diff --git a/spec/support/shared_examples/lib/gitlab/project_search_results_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/project_search_results_shared_examples.rb
new file mode 100644
index 00000000000..94ef41ce5a5
--- /dev/null
+++ b/spec/support/shared_examples/lib/gitlab/project_search_results_shared_examples.rb
@@ -0,0 +1,79 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'access restricted confidential issues' do
+ let(:query) { 'issue' }
+ let(:author) { create(:user) }
+ let(:assignee) { create(:user) }
+ let(:project) { create(:project, :internal) }
+
+ let!(:issue) { create(:issue, project: project, title: 'Issue 1') }
+ let!(:security_issue_1) { create(:issue, :confidential, project: project, title: 'Security issue 1', author: author) }
+ let!(:security_issue_2) { create(:issue, :confidential, title: 'Security issue 2', project: project, assignees: [assignee]) }
+
+ subject(:objects) do
+ described_class.new(user, query, project: project).objects('issues')
+ end
+
+ context 'when the user is non-member' do
+ let(:user) { create(:user) }
+
+ it 'does not list project confidential issues for non project members' do
+ expect(objects).to contain_exactly(issue)
+ expect(results.limited_issues_count).to eq 1
+ end
+ end
+
+ context 'when the member is guest' do
+ let(:user) do
+ create(:user) { |guest| project.add_guest(guest) }
+ end
+
+ it 'does not list project confidential issues for project members with guest role' do
+ expect(objects).to contain_exactly(issue)
+ expect(results.limited_issues_count).to eq 1
+ end
+ end
+
+ context 'when the user is the author' do
+ let(:user) { author }
+
+ it 'lists project confidential issues' do
+ expect(objects).to contain_exactly(issue,
+ security_issue_1)
+ expect(results.limited_issues_count).to eq 2
+ end
+ end
+
+ context 'when the user is the assignee' do
+ let(:user) { assignee }
+
+ it 'lists project confidential issues for assignee' do
+ expect(objects).to contain_exactly(issue,
+ security_issue_2)
+ expect(results.limited_issues_count).to eq 2
+ end
+ end
+
+ context 'when the user is a developper' do
+ let(:user) do
+ create(:user) { |user| project.add_developer(user) }
+ end
+
+ it 'lists project confidential issues' do
+ expect(objects).to contain_exactly(issue,
+ security_issue_1,
+ security_issue_2)
+ expect(results.limited_issues_count).to eq 3
+ end
+ end
+
+ context 'when the user is admin', :request_store do
+ let(:user) { create(:user, admin: true) }
+
+ it 'lists all project issues' do
+ expect(objects).to contain_exactly(issue,
+ security_issue_1,
+ security_issue_2)
+ end
+ end
+end
diff --git a/spec/support/shared_examples/lib/gitlab/repo_type_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/repo_type_shared_examples.rb
index 4aeae788114..025f0d5c7ea 100644
--- a/spec/support/shared_examples/lib/gitlab/repo_type_shared_examples.rb
+++ b/spec/support/shared_examples/lib/gitlab/repo_type_shared_examples.rb
@@ -17,5 +17,9 @@ RSpec.shared_examples 'a repo type' do
it 'finds the repository for the repo type' do
expect(described_class.repository_for(expected_container)).to eq(expected_repository)
end
+
+ it 'returns nil when container is nil' do
+ expect(described_class.repository_for(nil)).to eq(nil)
+ end
end
end
diff --git a/spec/support/shared_examples/lib/gitlab/search/recent_items.rb b/spec/support/shared_examples/lib/gitlab/search/recent_items.rb
new file mode 100644
index 00000000000..f96ff4b101e
--- /dev/null
+++ b/spec/support/shared_examples/lib/gitlab/search/recent_items.rb
@@ -0,0 +1,87 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.shared_examples 'search recent items' do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:recent_items) { described_class.new(user: user, items_limit: 5) }
+ let(:item) { create_item(content: 'hello world 1', project: project) }
+ let(:project) { create(:project, :public) }
+
+ describe '#log_view', :clean_gitlab_redis_shared_state do
+ it 'adds the item to the recent items' do
+ recent_items.log_view(item)
+
+ results = recent_items.search('hello')
+
+ expect(results).to eq([item])
+ end
+
+ it 'removes an item when it exceeds the size items_limit' do
+ (1..6).each do |i|
+ recent_items.log_view(create_item(content: "item #{i}", project: project))
+ end
+
+ results = recent_items.search('item')
+
+ expect(results.map(&:title)).to contain_exactly('item 6', 'item 5', 'item 4', 'item 3', 'item 2')
+ end
+
+ it 'expires the items after expires_after' do
+ recent_items = described_class.new(user: user, expires_after: 0)
+
+ recent_items.log_view(item)
+
+ results = recent_items.search('hello')
+
+ expect(results).to be_empty
+ end
+
+ it 'does not include results logged for another user' do
+ another_user = create(:user)
+ another_item = create_item(content: 'hello world 2', project: project)
+ described_class.new(user: another_user).log_view(another_item)
+ recent_items.log_view(item)
+
+ results = recent_items.search('hello')
+
+ expect(results).to eq([item])
+ end
+ end
+
+ describe '#search', :clean_gitlab_redis_shared_state do
+ let(:item1) { create_item(content: "matching item 1", project: project) }
+ let(:item2) { create_item(content: "matching item 2", project: project) }
+ let(:item3) { create_item(content: "matching item 3", project: project) }
+ let(:non_matching_item) { create_item(content: "different item", project: project) }
+ let!(:non_viewed_item) { create_item(content: "matching but not viewed item", project: project) }
+
+ before do
+ recent_items.log_view(item1)
+ recent_items.log_view(item2)
+ recent_items.log_view(item3)
+ recent_items.log_view(non_matching_item)
+ end
+
+ it 'matches partial text in the item title' do
+ expect(recent_items.search('matching')).to contain_exactly(item1, item2, item3)
+ end
+
+ it 'returns results sorted by recently viewed' do
+ recent_items.log_view(item2)
+
+ expect(recent_items.search('matching')).to eq([item2, item3, item1])
+ end
+
+ it 'does not leak items you no longer have access to' do
+ private_project = create(:project, :public, namespace: create(:group))
+ private_item = create_item(content: 'matching item title', project: private_project)
+
+ recent_items.log_view(private_item)
+
+ private_project.update!(visibility_level: Project::PRIVATE)
+
+ expect(recent_items.search('matching')).not_to include(private_item)
+ end
+ end
+end
diff --git a/spec/support/shared_examples/lib/gitlab/search_issue_state_filter_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/search_issue_state_filter_shared_examples.rb
new file mode 100644
index 00000000000..e80ec516407
--- /dev/null
+++ b/spec/support/shared_examples/lib/gitlab/search_issue_state_filter_shared_examples.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'search results filtered by state' do
+ context 'state not provided' do
+ let(:filters) { {} }
+
+ it 'returns opened and closed results', :aggregate_failures do
+ expect(results.objects(scope)).to include opened_result
+ expect(results.objects(scope)).to include closed_result
+ end
+ end
+
+ context 'all state' do
+ let(:filters) { { state: 'all' } }
+
+ it 'returns opened and closed results', :aggregate_failures do
+ expect(results.objects(scope)).to include opened_result
+ expect(results.objects(scope)).to include closed_result
+ end
+ end
+
+ context 'closed state' do
+ let(:filters) { { state: 'closed' } }
+
+ it 'returns only closed results', :aggregate_failures do
+ expect(results.objects(scope)).not_to include opened_result
+ expect(results.objects(scope)).to include closed_result
+ end
+ end
+
+ context 'opened state' do
+ let(:filters) { { state: 'opened' } }
+
+ it 'returns only opened results', :aggregate_failures do
+ expect(results.objects(scope)).to include opened_result
+ expect(results.objects(scope)).not_to include closed_result
+ end
+ end
+
+ context 'unsupported state' do
+ let(:filters) { { state: 'hello' } }
+
+ it 'returns only opened results', :aggregate_failures do
+ expect(results.objects(scope)).to include opened_result
+ expect(results.objects(scope)).to include closed_result
+ end
+ end
+end
diff --git a/spec/support/shared_examples/lib/gitlab/sql/set_operator_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/sql/set_operator_shared_examples.rb
new file mode 100644
index 00000000000..73beef06855
--- /dev/null
+++ b/spec/support/shared_examples/lib/gitlab/sql/set_operator_shared_examples.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'SQL set operator' do |operator_keyword|
+ operator_keyword = operator_keyword.upcase
+
+ let(:relation_1) { User.where(email: 'alice@example.com').select(:id) }
+ let(:relation_2) { User.where(email: 'bob@example.com').select(:id) }
+
+ def to_sql(relation)
+ relation.reorder(nil).to_sql
+ end
+
+ describe '.operator_keyword' do
+ it { expect(described_class.operator_keyword).to eq operator_keyword }
+ end
+
+ describe '#to_sql' do
+ it "returns a String joining relations together using a #{operator_keyword}" do
+ set_operator = described_class.new([relation_1, relation_2])
+
+ expect(set_operator.to_sql).to eq("(#{to_sql(relation_1)})\n#{operator_keyword}\n(#{to_sql(relation_2)})")
+ end
+
+ it 'skips Model.none segements' do
+ empty_relation = User.none
+ set_operator = described_class.new([empty_relation, relation_1, relation_2])
+
+ expect {User.where("users.id IN (#{set_operator.to_sql})").to_a}.not_to raise_error
+ expect(set_operator.to_sql).to eq("(#{to_sql(relation_1)})\n#{operator_keyword}\n(#{to_sql(relation_2)})")
+ end
+
+ it "uses #{operator_keyword} ALL when removing duplicates is disabled" do
+ set_operator = described_class
+ .new([relation_1, relation_2], remove_duplicates: false)
+
+ expect(set_operator.to_sql).to include("#{operator_keyword} ALL")
+ end
+
+ it 'returns `NULL` if all relations are empty' do
+ empty_relation = User.none
+ set_operator = described_class.new([empty_relation, empty_relation])
+
+ expect(set_operator.to_sql).to eq('NULL')
+ end
+ end
+end
diff --git a/spec/support/shared_examples/lib/gitlab/usage_data_counters/incident_management_activity_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/usage_data_counters/incident_management_activity_shared_examples.rb
new file mode 100644
index 00000000000..4e35e388b23
--- /dev/null
+++ b/spec/support/shared_examples/lib/gitlab/usage_data_counters/incident_management_activity_shared_examples.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'an incident management tracked event' do |event|
+ describe ".track_event", :clean_gitlab_redis_shared_state do
+ let(:counter) { Gitlab::UsageDataCounters::HLLRedisCounter }
+ let(:start_time) { 1.minute.ago }
+ let(:end_time) { 1.minute.from_now }
+
+ it "tracks the event using redis" do
+ # Allow other subsequent calls
+ allow(Gitlab::UsageDataCounters::HLLRedisCounter)
+ .to receive(:track_event)
+
+ expect(Gitlab::UsageDataCounters::HLLRedisCounter)
+ .to receive(:track_event)
+ .with(current_user.id, event.to_s)
+ .and_call_original
+
+ expect { subject }
+ .to change { counter.unique_events(event_names: event.to_s, start_date: start_time, end_date: end_time) }
+ .by 1
+ end
+ end
+end
+
+RSpec.shared_examples 'does not track incident management event' do |event|
+ it 'does not track the event', :clean_gitlab_redis_shared_state do
+ expect(Gitlab::UsageDataCounters::HLLRedisCounter)
+ .not_to receive(:track_event)
+ .with(anything, event.to_s)
+ end
+end
diff --git a/spec/support/shared_examples/models/chat_slash_commands_shared_examples.rb b/spec/support/shared_examples/models/chat_slash_commands_shared_examples.rb
index 6611a168c04..0ee24dd93d7 100644
--- a/spec/support/shared_examples/models/chat_slash_commands_shared_examples.rb
+++ b/spec/support/shared_examples/models/chat_slash_commands_shared_examples.rb
@@ -85,7 +85,7 @@ RSpec.shared_examples 'chat slash commands service' do
let(:params) { { token: 'token', team_id: chat_name.team_id, user_id: chat_name.chat_id } }
subject do
- described_class.create(project: project, properties: { token: 'token' })
+ described_class.create!(project: project, properties: { token: 'token' })
end
it 'triggers the command' do
diff --git a/spec/support/shared_examples/models/cluster_application_helm_cert_shared_examples.rb b/spec/support/shared_examples/models/cluster_application_helm_cert_shared_examples.rb
index 394253fb699..ac8022a4726 100644
--- a/spec/support/shared_examples/models/cluster_application_helm_cert_shared_examples.rb
+++ b/spec/support/shared_examples/models/cluster_application_helm_cert_shared_examples.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
RSpec.shared_examples 'cluster application helm specs' do |application_name|
- let(:application) { create(application_name) }
+ let(:application) { create(application_name) } # rubocop:disable Rails/SaveBang
describe '#uninstall_command' do
subject { application.uninstall_command }
diff --git a/spec/support/shared_examples/models/concerns/from_set_operator_shared_examples.rb b/spec/support/shared_examples/models/concerns/from_set_operator_shared_examples.rb
new file mode 100644
index 00000000000..6b208c0024d
--- /dev/null
+++ b/spec/support/shared_examples/models/concerns/from_set_operator_shared_examples.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'from set operator' do |sql_klass|
+ from_set_operator_concern = described_class
+ operator_keyword = sql_klass.operator_keyword
+ operator_method = "from_#{sql_klass.operator_keyword.downcase}"
+
+ describe "##{operator_method}" do
+ let(:model) do
+ Class.new(ActiveRecord::Base) do
+ self.table_name = 'users'
+
+ include from_set_operator_concern
+ end
+ end
+
+ it "selects from the results of the #{operator_keyword}" do
+ query = model.public_send(operator_method, [model.where(id: 1), model.where(id: 2)])
+
+ expect(query.to_sql).to match(/FROM \(\(SELECT.+\)\n#{operator_keyword}\n\(SELECT.+\)\) users/m)
+ end
+
+ it 'supports the use of a custom alias for the sub query' do
+ query = model.public_send(operator_method,
+ [model.where(id: 1), model.where(id: 2)],
+ alias_as: 'kittens'
+ )
+
+ expect(query.to_sql).to match(/FROM \(\(SELECT.+\)\n#{operator_keyword}\n\(SELECT.+\)\) kittens/m)
+ end
+
+ it 'supports keeping duplicate rows' do
+ query = model.public_send(operator_method,
+ [model.where(id: 1), model.where(id: 2)],
+ remove_duplicates: false
+ )
+
+ expect(query.to_sql)
+ .to match(/FROM \(\(SELECT.+\)\n#{operator_keyword} ALL\n\(SELECT.+\)\) users/m)
+ end
+ end
+end
diff --git a/spec/support/shared_examples/models/concerns/limitable_shared_examples.rb b/spec/support/shared_examples/models/concerns/limitable_shared_examples.rb
index d21823661f8..07d687147bc 100644
--- a/spec/support/shared_examples/models/concerns/limitable_shared_examples.rb
+++ b/spec/support/shared_examples/models/concerns/limitable_shared_examples.rb
@@ -8,26 +8,29 @@ RSpec.shared_examples 'includes Limitable concern' do
context 'without plan limits configured' do
it 'can create new models' do
- expect { subject.save }.to change { described_class.count }
+ 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)
+ plan_limits.update!(subject.class.limit_name => 1)
end
it 'can create new models' do
- expect { subject.save }.to change { described_class.count }
+ expect { subject.save! }.to change { described_class.count }
end
context 'with an existing model' do
before do
- subject.dup.save
+ subject.dup.save!
end
it 'cannot create new models exceeding the plan limits' do
- expect { subject.save }.not_to change { described_class.count }
+ expect do
+ expect { subject.save! }.to raise_error(ActiveRecord::RecordInvalid)
+ end
+ .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
diff --git a/spec/support/shared_examples/models/concerns/timebox_shared_examples.rb b/spec/support/shared_examples/models/concerns/timebox_shared_examples.rb
index 15ca1f56bd0..d199bae4170 100644
--- a/spec/support/shared_examples/models/concerns/timebox_shared_examples.rb
+++ b/spec/support/shared_examples/models/concerns/timebox_shared_examples.rb
@@ -102,7 +102,7 @@ RSpec.shared_examples 'a timebox' do |timebox_type|
let(:timebox) { create(timebox_type, *timebox_args, group: group) }
before do
- project.update(group: group)
+ project.update!(group: group)
end
it "does not accept the same title in a group twice" do
diff --git a/spec/support/shared_examples/models/diff_note_after_commit_shared_examples.rb b/spec/support/shared_examples/models/diff_note_after_commit_shared_examples.rb
index 8c3e073193c..64390ccdc25 100644
--- a/spec/support/shared_examples/models/diff_note_after_commit_shared_examples.rb
+++ b/spec/support/shared_examples/models/diff_note_after_commit_shared_examples.rb
@@ -10,7 +10,7 @@ RSpec.shared_examples 'a valid diff note with after commit callback' do
it 'raises an error' do
allow(diff_file_from_repository).to receive(:line_for_position).with(position).and_return(nil)
- expect { subject.save }.to raise_error(::DiffNote::NoteDiffFileCreationError,
+ expect { subject.save! }.to raise_error(::DiffNote::NoteDiffFileCreationError,
"Failed to find diff line for: #{diff_file_from_repository.file_path}, "\
"old_line: #{position.old_line}"\
", new_line: #{position.new_line}")
@@ -25,11 +25,11 @@ RSpec.shared_examples 'a valid diff note with after commit callback' do
it 'fallback to fetch file from repository' do
expect_any_instance_of(::Gitlab::Diff::Position).to receive(:diff_file).with(project.repository)
- subject.save
+ subject.save!
end
it 'creates a diff note file' do
- subject.save
+ subject.save!
expect(subject.reload.note_diff_file).to be_present
end
@@ -40,7 +40,7 @@ RSpec.shared_examples 'a valid diff note with after commit callback' do
it 'raises an error' do
allow_any_instance_of(::Gitlab::Diff::Position).to receive(:diff_file).with(project.repository).and_return(nil)
- expect { subject.save }.to raise_error(::DiffNote::NoteDiffFileCreationError, 'Failed to find diff file')
+ expect { subject.save! }.to raise_error(::DiffNote::NoteDiffFileCreationError, 'Failed to find diff file')
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 b0cdc77a378..759b22f794e 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
@@ -18,8 +18,6 @@ RSpec.shared_examples 'a valid diff positionable note' do |factory_on_commit|
)
end
- subject { build(factory_on_commit, commit_id: commit_id, position: position) }
-
context 'position diff refs matches commit diff refs' do
it 'is valid' do
expect(subject).to be_valid
diff --git a/spec/support/shared_examples/models/member_shared_examples.rb b/spec/support/shared_examples/models/member_shared_examples.rb
index 9bf157212d3..7ede6f0d8d4 100644
--- a/spec/support/shared_examples/models/member_shared_examples.rb
+++ b/spec/support/shared_examples/models/member_shared_examples.rb
@@ -25,21 +25,21 @@ RSpec.shared_examples 'inherited access level as a member of entity' do
it 'is allowed to change to be a developer of the entity' do
entity.add_maintainer(user)
- expect { member.update(access_level: Gitlab::Access::DEVELOPER) }
+ expect { member.update!(access_level: Gitlab::Access::DEVELOPER) }
.to change { member.access_level }.to(Gitlab::Access::DEVELOPER)
end
it 'is not allowed to change to be a guest of the entity' do
entity.add_maintainer(user)
- expect { member.update(access_level: Gitlab::Access::GUEST) }
+ expect { member.update(access_level: Gitlab::Access::GUEST) } # rubocop:disable Rails/SaveBang
.not_to change { member.reload.access_level }
end
it "shows an error if the member can't be updated" do
entity.add_maintainer(user)
- member.update(access_level: Gitlab::Access::REPORTER)
+ expect { member.update!(access_level: Gitlab::Access::REPORTER) }.to raise_error(ActiveRecord::RecordInvalid)
expect(member.errors.full_messages).to eq(["Access level should be greater than or equal to Developer inherited membership from group #{parent_entity.name}"])
end
@@ -51,7 +51,7 @@ RSpec.shared_examples 'inherited access level as a member of entity' do
non_member = entity.is_a?(Group) ? entity.group_member(non_member_user) : entity.project_member(non_member_user)
- expect { non_member.update(access_level: Gitlab::Access::GUEST) }
+ expect { non_member.update!(access_level: Gitlab::Access::GUEST) }
.to change { non_member.reload.access_level }
end
end
@@ -60,7 +60,7 @@ end
RSpec.shared_examples '#valid_level_roles' do |entity_name|
let(:member_user) { create(:user) }
let(:group) { create(:group) }
- let(:entity) { create(entity_name) }
+ let(:entity) { create(entity_name) } # rubocop:disable Rails/SaveBang
let(:entity_member) { create("#{entity_name}_member", :developer, source: entity, user: member_user) }
let(:presenter) { described_class.new(entity_member, current_user: member_user) }
let(:expected_roles) { { 'Developer' => 30, 'Maintainer' => 40, 'Reporter' => 20 } }
diff --git a/spec/support/shared_examples/models/members_notifications_shared_example.rb b/spec/support/shared_examples/models/members_notifications_shared_example.rb
index 050d710f1de..04af3935d15 100644
--- a/spec/support/shared_examples/models/members_notifications_shared_example.rb
+++ b/spec/support/shared_examples/models/members_notifications_shared_example.rb
@@ -13,7 +13,7 @@ RSpec.shared_examples 'members notifications' do |entity_type|
it "sends email to user" do
expect(notification_service).to receive(:"new_#{entity_type}_member").with(member)
- member.save
+ member.save!
end
end
diff --git a/spec/support/shared_examples/models/mentionable_shared_examples.rb b/spec/support/shared_examples/models/mentionable_shared_examples.rb
index dda5fa37b26..94c52bdaaa6 100644
--- a/spec/support/shared_examples/models/mentionable_shared_examples.rb
+++ b/spec/support/shared_examples/models/mentionable_shared_examples.rb
@@ -162,7 +162,7 @@ RSpec.shared_examples 'an editable mentionable' do
end
it 'creates new cross-reference notes when the mentionable text is edited' do
- subject.save
+ subject.save!
subject.create_cross_references!
new_text = <<-MSG.strip_heredoc
@@ -270,7 +270,7 @@ RSpec.shared_examples 'mentions in notes' do |mentionable_type|
let!(:mentionable) { note.noteable }
before do
- note.update(note: note_desc)
+ note.update!(note: note_desc)
note.store_mentions!
add_member(user)
end
@@ -292,7 +292,7 @@ RSpec.shared_examples 'load mentions from DB' do |mentionable_type|
let_it_be(:note_desc) { "#{mentioned_user.to_reference} and #{group.to_reference(full: true)} and @all" }
before do
- note.update(note: note_desc)
+ note.update!(note: note_desc)
note.store_mentions!
add_member(user)
end
@@ -305,7 +305,7 @@ RSpec.shared_examples 'load mentions from DB' do |mentionable_type|
mentioned_projects_ids: user_mention.mentioned_projects_ids.to_a << non_existing_record_id,
mentioned_groups_ids: user_mention.mentioned_groups_ids.to_a << non_existing_record_id
}
- user_mention.update(mention_ids)
+ user_mention.update!(mention_ids)
end
it 'filters out inexistent mentions' do
@@ -328,7 +328,7 @@ RSpec.shared_examples 'load mentions from DB' do |mentionable_type|
mentioned_projects_ids: user_mention.mentioned_projects_ids.to_a << private_project.id,
mentioned_groups_ids: user_mention.mentioned_groups_ids.to_a << private_group.id
}
- user_mention.update(mention_ids)
+ user_mention.update!(mention_ids)
add_member(mega_user)
private_project.add_developer(mega_user)
diff --git a/spec/support/shared_examples/models/project_latest_successful_build_for_shared_examples.rb b/spec/support/shared_examples/models/project_latest_successful_build_for_shared_examples.rb
index 7bbc0c5a364..7701ab42007 100644
--- a/spec/support/shared_examples/models/project_latest_successful_build_for_shared_examples.rb
+++ b/spec/support/shared_examples/models/project_latest_successful_build_for_shared_examples.rb
@@ -53,7 +53,7 @@ RSpec.shared_examples 'latest successful build for sha or ref' do
let(:build_name) { pending_build.name }
before do
- pipeline.update(status: 'pending')
+ pipeline.update!(status: 'pending')
end
it 'returns empty relation' do
diff --git a/spec/support/shared_examples/models/relative_positioning_shared_examples.rb b/spec/support/shared_examples/models/relative_positioning_shared_examples.rb
index e4668926d74..d1437244082 100644
--- a/spec/support/shared_examples/models/relative_positioning_shared_examples.rb
+++ b/spec/support/shared_examples/models/relative_positioning_shared_examples.rb
@@ -1,9 +1,25 @@
# frozen_string_literal: true
+# Notes for implementing classes:
+#
+# The following let bindings should be defined:
+# - `factory`: A symbol naming a factory to use to create items
+# - `default_params`: A HashMap of factory parameters to pass to the factory.
+#
+# The `default_params` should include the relative parent, so that any item
+# created with these parameters passed to the `factory` will be considered in
+# the same set of items relative to each other.
+#
+# For the purposes of efficiency, it is a good idea to bind the parent in
+# `let_it_be`, so that it is re-used across examples, but be careful that it
+# does not have any other children - it should only be used within this set of
+# shared examples.
RSpec.shared_examples 'a class that supports relative positioning' do
let(:item1) { create_item }
let(:item2) { create_item }
- let(:new_item) { create_item }
+ let(:new_item) { create_item(relative_position: nil) }
+
+ let(:set_size) { RelativePositioning.mover.context(item1).scoped_items.count }
def create_item(params = {})
create(factory, params.merge(default_params))
@@ -17,6 +33,7 @@ RSpec.shared_examples 'a class that supports relative positioning' do
describe '.move_nulls_to_end' do
let(:item3) { create_item }
+ let(:sibling_query) { item1.class.relative_positioning_query_base(item1) }
it 'moves items with null relative_position to the end' do
item1.update!(relative_position: 1000)
@@ -28,10 +45,9 @@ RSpec.shared_examples 'a class that supports relative positioning' do
expect(items.sort_by(&:relative_position)).to eq(items)
expect(item1.relative_position).to be(1000)
- expect(item1.prev_relative_position).to be_nil
- expect(item1.next_relative_position).to eq(item2.relative_position)
- expect(item2.next_relative_position).to eq(item3.relative_position)
- expect(item3.next_relative_position).to be_nil
+
+ expect(sibling_query.where(relative_position: nil)).not_to exist
+ expect(sibling_query.reorder(:relative_position, :id)).to eq([item1, item2, item3])
end
it 'preserves relative position' do
@@ -70,6 +86,37 @@ RSpec.shared_examples 'a class that supports relative positioning' do
expect(items.sort_by(&:relative_position)).to eq(items)
end
+ it 'manages to move nulls to the end even if there is not enough space' do
+ run = run_at_end(20).to_a
+ bunch_a = create_items_with_positions(run[0..18])
+ bunch_b = create_items_with_positions([run.last])
+
+ nils = create_items_with_positions([nil] * 4)
+ described_class.move_nulls_to_end(nils)
+
+ items = [*bunch_a, *bunch_b, *nils]
+ items.each(&:reset)
+
+ expect(items.map(&:relative_position)).to all(be_valid_position)
+ expect(items.reverse.sort_by(&:relative_position)).to eq(items)
+ end
+
+ it 'manages to move nulls to the end, stacking if we cannot create enough space' do
+ run = run_at_end(40).to_a
+ bunch = create_items_with_positions(run.select(&:even?))
+
+ nils = create_items_with_positions([nil] * 20)
+ described_class.move_nulls_to_end(nils)
+
+ items = [*bunch, *nils]
+ items.each(&:reset)
+
+ expect(items.map(&:relative_position)).to all(be_valid_position)
+ expect(bunch.reverse.sort_by(&:relative_position)).to eq(bunch)
+ expect(nils.reverse.sort_by(&:relative_position)).not_to eq(nils)
+ expect(bunch.map(&:relative_position)).to all(be < nils.map(&:relative_position).min)
+ end
+
it 'does not have an N+1 issue' do
create_items_with_positions(10..12)
@@ -89,6 +136,7 @@ RSpec.shared_examples 'a class that supports relative positioning' do
describe '.move_nulls_to_start' do
let(:item3) { create_item }
+ let(:sibling_query) { item1.class.relative_positioning_query_base(item1) }
it 'moves items with null relative_position to the start' do
item1.update!(relative_position: nil)
@@ -100,10 +148,8 @@ RSpec.shared_examples 'a class that supports relative positioning' do
items.map(&:reload)
expect(items.sort_by(&:relative_position)).to eq(items)
- expect(item1.prev_relative_position).to eq nil
- expect(item1.next_relative_position).to eq item2.relative_position
- expect(item2.next_relative_position).to eq item3.relative_position
- expect(item3.next_relative_position).to eq nil
+ expect(sibling_query.where(relative_position: nil)).not_to exist
+ expect(sibling_query.reorder(:relative_position, :id)).to eq(items)
expect(item3.relative_position).to be(1000)
end
@@ -130,193 +176,36 @@ RSpec.shared_examples 'a class that supports relative positioning' do
expect(described_class.move_nulls_to_start([item1])).to be(0)
expect(item1.reload.relative_position).to be(1)
end
- end
-
- describe '#max_relative_position' do
- it 'returns maximum position' do
- expect(item1.max_relative_position).to eq item2.relative_position
- end
- end
-
- describe '#prev_relative_position' do
- it 'returns previous position if there is an item above' do
- item1.update!(relative_position: 5)
- item2.update!(relative_position: 15)
-
- expect(item2.prev_relative_position).to eq item1.relative_position
- end
-
- it 'returns nil if there is no item above' do
- expect(item1.prev_relative_position).to eq nil
- end
- end
-
- describe '#next_relative_position' do
- it 'returns next position if there is an item below' do
- item1.update!(relative_position: 5)
- item2.update!(relative_position: 15)
-
- expect(item1.next_relative_position).to eq item2.relative_position
- end
-
- it 'returns nil if there is no item below' do
- expect(item2.next_relative_position).to eq nil
- end
- end
-
- describe '#find_next_gap_before' do
- context 'there is no gap' do
- let(:items) { create_items_with_positions(run_at_start) }
-
- it 'returns nil' do
- items.each do |item|
- expect(item.send(:find_next_gap_before)).to be_nil
- end
- end
- end
-
- context 'there is a sequence ending at MAX_POSITION' do
- let(:items) { create_items_with_positions(run_at_end) }
-
- let(:gaps) do
- items.map { |item| item.send(:find_next_gap_before) }
- end
-
- it 'can find the gap at the start for any item in the sequence' do
- gap = { start: items.first.relative_position, end: RelativePositioning::MIN_POSITION }
-
- expect(gaps).to all(eq(gap))
- end
-
- it 'respects lower bounds' do
- gap = { start: items.first.relative_position, end: 10 }
- new_item.update!(relative_position: 10)
-
- expect(gaps).to all(eq(gap))
- end
- end
-
- specify do
- item1.update!(relative_position: 5)
-
- (0..10).each do |pos|
- item2.update!(relative_position: pos)
-
- gap = item2.send(:find_next_gap_before)
-
- expect(gap[:start]).to be <= item2.relative_position
- expect((gap[:end] - gap[:start]).abs).to be >= RelativePositioning::MIN_GAP
- expect(gap[:start]).to be_valid_position
- expect(gap[:end]).to be_valid_position
- end
- end
-
- it 'deals with there not being any items to the left' do
- create_items_with_positions([1, 2, 3])
- new_item.update!(relative_position: 0)
-
- expect(new_item.send(:find_next_gap_before)).to eq(start: 0, end: RelativePositioning::MIN_POSITION)
- end
-
- it 'finds the next gap to the left, skipping adjacent values' do
- create_items_with_positions([1, 9, 10])
- new_item.update!(relative_position: 11)
-
- expect(new_item.send(:find_next_gap_before)).to eq(start: 9, end: 1)
- end
-
- it 'finds the next gap to the left' do
- create_items_with_positions([2, 10])
-
- new_item.update!(relative_position: 15)
- expect(new_item.send(:find_next_gap_before)).to eq(start: 15, end: 10)
-
- new_item.update!(relative_position: 11)
- expect(new_item.send(:find_next_gap_before)).to eq(start: 10, end: 2)
-
- new_item.update!(relative_position: 9)
- expect(new_item.send(:find_next_gap_before)).to eq(start: 9, end: 2)
-
- new_item.update!(relative_position: 5)
- expect(new_item.send(:find_next_gap_before)).to eq(start: 5, end: 2)
- end
- end
-
- describe '#find_next_gap_after' do
- context 'there is no gap' do
- let(:items) { create_items_with_positions(run_at_end) }
- it 'returns nil' do
- items.each do |item|
- expect(item.send(:find_next_gap_after)).to be_nil
- end
- end
- end
-
- context 'there is a sequence starting at MIN_POSITION' do
- let(:items) { create_items_with_positions(run_at_start) }
-
- let(:gaps) do
- items.map { |item| item.send(:find_next_gap_after) }
- end
-
- it 'can find the gap at the end for any item in the sequence' do
- gap = { start: items.last.relative_position, end: RelativePositioning::MAX_POSITION }
-
- expect(gaps).to all(eq(gap))
- end
+ it 'manages to move nulls to the start even if there is not enough space' do
+ run = run_at_start(20).to_a
+ bunch_a = create_items_with_positions([run.first])
+ bunch_b = create_items_with_positions(run[2..])
- it 'respects upper bounds' do
- gap = { start: items.last.relative_position, end: 10 }
- new_item.update!(relative_position: 10)
+ nils = create_items_with_positions([nil, nil, nil, nil])
+ described_class.move_nulls_to_start(nils)
- expect(gaps).to all(eq(gap))
- end
- end
-
- specify do
- item1.update!(relative_position: 5)
-
- (0..10).each do |pos|
- item2.update!(relative_position: pos)
-
- gap = item2.send(:find_next_gap_after)
-
- expect(gap[:start]).to be >= item2.relative_position
- expect((gap[:end] - gap[:start]).abs).to be >= RelativePositioning::MIN_GAP
- expect(gap[:start]).to be_valid_position
- expect(gap[:end]).to be_valid_position
- end
- end
-
- it 'deals with there not being any items to the right' do
- create_items_with_positions([1, 2, 3])
- new_item.update!(relative_position: 5)
-
- expect(new_item.send(:find_next_gap_after)).to eq(start: 5, end: RelativePositioning::MAX_POSITION)
- end
-
- it 'finds the next gap to the right, skipping adjacent values' do
- create_items_with_positions([1, 2, 10])
- new_item.update!(relative_position: 0)
+ items = [*nils, *bunch_a, *bunch_b]
+ items.each(&:reset)
- expect(new_item.send(:find_next_gap_after)).to eq(start: 2, end: 10)
+ expect(items.map(&:relative_position)).to all(be_valid_position)
+ expect(items.reverse.sort_by(&:relative_position)).to eq(items)
end
- it 'finds the next gap to the right' do
- create_items_with_positions([2, 10])
+ it 'manages to move nulls to the end, stacking if we cannot create enough space' do
+ run = run_at_start(40).to_a
+ bunch = create_items_with_positions(run.select(&:even?))
- new_item.update!(relative_position: 0)
- expect(new_item.send(:find_next_gap_after)).to eq(start: 0, end: 2)
+ nils = create_items_with_positions([nil].cycle.take(20))
+ described_class.move_nulls_to_start(nils)
- new_item.update!(relative_position: 1)
- expect(new_item.send(:find_next_gap_after)).to eq(start: 2, end: 10)
-
- new_item.update!(relative_position: 3)
- expect(new_item.send(:find_next_gap_after)).to eq(start: 3, end: 10)
+ items = [*nils, *bunch]
+ items.each(&:reset)
- new_item.update!(relative_position: 5)
- expect(new_item.send(:find_next_gap_after)).to eq(start: 5, end: 10)
+ expect(items.map(&:relative_position)).to all(be_valid_position)
+ expect(bunch.reverse.sort_by(&:relative_position)).to eq(bunch)
+ expect(nils.reverse.sort_by(&:relative_position)).not_to eq(nils)
+ expect(bunch.map(&:relative_position)).to all(be > nils.map(&:relative_position).max)
end
end
@@ -384,36 +273,39 @@ RSpec.shared_examples 'a class that supports relative positioning' do
end
context 'leap-frogging to the left' do
+ let(:item3) { create(factory, default_params) }
+ let(:start) { RelativePositioning::START_POSITION }
+
before do
- start = RelativePositioning::START_POSITION
item1.update!(relative_position: start - RelativePositioning::IDEAL_DISTANCE * 0)
item2.update!(relative_position: start - RelativePositioning::IDEAL_DISTANCE * 1)
item3.update!(relative_position: start - RelativePositioning::IDEAL_DISTANCE * 2)
end
- let(:item3) { create(factory, default_params) }
+ def leap_frog
+ a, b = [item1.reset, item2.reset].sort_by(&:relative_position)
- def leap_frog(steps)
- a = item1
- b = item2
-
- steps.times do |i|
- a.move_before(b)
- a.save!
- a, b = b, a
- end
+ b.move_before(a)
+ b.save!
end
- it 'can leap-frog STEPS - 1 times before needing to rebalance' do
- # This is less efficient than going right, due to the flooring of
- # integer division
- expect { leap_frog(RelativePositioning::STEPS - 1) }
- .not_to change { item3.reload.relative_position }
+ it 'can leap-frog STEPS times before needing to rebalance' do
+ expect { RelativePositioning::STEPS.times { leap_frog } }
+ .to change { item3.reload.relative_position }.by(0)
+ .and change { item1.reload.relative_position }.by(be < 0)
+ .and change { item2.reload.relative_position }.by(be < 0)
+
+ expect { leap_frog }
+ .to change { item3.reload.relative_position }.by(be < 0)
end
- it 'rebalances after leap-frogging STEPS times' do
- expect { leap_frog(RelativePositioning::STEPS) }
- .to change { item3.reload.relative_position }
+ context 'there is no space to the left after moving STEPS times' do
+ let(:start) { RelativePositioning::MIN_POSITION + (2 * RelativePositioning::IDEAL_DISTANCE) }
+
+ it 'rebalances to the right' do
+ expect { RelativePositioning::STEPS.succ.times { leap_frog } }
+ .not_to change { item3.reload.relative_position }
+ end
end
end
end
@@ -476,25 +368,25 @@ RSpec.shared_examples 'a class that supports relative positioning' do
let(:item3) { create(factory, default_params) }
- def leap_frog(steps)
- a = item1
- b = item2
+ def leap_frog
+ a, b = [item1.reset, item2.reset].sort_by(&:relative_position)
- steps.times do |i|
- a.move_after(b)
- a.save!
- a, b = b, a
- end
+ a.move_after(b)
+ a.save!
end
- it 'can leap-frog STEPS times before needing to rebalance' do
- expect { leap_frog(RelativePositioning::STEPS) }
- .not_to change { item3.reload.relative_position }
- end
+ it 'rebalances after STEPS jumps' do
+ RelativePositioning::STEPS.pred.times do
+ expect { leap_frog }
+ .to change { item3.reload.relative_position }.by(0)
+ .and change { item1.reset.relative_position }.by(be >= 0)
+ .and change { item2.reset.relative_position }.by(be >= 0)
+ end
- it 'rebalances after leap-frogging STEPS+1 times' do
- expect { leap_frog(RelativePositioning::STEPS + 1) }
- .to change { item3.reload.relative_position }
+ expect { leap_frog }
+ .to change { item3.reload.relative_position }.by(0)
+ .and change { item1.reset.relative_position }.by(be < 0)
+ .and change { item2.reset.relative_position }.by(be < 0)
end
end
end
@@ -506,12 +398,26 @@ RSpec.shared_examples 'a class that supports relative positioning' do
end
end
+ it 'places items at most IDEAL_DISTANCE from the start when the range is open' do
+ n = set_size
+
+ expect([item1, item2].map(&:relative_position)).to all(be >= (RelativePositioning::START_POSITION - (n * RelativePositioning::IDEAL_DISTANCE)))
+ end
+
it 'moves item to the end' do
new_item.move_to_start
expect(new_item.relative_position).to be < item2.relative_position
end
+ it 'positions the item at MIN_POSITION when there is only one space left' do
+ item2.update!(relative_position: RelativePositioning::MIN_POSITION + 1)
+
+ new_item.move_to_start
+
+ expect(new_item.relative_position).to eq RelativePositioning::MIN_POSITION
+ end
+
it 'rebalances when there is already an item at the MIN_POSITION' do
item2.update!(relative_position: RelativePositioning::MIN_POSITION)
@@ -543,12 +449,26 @@ RSpec.shared_examples 'a class that supports relative positioning' do
end
end
+ it 'places items at most IDEAL_DISTANCE from the start when the range is open' do
+ n = set_size
+
+ expect([item1, item2].map(&:relative_position)).to all(be <= (RelativePositioning::START_POSITION + (n * RelativePositioning::IDEAL_DISTANCE)))
+ end
+
it 'moves item to the end' do
new_item.move_to_end
expect(new_item.relative_position).to be > item2.relative_position
end
+ it 'positions the item at MAX_POSITION when there is only one space left' do
+ item2.update!(relative_position: RelativePositioning::MAX_POSITION - 1)
+
+ new_item.move_to_end
+
+ expect(new_item.relative_position).to eq RelativePositioning::MAX_POSITION
+ end
+
it 'rebalances when there is already an item at the MAX_POSITION' do
item2.update!(relative_position: RelativePositioning::MAX_POSITION)
@@ -712,63 +632,6 @@ RSpec.shared_examples 'a class that supports relative positioning' do
end
end
- describe '#move_sequence_before' do
- it 'moves the whole sequence of items to the middle of the nearest gap' do
- items = create_items_with_positions([90, 100, 101, 102])
-
- items.last.move_sequence_before
- items.last.save!
-
- positions = items.map { |item| item.reload.relative_position }
- expect(positions).to eq([90, 95, 96, 102])
- end
-
- it 'raises an error if there is no space' do
- items = create_items_with_positions(run_at_start)
-
- expect { items.last.move_sequence_before }.to raise_error(RelativePositioning::NoSpaceLeft)
- end
-
- it 'finds a gap if there are unused positions' do
- items = create_items_with_positions([100, 101, 102])
-
- items.last.move_sequence_before
- items.last.save!
-
- positions = items.map { |item| item.reload.relative_position }
-
- expect(positions.last - positions.second).to be > RelativePositioning::MIN_GAP
- end
- end
-
- describe '#move_sequence_after' do
- it 'moves the whole sequence of items to the middle of the nearest gap' do
- items = create_items_with_positions([100, 101, 102, 110])
-
- items.first.move_sequence_after
- items.first.save!
-
- positions = items.map { |item| item.reload.relative_position }
- expect(positions).to eq([100, 105, 106, 110])
- end
-
- it 'finds a gap if there are unused positions' do
- items = create_items_with_positions([100, 101, 102])
-
- items.first.move_sequence_after
- items.first.save!
-
- positions = items.map { |item| item.reload.relative_position }
- expect(positions.second - positions.first).to be > RelativePositioning::MIN_GAP
- end
-
- it 'raises an error if there is no space' do
- items = create_items_with_positions(run_at_end)
-
- expect { items.first.move_sequence_after }.to raise_error(RelativePositioning::NoSpaceLeft)
- end
- end
-
def be_valid_position
be_between(RelativePositioning::MIN_POSITION, RelativePositioning::MAX_POSITION)
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 a5228c43f6f..a1867e1ce39 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
@@ -164,7 +164,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do |service_name|
context "event channels" do
it "uses the right channel for push event" do
- chat_service.update(push_channel: "random")
+ chat_service.update!(push_channel: "random")
expect(Slack::Messenger).to execute_with_options(channel: ['random'])
@@ -172,7 +172,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do |service_name|
end
it "uses the right channel for merge request event" do
- chat_service.update(merge_request_channel: "random")
+ chat_service.update!(merge_request_channel: "random")
expect(Slack::Messenger).to execute_with_options(channel: ['random'])
@@ -180,7 +180,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do |service_name|
end
it "uses the right channel for issue event" do
- chat_service.update(issue_channel: "random")
+ chat_service.update!(issue_channel: "random")
expect(Slack::Messenger).to execute_with_options(channel: ['random'])
@@ -191,7 +191,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do |service_name|
let(:issue_service_options) { { title: 'Secret', confidential: true } }
it "uses confidential issue channel" do
- chat_service.update(confidential_issue_channel: 'confidential')
+ chat_service.update!(confidential_issue_channel: 'confidential')
expect(Slack::Messenger).to execute_with_options(channel: ['confidential'])
@@ -199,7 +199,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do |service_name|
end
it 'falls back to issue channel' do
- chat_service.update(issue_channel: 'fallback_channel')
+ chat_service.update!(issue_channel: 'fallback_channel')
expect(Slack::Messenger).to execute_with_options(channel: ['fallback_channel'])
@@ -208,7 +208,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do |service_name|
end
it "uses the right channel for wiki event" do
- chat_service.update(wiki_page_channel: "random")
+ chat_service.update!(wiki_page_channel: "random")
expect(Slack::Messenger).to execute_with_options(channel: ['random'])
@@ -221,7 +221,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do |service_name|
end
it "uses the right channel" do
- chat_service.update(note_channel: "random")
+ chat_service.update!(note_channel: "random")
note_data = Gitlab::DataBuilder::Note.build(issue_note, user)
@@ -236,7 +236,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do |service_name|
end
it "uses confidential channel" do
- chat_service.update(confidential_note_channel: "confidential")
+ chat_service.update!(confidential_note_channel: "confidential")
note_data = Gitlab::DataBuilder::Note.build(issue_note, user)
@@ -246,7 +246,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do |service_name|
end
it 'falls back to note channel' do
- chat_service.update(note_channel: "fallback_channel")
+ chat_service.update!(note_channel: "fallback_channel")
note_data = Gitlab::DataBuilder::Note.build(issue_note, user)
diff --git a/spec/support/shared_examples/models/throttled_touch_shared_examples.rb b/spec/support/shared_examples/models/throttled_touch_shared_examples.rb
index fc4f6053bb9..14b851d2828 100644
--- a/spec/support/shared_examples/models/throttled_touch_shared_examples.rb
+++ b/spec/support/shared_examples/models/throttled_touch_shared_examples.rb
@@ -3,7 +3,7 @@
RSpec.shared_examples 'throttled touch' do
describe '#touch' do
it 'updates the updated_at timestamp' do
- Timecop.freeze do
+ freeze_time do
subject.touch
expect(subject.updated_at).to be_like_time(Time.zone.now)
end
diff --git a/spec/support/shared_examples/models/update_project_statistics_shared_examples.rb b/spec/support/shared_examples/models/update_project_statistics_shared_examples.rb
index 7f0da19996e..557025569b8 100644
--- a/spec/support/shared_examples/models/update_project_statistics_shared_examples.rb
+++ b/spec/support/shared_examples/models/update_project_statistics_shared_examples.rb
@@ -105,7 +105,7 @@ RSpec.shared_examples 'UpdateProjectStatistics' do
expect(ProjectStatistics)
.not_to receive(:increment_statistic)
- project.update(pending_delete: true)
+ project.update!(pending_delete: true)
project.destroy!
end
@@ -113,7 +113,7 @@ RSpec.shared_examples 'UpdateProjectStatistics' do
expect(Namespaces::ScheduleAggregationWorker)
.not_to receive(:perform_async)
- project.update(pending_delete: true)
+ project.update!(pending_delete: true)
project.destroy!
end
end
diff --git a/spec/support/shared_examples/models/wiki_shared_examples.rb b/spec/support/shared_examples/models/wiki_shared_examples.rb
index a881d5f036c..b87f7fe97e1 100644
--- a/spec/support/shared_examples/models/wiki_shared_examples.rb
+++ b/spec/support/shared_examples/models/wiki_shared_examples.rb
@@ -322,8 +322,8 @@ RSpec.shared_examples 'wiki model' do
expect(commit.committer_email).to eq(user.commit_email)
end
- it 'updates container activity' do
- expect(subject).to receive(:update_container_activity)
+ it 'runs after_wiki_activity callbacks' do
+ expect(subject).to receive(:after_wiki_activity)
subject.create_page('Test Page', 'This is content')
end
@@ -363,10 +363,10 @@ RSpec.shared_examples 'wiki model' do
expect(commit.committer_email).to eq(user.commit_email)
end
- it 'updates container activity' do
+ it 'runs after_wiki_activity callbacks' do
page
- expect(subject).to receive(:update_container_activity)
+ expect(subject).to receive(:after_wiki_activity)
update_page
end
@@ -389,10 +389,10 @@ RSpec.shared_examples 'wiki model' do
expect(commit.committer_email).to eq(user.commit_email)
end
- it 'updates container activity' do
+ it 'runs after_wiki_activity callbacks' do
page
- expect(subject).to receive(:update_container_activity)
+ expect(subject).to receive(:after_wiki_activity)
subject.delete_page(page)
end
diff --git a/spec/support/shared_examples/models/with_uploads_shared_examples.rb b/spec/support/shared_examples/models/with_uploads_shared_examples.rb
index f2a4d9919b7..0c930ec1fce 100644
--- a/spec/support/shared_examples/models/with_uploads_shared_examples.rb
+++ b/spec/support/shared_examples/models/with_uploads_shared_examples.rb
@@ -12,7 +12,7 @@ RSpec.shared_examples 'model with uploads' do |supports_fileuploads|
it 'deletes remote uploads' do
expect_any_instance_of(CarrierWave::Storage::Fog::File).to receive(:delete).and_call_original
- expect { model_object.destroy }.to change { Upload.count }.by(-1)
+ expect { model_object.destroy! }.to change { Upload.count }.by(-1)
end
end
@@ -21,13 +21,13 @@ RSpec.shared_examples 'model with uploads' do |supports_fileuploads|
let!(:uploads) { create_list(:upload, 2, uploader: FileUploader, model: model_object) }
it 'deletes any FileUploader uploads which are not mounted' do
- expect { model_object.destroy }.to change { Upload.count }.by(-3)
+ expect { model_object.destroy! }.to change { Upload.count }.by(-3)
end
it 'deletes local files' do
expect_any_instance_of(Uploads::Local).to receive(:delete_keys).with(uploads.map(&:absolute_path))
- model_object.destroy
+ model_object.destroy!
end
end
@@ -35,14 +35,14 @@ RSpec.shared_examples 'model with uploads' do |supports_fileuploads|
let!(:uploads) { create_list(:upload, 2, :object_storage, uploader: FileUploader, model: model_object) }
it 'deletes any FileUploader uploads which are not mounted' do
- expect { model_object.destroy }.to change { Upload.count }.by(-3)
+ expect { model_object.destroy! }.to change { Upload.count }.by(-3)
end
it 'deletes remote files' do
expected_array = array_including(*uploads.map(&:path))
expect_any_instance_of(Uploads::Fog).to receive(:delete_keys).with(expected_array)
- model_object.destroy
+ model_object.destroy!
end
end
end
diff --git a/spec/support/shared_examples/path_extraction_shared_examples.rb b/spec/support/shared_examples/path_extraction_shared_examples.rb
index ff55bc9a490..39c7c1f2a94 100644
--- a/spec/support/shared_examples/path_extraction_shared_examples.rb
+++ b/spec/support/shared_examples/path_extraction_shared_examples.rb
@@ -146,20 +146,6 @@ RSpec.shared_examples 'extracts refs' do
expect(extract_ref('release/app/doc/README.md')).to eq(['release/app', 'doc/README.md'])
end
-
- context 'when the extracts_path_optimization feature flag is disabled' do
- before do
- stub_feature_flags(extracts_path_optimization: false)
- end
-
- it 'always fetches all ref names' do
- expect(self).to receive(:ref_names).and_call_original
- expect(container.repository).not_to receive(:branch_names_include?)
- expect(container.repository).not_to receive(:tag_names_include?)
-
- expect(extract_ref('v1.0.0/doc/README.md')).to eq(['v1.0.0', 'doc/README.md'])
- end
- end
end
context 'when the repository has ambiguous refs' do
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 d8476f5dcc2..d05e5eb9120 100644
--- a/spec/support/shared_examples/policies/project_policy_shared_examples.rb
+++ b/spec/support/shared_examples/policies/project_policy_shared_examples.rb
@@ -59,8 +59,7 @@ RSpec.shared_examples 'project policies as anonymous' do
let(:project) { create(:project, :public, namespace: group) }
let(:user_permissions) { [:create_merge_request_in, :create_project, :create_issue, :create_note, :upload_file, :award_emoji] }
let(:anonymous_permissions) { guest_permissions - user_permissions }
-
- subject { described_class.new(nil, project) }
+ let(:current_user) { anonymous }
before do
create(:group_member, :invited, group: group)
@@ -78,9 +77,8 @@ RSpec.shared_examples 'project policies as anonymous' do
end
context 'abilities for non-public projects' do
- let(:project) { create(:project, namespace: owner.namespace) }
-
- subject { described_class.new(nil, project) }
+ let(:project) { private_project }
+ let(:current_user) { anonymous }
it { is_expected.to be_banned }
end
@@ -109,10 +107,10 @@ RSpec.shared_examples 'deploy token does not get confused with user' do
end
RSpec.shared_examples 'project policies as guest' do
- subject { described_class.new(guest, project) }
-
context 'abilities for non-public projects' do
- let(:project) { create(:project, namespace: owner.namespace) }
+ let(:project) { private_project }
+ let(:current_user) { guest }
+
let(:reporter_public_build_permissions) do
reporter_permissions - [:read_build, :read_pipeline]
end
@@ -167,9 +165,8 @@ end
RSpec.shared_examples 'project policies as reporter' do
context 'abilities for non-public projects' do
- let(:project) { create(:project, namespace: owner.namespace) }
-
- subject { described_class.new(reporter, project) }
+ let(:project) { private_project }
+ let(:current_user) { reporter }
it do
expect_allowed(*guest_permissions)
@@ -192,9 +189,8 @@ end
RSpec.shared_examples 'project policies as developer' do
context 'abilities for non-public projects' do
- let(:project) { create(:project, namespace: owner.namespace) }
-
- subject { described_class.new(developer, project) }
+ let(:project) { private_project }
+ let(:current_user) { developer }
it do
expect_allowed(*guest_permissions)
@@ -217,9 +213,8 @@ end
RSpec.shared_examples 'project policies as maintainer' do
context 'abilities for non-public projects' do
- let(:project) { create(:project, namespace: owner.namespace) }
-
- subject { described_class.new(maintainer, project) }
+ let(:project) { private_project }
+ let(:current_user) { maintainer }
it do
expect_allowed(*guest_permissions)
@@ -242,9 +237,8 @@ end
RSpec.shared_examples 'project policies as owner' do
context 'abilities for non-public projects' do
- let(:project) { create(:project, namespace: owner.namespace) }
-
- subject { described_class.new(owner, project) }
+ let(:project) { private_project }
+ let(:current_user) { owner }
it do
expect_allowed(*guest_permissions)
@@ -267,9 +261,8 @@ end
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) }
+ let(:project) { private_project }
+ let(:current_user) { admin }
it do
expect_allowed(*guest_permissions)
@@ -316,9 +309,8 @@ 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) }
+ let(:project) { private_project }
+ let(:current_user) { admin }
it { is_expected.to be_banned }
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 9bfd1e6faa0..e94d29febfb 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
@@ -1,12 +1,13 @@
# frozen_string_literal: true
-# Shared examples to that test code that creates AwardEmoji also mark Todos
-# as done.
+# Shared examples to test that the code that creates AwardEmoji also marks
+# ToDos as done.
#
# The examples expect these to be defined in the calling spec:
# - `subject` the callable code that executes the creation of an AwardEmoji
# - `user`
# - `project`
+#
RSpec.shared_examples 'creating award emojis marks Todos as done' do
using RSpec::Parameterized::TableSyntax
@@ -22,7 +23,7 @@ RSpec.shared_examples 'creating award emojis marks Todos as done' do
with_them do
let(:project) { awardable.project }
- let(:awardable) { create(type) }
+ let(:awardable) { create(type) } # rubocop:disable Rails/SaveBang
let!(:todo) { create(:todo, target: awardable, project: project, user: user) }
specify do
diff --git a/spec/support/shared_examples/requests/api/boards_shared_examples.rb b/spec/support/shared_examples/requests/api/boards_shared_examples.rb
index 20b0f4f0dd2..0096aab55e3 100644
--- a/spec/support/shared_examples/requests/api/boards_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/boards_shared_examples.rb
@@ -169,7 +169,7 @@ RSpec.shared_examples 'group and project boards' do |route_definition, ee = fals
before do
if board_parent.try(:namespace)
- board_parent.update(namespace: owner.namespace)
+ board_parent.update!(namespace: owner.namespace)
else
board.resource_parent.add_owner(owner)
end
diff --git a/spec/support/shared_examples/requests/api/composer_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/composer_packages_shared_examples.rb
index 09743c20fba..5c122b4b5d6 100644
--- a/spec/support/shared_examples/requests/api/composer_packages_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/composer_packages_shared_examples.rb
@@ -16,8 +16,11 @@ RSpec.shared_examples 'Composer package index' do |user_type, status, add_member
subject
expect(response).to have_gitlab_http_status(status)
- expect(response).to match_response_schema('public_api/v4/packages/composer/index')
- expect(json_response).to eq presenter.root
+
+ if status == :success
+ expect(response).to match_response_schema('public_api/v4/packages/composer/index')
+ expect(json_response).to eq presenter.root
+ end
end
end
end
@@ -87,13 +90,22 @@ RSpec.shared_examples 'process Composer api request' do |user_type, status, add_
end
end
-RSpec.shared_context 'Composer auth headers' do |user_role, user_token|
+RSpec.shared_context 'Composer auth headers' do |user_role, user_token, auth_method = :token|
let(:token) { user_token ? personal_access_token.token : 'wrong' }
- let(:headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) }
+
+ let(:headers) do
+ if user_role == :anonymous
+ {}
+ elsif auth_method == :token
+ { 'Private-Token' => token }
+ else
+ basic_auth_header(user.username, token)
+ end
+ end
end
-RSpec.shared_context 'Composer api project access' do |project_visibility_level, user_role, user_token|
- include_context 'Composer auth headers', user_role, user_token do
+RSpec.shared_context 'Composer api project access' do |project_visibility_level, user_role, user_token, auth_method|
+ include_context 'Composer auth headers', user_role, user_token, auth_method do
before do
project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility_level, false))
end
diff --git a/spec/support/shared_examples/requests/api/conan_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/conan_packages_shared_examples.rb
new file mode 100644
index 00000000000..c56290a0aa9
--- /dev/null
+++ b/spec/support/shared_examples/requests/api/conan_packages_shared_examples.rb
@@ -0,0 +1,843 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'conan ping endpoint' do
+ it 'responds with 401 Unauthorized when no token provided' do
+ get api(url)
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+
+ it 'responds with 200 OK when valid token is provided' do
+ jwt = build_jwt(personal_access_token)
+ get api(url), headers: build_token_auth_header(jwt.encoded)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.headers['X-Conan-Server-Capabilities']).to eq("")
+ end
+
+ it 'responds with 200 OK when valid job token is provided' do
+ jwt = build_jwt_from_job(job)
+ get api(url), headers: build_token_auth_header(jwt.encoded)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.headers['X-Conan-Server-Capabilities']).to eq("")
+ end
+
+ it 'responds with 200 OK when valid deploy token is provided' do
+ jwt = build_jwt_from_deploy_token(deploy_token)
+ get api(url), headers: build_token_auth_header(jwt.encoded)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.headers['X-Conan-Server-Capabilities']).to eq("")
+ end
+
+ it 'responds with 401 Unauthorized when invalid access token ID is provided' do
+ jwt = build_jwt(double(id: 12345), user_id: personal_access_token.user_id)
+ get api(url), headers: build_token_auth_header(jwt.encoded)
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+
+ it 'responds with 401 Unauthorized when invalid user is provided' do
+ jwt = build_jwt(personal_access_token, user_id: 12345)
+ get api(url), headers: build_token_auth_header(jwt.encoded)
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+
+ it 'responds with 401 Unauthorized when the provided JWT is signed with different secret' do
+ jwt = build_jwt(personal_access_token, secret: SecureRandom.base64(32))
+ get api(url), headers: build_token_auth_header(jwt.encoded)
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+
+ it 'responds with 401 Unauthorized when invalid JWT is provided' do
+ get api(url), headers: build_token_auth_header('invalid-jwt')
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+
+ context 'packages feature disabled' do
+ it 'responds with 404 Not Found' do
+ stub_packages_setting(enabled: false)
+ get api(url)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+end
+
+RSpec.shared_examples 'conan search endpoint' do
+ before do
+ project.update_column(:visibility_level, Gitlab::VisibilityLevel::PUBLIC)
+
+ get api(url), headers: headers, params: params
+ end
+
+ subject { json_response['results'] }
+
+ context 'returns packages with a matching name' do
+ let(:params) { { q: package.conan_recipe } }
+
+ it { is_expected.to contain_exactly(package.conan_recipe) }
+ end
+
+ context 'returns packages using a * wildcard' do
+ let(:params) { { q: "#{package.name[0, 3]}*" } }
+
+ it { is_expected.to contain_exactly(package.conan_recipe) }
+ end
+
+ context 'does not return non-matching packages' do
+ let(:params) { { q: "foo" } }
+
+ it { is_expected.to be_blank }
+ end
+end
+
+RSpec.shared_examples 'conan authenticate endpoint' do
+ subject { get api(url), headers: headers }
+
+ context 'when using invalid token' do
+ let(:auth_token) { 'invalid_token' }
+
+ it 'responds with 401' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
+
+ context 'when valid JWT access token is provided' do
+ it 'responds with 200' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+
+ it 'token has valid validity time' do
+ freeze_time do
+ subject
+
+ payload = JSONWebToken::HMACToken.decode(
+ response.body, jwt_secret).first
+ expect(payload['access_token']).to eq(personal_access_token.id)
+ expect(payload['user_id']).to eq(personal_access_token.user_id)
+
+ duration = payload['exp'] - payload['iat']
+ expect(duration).to eq(1.hour)
+ end
+ end
+ end
+
+ context 'with valid job token' do
+ let(:auth_token) { job_token }
+
+ it 'responds with 200' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+
+ context 'with valid deploy token' do
+ let(:auth_token) { deploy_token.token }
+
+ it 'responds with 200' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+end
+
+RSpec.shared_examples 'conan check_credentials endpoint' do
+ it 'responds with a 200 OK with PAT' do
+ get api(url), headers: headers
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+
+ context 'with job token' do
+ let(:auth_token) { job_token }
+
+ it 'responds with a 200 OK with job token' do
+ get api(url), headers: headers
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+
+ context 'with deploy token' do
+ let(:auth_token) { deploy_token.token }
+
+ it 'responds with a 200 OK with job token' do
+ get api(url), headers: headers
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+
+ it 'responds with a 401 Unauthorized when an invalid token is used' do
+ get api(url), headers: build_token_auth_header('invalid-token')
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+end
+
+RSpec.shared_examples 'rejects invalid recipe' do
+ context 'with invalid recipe path' do
+ let(:recipe_path) { '../../foo++../..' }
+
+ it 'returns 400' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+ end
+end
+
+RSpec.shared_examples 'rejects invalid file_name' do |invalid_file_name|
+ let(:file_name) { invalid_file_name }
+
+ context 'with invalid file_name' do
+ it 'returns 400' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+ end
+end
+
+RSpec.shared_examples 'rejects recipe for invalid project' do
+ context 'with invalid project' do
+ let(:recipe_path) { 'aa/bb/cc/dd' }
+ let(:project_id) { 9999 }
+
+ it_behaves_like 'not found request'
+ end
+end
+
+RSpec.shared_examples 'empty recipe for not found package' do
+ context 'with invalid recipe url' do
+ let(:recipe_path) do
+ 'aa/bb/%{project}/ccc' % { project: ::Packages::Conan::Metadatum.package_username_from(full_path: project.full_path) }
+ end
+
+ it 'returns not found' do
+ allow(::Packages::Conan::PackagePresenter).to receive(:new)
+ .with(
+ nil,
+ user,
+ project,
+ any_args
+ ).and_return(presenter)
+ allow(presenter).to receive(:recipe_snapshot) { {} }
+ allow(presenter).to receive(:package_snapshot) { {} }
+
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.body).to eq("{}")
+ end
+ end
+end
+
+RSpec.shared_examples 'not selecting a package with the wrong type' do
+ context 'with a nuget package with same name and version' do
+ let(:conan_username) { ::Packages::Conan::Metadatum.package_username_from(full_path: project.full_path) }
+ let(:wrong_package) { create(:nuget_package, name: "wrong", version: '1.0.0', project: project) }
+ let(:recipe_path) { "#{wrong_package.name}/#{wrong_package.version}/#{conan_username}/foo" }
+
+ it 'calls the presenter with a nil package' do
+ expect(::Packages::Conan::PackagePresenter).to receive(:new)
+ .with(nil, user, project, any_args)
+
+ subject
+ end
+ end
+end
+
+RSpec.shared_examples 'recipe download_urls' do
+ let(:recipe_path) { package.conan_recipe_path }
+
+ it 'returns the download_urls for the recipe files' do
+ expected_response = {
+ 'conanfile.py' => "#{url_prefix}/packages/conan/v1/files/#{package.conan_recipe_path}/0/export/conanfile.py",
+ 'conanmanifest.txt' => "#{url_prefix}/packages/conan/v1/files/#{package.conan_recipe_path}/0/export/conanmanifest.txt"
+ }
+
+ allow(presenter).to receive(:recipe_urls) { expected_response }
+
+ subject
+
+ expect(json_response).to eq(expected_response)
+ end
+
+ it_behaves_like 'not selecting a package with the wrong type'
+end
+
+RSpec.shared_examples 'package download_urls' do
+ let(:recipe_path) { package.conan_recipe_path }
+
+ it 'returns the download_urls for the package files' do
+ expected_response = {
+ 'conaninfo.txt' => "#{url_prefix}/packages/conan/v1/files/#{package.conan_recipe_path}/0/package/123456789/0/conaninfo.txt",
+ 'conanmanifest.txt' => "#{url_prefix}/packages/conan/v1/files/#{package.conan_recipe_path}/0/package/123456789/0/conanmanifest.txt",
+ 'conan_package.tgz' => "#{url_prefix}/packages/conan/v1/files/#{package.conan_recipe_path}/0/package/123456789/0/conan_package.tgz"
+ }
+
+ allow(presenter).to receive(:package_urls) { expected_response }
+
+ subject
+
+ expect(json_response).to eq(expected_response)
+ end
+
+ it_behaves_like 'not selecting a package with the wrong type'
+end
+
+RSpec.shared_examples 'rejects invalid upload_url params' do
+ context 'with unaccepted json format' do
+ let(:params) { %w[foo bar] }
+
+ it 'returns 400' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+ end
+end
+
+RSpec.shared_examples 'successful response when using Unicorn' do
+ context 'on Unicorn', :unicorn do
+ it 'returns successfully' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+end
+
+RSpec.shared_examples 'recipe snapshot endpoint' do
+ subject { get api(url), headers: headers }
+
+ it_behaves_like 'rejects invalid recipe'
+ it_behaves_like 'rejects recipe for invalid project'
+ it_behaves_like 'empty recipe for not found package'
+
+ context 'with existing package' do
+ it 'returns a hash of files with their md5 hashes' do
+ expected_response = {
+ 'conanfile.py' => 'md5hash1',
+ 'conanmanifest.txt' => 'md5hash2'
+ }
+
+ allow(presenter).to receive(:recipe_snapshot) { expected_response }
+
+ subject
+
+ expect(json_response).to eq(expected_response)
+ end
+ end
+end
+
+RSpec.shared_examples 'package snapshot endpoint' do
+ subject { get api(url), headers: headers }
+
+ it_behaves_like 'rejects invalid recipe'
+ it_behaves_like 'rejects recipe for invalid project'
+ it_behaves_like 'empty recipe for not found package'
+
+ context 'with existing package' do
+ it 'returns a hash of md5 values for the files' do
+ expected_response = {
+ 'conaninfo.txt' => "md5hash1",
+ 'conanmanifest.txt' => "md5hash2",
+ 'conan_package.tgz' => "md5hash3"
+ }
+
+ allow(presenter).to receive(:package_snapshot) { expected_response }
+
+ subject
+
+ expect(json_response).to eq(expected_response)
+ end
+ end
+end
+
+RSpec.shared_examples 'recipe download_urls endpoint' do
+ it_behaves_like 'rejects invalid recipe'
+ it_behaves_like 'rejects recipe for invalid project'
+ it_behaves_like 'recipe download_urls'
+end
+
+RSpec.shared_examples 'package download_urls endpoint' do
+ it_behaves_like 'rejects invalid recipe'
+ it_behaves_like 'rejects recipe for invalid project'
+ it_behaves_like 'package download_urls'
+end
+
+RSpec.shared_examples 'recipe upload_urls endpoint' do
+ let(:recipe_path) { package.conan_recipe_path }
+
+ let(:params) do
+ { 'conanfile.py': 24,
+ 'conanmanifest.txt': 123 }
+ end
+
+ it_behaves_like 'rejects invalid recipe'
+ it_behaves_like 'rejects invalid upload_url params'
+ it_behaves_like 'successful response when using Unicorn'
+
+ it 'returns a set of upload urls for the files requested' do
+ subject
+
+ expected_response = {
+ 'conanfile.py': "#{url_prefix}/packages/conan/v1/files/#{package.conan_recipe_path}/0/export/conanfile.py",
+ 'conanmanifest.txt': "#{url_prefix}/packages/conan/v1/files/#{package.conan_recipe_path}/0/export/conanmanifest.txt"
+ }
+
+ expect(response.body).to eq(expected_response.to_json)
+ end
+
+ context 'with conan_sources and conan_export files' do
+ let(:params) do
+ { 'conan_sources.tgz': 345,
+ 'conan_export.tgz': 234,
+ 'conanmanifest.txt': 123 }
+ end
+
+ it 'returns upload urls for the additional files' do
+ subject
+
+ expected_response = {
+ 'conan_sources.tgz': "#{url_prefix}/packages/conan/v1/files/#{package.conan_recipe_path}/0/export/conan_sources.tgz",
+ 'conan_export.tgz': "#{url_prefix}/packages/conan/v1/files/#{package.conan_recipe_path}/0/export/conan_export.tgz",
+ 'conanmanifest.txt': "#{url_prefix}/packages/conan/v1/files/#{package.conan_recipe_path}/0/export/conanmanifest.txt"
+ }
+
+ expect(response.body).to eq(expected_response.to_json)
+ end
+ end
+
+ context 'with an invalid file' do
+ let(:params) do
+ { 'invalid_file.txt': 10,
+ 'conanmanifest.txt': 123 }
+ end
+
+ it 'does not return the invalid file as an upload_url' do
+ subject
+
+ expected_response = {
+ 'conanmanifest.txt': "#{url_prefix}/packages/conan/v1/files/#{package.conan_recipe_path}/0/export/conanmanifest.txt"
+ }
+
+ expect(response.body).to eq(expected_response.to_json)
+ end
+ end
+end
+
+RSpec.shared_examples 'package upload_urls endpoint' do
+ let(:recipe_path) { package.conan_recipe_path }
+
+ let(:params) do
+ { 'conaninfo.txt': 24,
+ 'conanmanifest.txt': 123,
+ 'conan_package.tgz': 523 }
+ end
+
+ it_behaves_like 'rejects invalid recipe'
+ it_behaves_like 'rejects invalid upload_url params'
+ it_behaves_like 'successful response when using Unicorn'
+
+ it 'returns a set of upload urls for the files requested' do
+ expected_response = {
+ 'conaninfo.txt': "#{url_prefix}/packages/conan/v1/files/#{package.conan_recipe_path}/0/package/123456789/0/conaninfo.txt",
+ 'conanmanifest.txt': "#{url_prefix}/packages/conan/v1/files/#{package.conan_recipe_path}/0/package/123456789/0/conanmanifest.txt",
+ 'conan_package.tgz': "#{url_prefix}/packages/conan/v1/files/#{package.conan_recipe_path}/0/package/123456789/0/conan_package.tgz"
+ }
+
+ subject
+
+ expect(response.body).to eq(expected_response.to_json)
+ end
+
+ context 'with invalid files' do
+ let(:params) do
+ { 'conaninfo.txt': 24,
+ 'invalid_file.txt': 10 }
+ end
+
+ it 'returns upload urls only for the valid requested files' do
+ expected_response = {
+ 'conaninfo.txt': "#{url_prefix}/packages/conan/v1/files/#{package.conan_recipe_path}/0/package/123456789/0/conaninfo.txt"
+ }
+
+ subject
+
+ expect(response.body).to eq(expected_response.to_json)
+ end
+ end
+end
+
+RSpec.shared_examples 'delete package endpoint' do
+ let(:recipe_path) { package.conan_recipe_path }
+
+ it_behaves_like 'rejects invalid recipe'
+
+ it 'returns unauthorized for users without valid permission' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+
+ context 'with delete permissions' do
+ before do
+ project.add_maintainer(user)
+ end
+
+ it_behaves_like 'a gitlab tracking event', 'API::ConanPackages', 'delete_package'
+
+ it 'deletes a package' do
+ expect { subject }.to change { Packages::Package.count }.from(2).to(1)
+ end
+ end
+end
+
+RSpec.shared_examples 'denies download with no token' do
+ context 'with no private token' do
+ let(:headers) { {} }
+
+ it 'returns 400' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
+end
+
+RSpec.shared_examples 'a public project with packages' do
+ it 'returns the file' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.media_type).to eq('application/octet-stream')
+ end
+end
+
+RSpec.shared_examples 'an internal project with packages' do
+ before do
+ project.team.truncate
+ project.update_column(:visibility_level, Gitlab::VisibilityLevel::INTERNAL)
+ end
+
+ it_behaves_like 'denies download with no token'
+
+ it 'returns the file' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.media_type).to eq('application/octet-stream')
+ end
+end
+
+RSpec.shared_examples 'a private project with packages' do
+ before do
+ project.update_column(:visibility_level, Gitlab::VisibilityLevel::PRIVATE)
+ end
+
+ it_behaves_like 'denies download with no token'
+
+ it 'returns the file' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.media_type).to eq('application/octet-stream')
+ end
+
+ it 'denies download when not enough permissions' do
+ project.add_guest(user)
+
+ subject
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+end
+
+RSpec.shared_examples 'not found request' do
+ it 'returns not found' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+end
+
+RSpec.shared_examples 'recipe file download endpoint' do
+ it_behaves_like 'a public project with packages'
+ it_behaves_like 'an internal project with packages'
+ it_behaves_like 'a private project with packages'
+end
+
+RSpec.shared_examples 'package file download endpoint' do
+ it_behaves_like 'a public project with packages'
+ it_behaves_like 'an internal project with packages'
+ it_behaves_like 'a private project with packages'
+
+ context 'tracking the conan_package.tgz download' do
+ let(:package_file) { package.package_files.find_by(file_name: ::Packages::Conan::FileMetadatum::PACKAGE_BINARY) }
+
+ it_behaves_like 'a gitlab tracking event', 'API::ConanPackages', 'pull_package'
+ end
+end
+
+RSpec.shared_examples 'project not found by recipe' do
+ let(:recipe_path) { 'not/package/for/project' }
+
+ it_behaves_like 'not found request'
+end
+
+RSpec.shared_examples 'project not found by project id' do
+ let(:project_id) { 99999 }
+
+ it_behaves_like 'not found request'
+end
+
+RSpec.shared_examples 'workhorse authorize endpoint' do
+ it_behaves_like 'rejects invalid recipe'
+ it_behaves_like 'rejects invalid file_name', 'conanfile.py.git%2fgit-upload-pack'
+ it_behaves_like 'workhorse authorization'
+end
+
+RSpec.shared_examples 'workhorse recipe file upload endpoint' do
+ let(:file_name) { 'conanfile.py' }
+ let(:params) { { file: temp_file(file_name) } }
+
+ subject do
+ workhorse_finalize(
+ url,
+ method: :put,
+ file_key: :file,
+ params: params,
+ headers: headers_with_token,
+ send_rewritten_field: true
+ )
+ end
+
+ it_behaves_like 'rejects invalid recipe'
+ it_behaves_like 'rejects invalid file_name', 'conanfile.py.git%2fgit-upload-pack'
+ it_behaves_like 'uploads a package file'
+end
+
+RSpec.shared_examples 'workhorse package file upload endpoint' do
+ let(:file_name) { 'conaninfo.txt' }
+ let(:params) { { file: temp_file(file_name) } }
+
+ subject do
+ workhorse_finalize(
+ url,
+ method: :put,
+ file_key: :file,
+ params: params,
+ headers: headers_with_token,
+ send_rewritten_field: true
+ )
+ end
+
+ it_behaves_like 'rejects invalid recipe'
+ it_behaves_like 'rejects invalid file_name', 'conaninfo.txttest'
+ it_behaves_like 'uploads a package file'
+
+ context 'tracking the conan_package.tgz upload' do
+ let(:file_name) { ::Packages::Conan::FileMetadatum::PACKAGE_BINARY }
+
+ it_behaves_like 'a gitlab tracking event', 'API::ConanPackages', 'push_package'
+ end
+end
+
+RSpec.shared_examples 'uploads a package file' do
+ context 'file size above maximum limit' do
+ before do
+ params['file.size'] = project.actual_limits.conan_max_file_size + 1
+ end
+
+ it 'handles as a local file' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+ end
+
+ context 'with object storage disabled' do
+ context 'without a file from workhorse' do
+ let(:params) { { file: nil } }
+
+ it 'rejects the request' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+ end
+
+ context 'with a file' do
+ it_behaves_like 'package workhorse uploads'
+ end
+
+ context 'without a token' do
+ it 'rejects request without a token' do
+ headers_with_token.delete('HTTP_AUTHORIZATION')
+
+ subject
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
+
+ context 'when params from workhorse are correct' do
+ it 'creates package and stores package file' do
+ expect { subject }
+ .to change { project.packages.count }.by(1)
+ .and change { Packages::PackageFile.count }.by(1)
+
+ expect(response).to have_gitlab_http_status(:ok)
+
+ package_file = project.packages.last.package_files.reload.last
+ expect(package_file.file_name).to eq(params[:file].original_filename)
+ end
+
+ it "doesn't attempt to migrate file to object storage" do
+ expect(ObjectStorage::BackgroundMoveWorker).not_to receive(:perform_async)
+
+ subject
+ end
+ end
+ end
+
+ context 'with object storage enabled' do
+ context 'and direct upload enabled' do
+ let!(:fog_connection) do
+ stub_package_file_object_storage(direct_upload: true)
+ end
+
+ let(:tmp_object) do
+ fog_connection.directories.new(key: 'packages').files.create( # rubocop:disable Rails/SaveBang
+ key: "tmp/uploads/#{file_name}",
+ body: 'content'
+ )
+ end
+
+ let(:fog_file) { fog_to_uploaded_file(tmp_object) }
+
+ ['123123', '../../123123'].each do |remote_id|
+ context "with invalid remote_id: #{remote_id}" do
+ let(:params) do
+ {
+ file: fog_file,
+ 'file.remote_id' => remote_id
+ }
+ end
+
+ it 'responds with status 403' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+ end
+
+ context 'with valid remote_id' do
+ let(:params) do
+ {
+ file: fog_file,
+ 'file.remote_id' => file_name
+ }
+ end
+
+ it 'creates package and stores package file' do
+ expect { subject }
+ .to change { project.packages.count }.by(1)
+ .and change { Packages::PackageFile.count }.by(1)
+
+ expect(response).to have_gitlab_http_status(:ok)
+
+ package_file = project.packages.last.package_files.reload.last
+ expect(package_file.file_name).to eq(params[:file].original_filename)
+ expect(package_file.file.read).to eq('content')
+ end
+ end
+ end
+
+ it_behaves_like 'background upload schedules a file migration'
+ end
+end
+
+RSpec.shared_examples 'workhorse authorization' do
+ it 'authorizes posting package with a valid token' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.media_type).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
+ end
+
+ it 'rejects request without a valid token' do
+ headers_with_token['HTTP_AUTHORIZATION'] = 'foo'
+
+ subject
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+
+ it 'rejects request without a valid permission' do
+ project.add_guest(user)
+
+ subject
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+
+ it 'rejects requests that bypassed gitlab-workhorse' do
+ headers_with_token.delete(Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER)
+
+ subject
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+
+ context 'when using remote storage' do
+ context 'when direct upload is enabled' do
+ before do
+ stub_package_file_object_storage(enabled: true, direct_upload: true)
+ end
+
+ it 'responds with status 200, location of package remote store and object details' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.media_type).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
+ expect(json_response).not_to have_key('TempPath')
+ expect(json_response['RemoteObject']).to have_key('ID')
+ expect(json_response['RemoteObject']).to have_key('GetURL')
+ expect(json_response['RemoteObject']).to have_key('StoreURL')
+ expect(json_response['RemoteObject']).to have_key('DeleteURL')
+ expect(json_response['RemoteObject']).not_to have_key('MultipartUpload')
+ end
+ end
+
+ context 'when direct upload is disabled' do
+ before do
+ stub_package_file_object_storage(enabled: true, direct_upload: false)
+ end
+
+ it 'handles as a local file' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.media_type).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
+ expect(json_response['TempPath']).to eq(::Packages::PackageFileUploader.workhorse_local_upload_path)
+ expect(json_response['RemoteObject']).to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/requests/api/custom_attributes_shared_examples.rb b/spec/support/shared_examples/requests/api/custom_attributes_shared_examples.rb
index 8cbf11b6de1..f31cbcfdec1 100644
--- a/spec/support/shared_examples/requests/api/custom_attributes_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/custom_attributes_shared_examples.rb
@@ -1,8 +1,8 @@
# frozen_string_literal: true
RSpec.shared_examples 'custom attributes endpoints' do |attributable_name|
- let!(:custom_attribute1) { attributable.custom_attributes.create key: 'foo', value: 'foo' }
- let!(:custom_attribute2) { attributable.custom_attributes.create key: 'bar', value: 'bar' }
+ let!(:custom_attribute1) { attributable.custom_attributes.create! key: 'foo', value: 'foo' }
+ let!(:custom_attribute2) { attributable.custom_attributes.create! key: 'bar', value: 'bar' }
describe "GET /#{attributable_name} with custom attributes filter" do
before do
@@ -14,8 +14,7 @@ RSpec.shared_examples 'custom attributes endpoints' do |attributable_name|
get api("/#{attributable_name}", user), params: { custom_attributes: { foo: 'foo', bar: 'bar' } }
expect(response).to have_gitlab_http_status(:ok)
- expect(json_response.size).to be 2
- expect(json_response.map { |r| r['id'] }).to contain_exactly attributable.id, other_attributable.id
+ expect(json_response.map { |r| r['id'] }).to include(attributable.id, other_attributable.id)
end
end
@@ -40,7 +39,7 @@ RSpec.shared_examples 'custom attributes endpoints' do |attributable_name|
get api("/#{attributable_name}", user), params: { with_custom_attributes: true }
expect(response).to have_gitlab_http_status(:ok)
- expect(json_response.size).to be 2
+ expect(json_response).not_to be_empty
expect(json_response.first).not_to include 'custom_attributes'
end
end
@@ -50,16 +49,15 @@ RSpec.shared_examples 'custom attributes endpoints' do |attributable_name|
get api("/#{attributable_name}", admin)
expect(response).to have_gitlab_http_status(:ok)
- expect(json_response.size).to be 2
+ expect(json_response).not_to be_empty
expect(json_response.first).not_to include 'custom_attributes'
- expect(json_response.second).not_to include 'custom_attributes'
end
it 'includes custom attributes if requested' do
get api("/#{attributable_name}", admin), params: { with_custom_attributes: true }
expect(response).to have_gitlab_http_status(:ok)
- expect(json_response.size).to be 2
+ expect(json_response).not_to be_empty
attributable_response = json_response.find { |r| r['id'] == attributable.id }
other_attributable_response = json_response.find { |r| r['id'] == other_attributable.id }
@@ -132,7 +130,7 @@ RSpec.shared_examples 'custom attributes endpoints' do |attributable_name|
end
context 'with an authorized user' do
- it'returns a single custom attribute' do
+ it 'returns a single custom attribute' do
get api("/#{attributable_name}/#{attributable.id}/custom_attributes/foo", admin)
expect(response).to have_gitlab_http_status(:ok)
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
index 48824a4b0d2..62dbac3fd4d 100644
--- 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
@@ -8,3 +8,42 @@ RSpec.shared_examples 'when the snippet is not found' do
it_behaves_like 'a mutation that returns top-level errors',
errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR]
end
+
+RSpec.shared_examples 'snippet edit usage data counters' do
+ context 'when user is sessionless' do
+ it 'does not track usage data actions' do
+ expect(::Gitlab::UsageDataCounters::EditorUniqueCounter).not_to receive(:track_snippet_editor_edit_action)
+
+ post_graphql_mutation(mutation, current_user: current_user)
+ end
+ end
+
+ context 'when user is not sessionless' do
+ before do
+ session_id = Rack::Session::SessionId.new('6919a6f1bb119dd7396fadc38fd18d0d')
+ session_hash = { 'warden.user.user.key' => [[current_user.id], current_user.encrypted_password[0, 29]] }
+
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.set("session:gitlab:#{session_id.private_id}", Marshal.dump(session_hash))
+ end
+
+ cookies[Gitlab::Application.config.session_options[:key]] = session_id.public_id
+ end
+
+ it 'tracks usage data actions', :clean_gitlab_redis_shared_state do
+ expect(::Gitlab::UsageDataCounters::EditorUniqueCounter).to receive(:track_snippet_editor_edit_action)
+
+ post_graphql_mutation(mutation)
+ end
+
+ context 'when mutation result raises an error' do
+ it 'does not track usage data actions' do
+ mutation_vars[:title] = nil
+
+ expect(::Gitlab::UsageDataCounters::EditorUniqueCounter).not_to receive(:track_snippet_editor_edit_action)
+
+ post_graphql_mutation(mutation)
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb
index fcdc594f258..6aac51a5903 100644
--- a/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb
@@ -175,7 +175,7 @@ RSpec.shared_examples 'process nuget upload' do |user_type, status, add_member =
context 'with object storage enabled' do
let(:tmp_object) do
- fog_connection.directories.new(key: 'packages').files.create(
+ fog_connection.directories.new(key: 'packages').files.create( # rubocop:disable Rails/SaveBang
key: "tmp/uploads/#{file_name}",
body: 'content'
)
diff --git a/spec/support/shared_examples/requests/api/packages_shared_examples.rb b/spec/support/shared_examples/requests/api/packages_shared_examples.rb
index 6f4a0236b66..c9a33701161 100644
--- a/spec/support/shared_examples/requests/api/packages_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/packages_shared_examples.rb
@@ -41,3 +41,88 @@ RSpec.shared_examples 'deploy token for package uploads' do
end
end
end
+
+RSpec.shared_examples 'does not cause n^2 queries' do
+ it 'avoids N^2 database queries' do
+ # we create a package to set the baseline for expected queries from 1 package
+ create(
+ :npm_package,
+ name: "@#{project.root_namespace.path}/my-package",
+ project: project,
+ version: "0.0.1"
+ )
+
+ control = ActiveRecord::QueryRecorder.new do
+ get api(url)
+ end
+
+ 5.times do |n|
+ create(
+ :npm_package,
+ name: "@#{project.root_namespace.path}/my-package",
+ project: project,
+ version: "#{n}.0.0"
+ )
+ end
+
+ expect do
+ get api(url)
+ end.not_to exceed_query_limit(control)
+ end
+end
+
+RSpec.shared_examples 'job token for package GET requests' do
+ context 'with job token headers' do
+ let(:headers) { basic_auth_header(::Gitlab::Auth::CI_JOB_USER, job.token) }
+
+ subject { get api(url), headers: headers }
+
+ before do
+ project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
+ project.add_developer(user)
+ end
+
+ context 'valid token' do
+ it_behaves_like 'returning response status', :success
+ end
+
+ context 'invalid token' do
+ let(:headers) { basic_auth_header(::Gitlab::Auth::CI_JOB_USER, 'bar') }
+
+ it_behaves_like 'returning response status', :unauthorized
+ end
+
+ context 'invalid user' do
+ let(:headers) { basic_auth_header('foo', job.token) }
+
+ it_behaves_like 'returning response status', :unauthorized
+ end
+ end
+end
+
+RSpec.shared_examples 'job token for package uploads' do
+ context 'with job token headers' do
+ let(:headers) { basic_auth_header(::Gitlab::Auth::CI_JOB_USER, job.token).merge(workhorse_header) }
+
+ before do
+ project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
+ project.add_developer(user)
+ end
+
+ context 'valid token' do
+ it_behaves_like 'returning response status', :success
+ end
+
+ context 'invalid token' do
+ let(:headers) { basic_auth_header(::Gitlab::Auth::CI_JOB_USER, 'bar').merge(workhorse_header) }
+
+ it_behaves_like 'returning response status', :unauthorized
+ end
+
+ context 'invalid user' do
+ let(:headers) { basic_auth_header('foo', job.token).merge(workhorse_header) }
+
+ it_behaves_like 'returning response status', :unauthorized
+ end
+ end
+end
diff --git a/spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb
index 4954151b93b..715c494840e 100644
--- a/spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb
@@ -58,7 +58,7 @@ RSpec.shared_examples 'PyPi package creation' do |user_type, status, add_member
context 'with object storage enabled' do
let(:tmp_object) do
- fog_connection.directories.new(key: 'packages').files.create(
+ fog_connection.directories.new(key: 'packages').files.create( # rubocop:disable Rails/SaveBang
key: "tmp/uploads/#{file_name}",
body: 'content'
)
diff --git a/spec/support/shared_examples/requests/api/snippets_shared_examples.rb b/spec/support/shared_examples/requests/api/snippets_shared_examples.rb
index cfbb84dd099..051367fbe96 100644
--- a/spec/support/shared_examples/requests/api/snippets_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/snippets_shared_examples.rb
@@ -77,3 +77,142 @@ RSpec.shared_examples 'raw snippet files' do
end
end
end
+
+RSpec.shared_examples 'snippet file updates' do
+ let(:create_action) { { action: 'create', file_path: 'foo.txt', content: 'bar' } }
+ let(:update_action) { { action: 'update', file_path: 'CHANGELOG', content: 'bar' } }
+ let(:move_action) { { action: 'move', file_path: '.old-gitattributes', previous_path: '.gitattributes' } }
+ let(:delete_action) { { action: 'delete', file_path: 'CONTRIBUTING.md' } }
+ let(:bad_file_path) { { action: 'create', file_path: '../../etc/passwd', content: 'bar' } }
+ let(:bad_previous_path) { { action: 'create', previous_path: '../../etc/passwd', file_path: 'CHANGELOG', content: 'bar' } }
+ let(:invalid_move) { { action: 'move', file_path: 'missing_previous_path.txt' } }
+
+ context 'with various snippet file changes' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:is_multi_file, :file_name, :content, :files, :status) do
+ true | nil | nil | [create_action] | :success
+ true | nil | nil | [update_action] | :success
+ true | nil | nil | [move_action] | :success
+ true | nil | nil | [delete_action] | :success
+ true | nil | nil | [create_action, update_action] | :success
+ true | 'foo.txt' | 'bar' | [create_action] | :bad_request
+ true | 'foo.txt' | 'bar' | nil | :bad_request
+ true | nil | nil | nil | :bad_request
+ true | 'foo.txt' | nil | [create_action] | :bad_request
+ true | nil | 'bar' | [create_action] | :bad_request
+ true | '' | nil | [create_action] | :bad_request
+ true | nil | '' | [create_action] | :bad_request
+ true | nil | nil | [bad_file_path] | :bad_request
+ true | nil | nil | [bad_previous_path] | :bad_request
+ true | nil | nil | [invalid_move] | :unprocessable_entity
+
+ false | 'foo.txt' | 'bar' | nil | :success
+ false | 'foo.txt' | nil | nil | :success
+ false | nil | 'bar' | nil | :success
+ false | 'foo.txt' | 'bar' | [create_action] | :bad_request
+ false | nil | nil | nil | :bad_request
+ false | nil | '' | nil | :bad_request
+ false | nil | nil | [bad_file_path] | :bad_request
+ false | nil | nil | [bad_previous_path] | :bad_request
+ end
+
+ with_them do
+ before do
+ allow_any_instance_of(Snippet).to receive(:multiple_files?).and_return(is_multi_file)
+ end
+
+ it 'has the correct response' do
+ update_params = {}.tap do |params|
+ params[:files] = files if files
+ params[:file_name] = file_name if file_name
+ params[:content] = content if content
+ end
+
+ update_snippet(params: update_params)
+
+ expect(response).to have_gitlab_http_status(status)
+ end
+ end
+
+ context 'when save fails due to a repository commit error' do
+ before do
+ allow_next_instance_of(Repository) do |instance|
+ allow(instance).to receive(:multi_action).and_raise(Gitlab::Git::CommitError)
+ end
+
+ update_snippet(params: { files: [create_action] })
+ end
+
+ it 'returns a bad request response' do
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+ end
+ end
+end
+
+RSpec.shared_examples 'snippet non-file updates' do
+ it 'updates a snippet non-file attributes' do
+ new_description = 'New description'
+ new_title = 'New title'
+ new_visibility = 'internal'
+
+ update_snippet(params: { title: new_title, description: new_description, visibility: new_visibility })
+
+ snippet.reload
+
+ aggregate_failures do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(snippet.description).to eq(new_description)
+ expect(snippet.visibility).to eq(new_visibility)
+ expect(snippet.title).to eq(new_title)
+ end
+ end
+end
+
+RSpec.shared_examples 'snippet individual non-file updates' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:attribute, :updated_value) do
+ :description | 'new description'
+ :title | 'new title'
+ :visibility | 'private'
+ end
+
+ with_them do
+ it 'updates the attribute' do
+ params = { attribute => updated_value }
+
+ expect { update_snippet(params: params) }
+ .to change { snippet.reload.send(attribute) }.to(updated_value)
+ end
+ end
+end
+
+RSpec.shared_examples 'invalid snippet updates' do
+ it 'returns 404 for invalid snippet id' do
+ update_snippet(snippet_id: non_existing_record_id, params: { title: 'foo' })
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect(json_response['message']).to eq('404 Snippet Not Found')
+ end
+
+ it 'returns 400 for missing parameters' do
+ update_snippet
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+
+ it 'returns 400 if content is blank' do
+ update_snippet(params: { content: '' })
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+
+ it 'returns 400 if title is blank' do
+ update_snippet(params: { title: '' })
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['error']).to eq 'title is empty'
+ end
+end
diff --git a/spec/support/shared_examples/requests/snippet_shared_examples.rb b/spec/support/shared_examples/requests/snippet_shared_examples.rb
index a17163328f4..84ef7723b9b 100644
--- a/spec/support/shared_examples/requests/snippet_shared_examples.rb
+++ b/spec/support/shared_examples/requests/snippet_shared_examples.rb
@@ -2,6 +2,10 @@
RSpec.shared_examples 'update with repository actions' do
context 'when the repository exists' do
+ before do
+ allow_any_instance_of(Snippet).to receive(:multiple_files?).and_return(false)
+ end
+
it 'commits the changes to the repository' do
existing_blob = snippet.blobs.first
new_file_name = existing_blob.path + '_new'
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 1ef08de31a9..7608f1c7f8a 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
@@ -57,16 +57,6 @@ RSpec.shared_examples 'diff file entity' do
expect(subject).to include(:highlighted_diff_lines)
end
end
-
- context 'when the `single_mr_diff_view` feature is disabled' do
- before do
- stub_feature_flags(single_mr_diff_view: false)
- end
-
- it 'contains both kinds of diffs' do
- expect(subject).to include(:highlighted_diff_lines, :parallel_diff_lines)
- end
- end
end
end
diff --git a/spec/support/shared_examples/services/alert_management_shared_examples.rb b/spec/support/shared_examples/services/alert_management_shared_examples.rb
index a1354a8099b..1ae74979b7a 100644
--- a/spec/support/shared_examples/services/alert_management_shared_examples.rb
+++ b/spec/support/shared_examples/services/alert_management_shared_examples.rb
@@ -39,3 +39,41 @@ RSpec.shared_examples 'adds an alert management alert event' do
subject
end
end
+
+RSpec.shared_examples 'processes incident issues' do
+ let(:create_incident_service) { spy }
+
+ before do
+ allow_any_instance_of(AlertManagement::Alert).to receive(:execute_services)
+ end
+
+ it 'processes issues' do
+ expect(IncidentManagement::ProcessAlertWorker)
+ .to receive(:perform_async)
+ .with(nil, nil, kind_of(Integer))
+ .once
+
+ Sidekiq::Testing.inline! do
+ expect(subject).to be_success
+ end
+ end
+end
+
+RSpec.shared_examples 'does not process incident issues' do
+ it 'does not process issues' do
+ expect(IncidentManagement::ProcessAlertWorker)
+ .not_to receive(:perform_async)
+
+ expect(subject).to be_success
+ end
+end
+
+RSpec.shared_examples 'does not process incident issues due to error' do |http_status:|
+ it 'does not process issues' do
+ expect(IncidentManagement::ProcessAlertWorker)
+ .not_to receive(:perform_async)
+
+ expect(subject).to be_error
+ expect(subject.http_status).to eq(http_status)
+ end
+end
diff --git a/spec/support/shared_examples/services/common_system_notes_shared_examples.rb b/spec/support/shared_examples/services/common_system_notes_shared_examples.rb
index 20856b05de6..5b95a5753a1 100644
--- a/spec/support/shared_examples/services/common_system_notes_shared_examples.rb
+++ b/spec/support/shared_examples/services/common_system_notes_shared_examples.rb
@@ -5,7 +5,7 @@ RSpec.shared_examples 'system note creation' do |update_params, note_text|
before do
issuable.assign_attributes(update_params)
- issuable.save
+ issuable.save!
end
it 'creates 1 system note with the correct content' do
diff --git a/spec/support/shared_examples/services/incident_shared_examples.rb b/spec/support/shared_examples/services/incident_shared_examples.rb
new file mode 100644
index 00000000000..d6e79931df5
--- /dev/null
+++ b/spec/support/shared_examples/services/incident_shared_examples.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+# This shared_example requires the following variables:
+# - issue (required)
+#
+# Usage:
+#
+# it_behaves_like 'incident issue' do
+# let(:issue) { ... }
+# end
+#
+# include_examples 'incident issue'
+RSpec.shared_examples 'incident issue' do
+ let(:label_properties) { attributes_for(:label, :incident) }
+
+ it 'has incident as issue type' do
+ expect(issue.issue_type).to eq('incident')
+ end
+
+ it 'has exactly one incident label' do
+ expect(issue.labels).to be_one do |label|
+ label.slice(*label_properties.keys).symbolize_keys == label_properties
+ end
+ end
+end
+
+# This shared_example requires the following variables:
+# - issue (required)
+#
+# Usage:
+#
+# it_behaves_like 'not an incident issue' do
+# let(:issue) { ... }
+# end
+#
+# include_examples 'not an incident issue'
+RSpec.shared_examples 'not an incident issue' do
+ let(:label_properties) { attributes_for(:label, :incident) }
+
+ it 'has not incident as issue type' do
+ expect(issue.issue_type).not_to eq('incident')
+ end
+
+ it 'has not an incident label' do
+ expect(issue.labels).not_to include(have_attributes(label_properties))
+ end
+end
diff --git a/spec/support/shared_examples/services/issuable_shared_examples.rb b/spec/support/shared_examples/services/issuable_shared_examples.rb
index 9eb66e33513..47c7a1e7356 100644
--- a/spec/support/shared_examples/services/issuable_shared_examples.rb
+++ b/spec/support/shared_examples/services/issuable_shared_examples.rb
@@ -8,37 +8,6 @@ RSpec.shared_examples 'cache counters invalidator' do
end
end
-RSpec.shared_examples 'system notes for milestones' do
- def update_issuable(opts)
- issuable = try(:issue) || try(:merge_request)
- described_class.new(project, user, opts).execute(issuable)
- end
-
- context 'group milestones' do
- let(:group) { create(:group) }
- let(:group_milestone) { create(:milestone, group: group) }
-
- before do
- project.update(namespace: group)
- create(:group_member, group: group, user: user)
- end
-
- it 'creates a system note' do
- expect do
- update_issuable(milestone: group_milestone)
- end.to change { Note.system.count }.by(1)
- end
- end
-
- context 'project milestones' do
- it 'creates a system note' do
- expect do
- update_issuable(milestone: create(:milestone, project: project))
- end.to change { Note.system.count }.by(1)
- end
- end
-end
-
RSpec.shared_examples 'updating a single task' do
def update_issuable(opts)
issuable = try(:issue) || try(:merge_request)
diff --git a/spec/support/shared_examples/services/merge_request_shared_examples.rb b/spec/support/shared_examples/services/merge_request_shared_examples.rb
new file mode 100644
index 00000000000..a7032640217
--- /dev/null
+++ b/spec/support/shared_examples/services/merge_request_shared_examples.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'reviewer_ids filter' do
+ context 'filter_reviewer' do
+ let(:opts) { super().merge(reviewer_ids_param) }
+
+ context 'without reviewer_ids' do
+ let(:reviewer_ids_param) { {} }
+
+ it 'contains no reviewer_ids' do
+ expect(execute.reviewers).to eq []
+ end
+ end
+
+ context 'with reviewer_ids' do
+ let(:reviewer_ids_param) { { reviewer_ids: [reviewer1.id, reviewer2.id, reviewer3.id] } }
+
+ let(:reviewer1) { create(:user) }
+ let(:reviewer2) { create(:user) }
+ let(:reviewer3) { create(:user) }
+
+ context 'when the current user can admin the merge_request' do
+ context 'when merge_request_reviewer feature is enabled' do
+ before do
+ stub_feature_flags(merge_request_reviewer: true)
+ end
+
+ context 'with reviewers who can read the merge_request' do
+ before do
+ project.add_developer(reviewer1)
+ project.add_developer(reviewer2)
+ end
+
+ it 'contains reviewers who can read the merge_request' do
+ expect(execute.reviewers).to contain_exactly(reviewer1, reviewer2)
+ end
+ end
+ end
+
+ context 'when merge_request_reviewer feature is disabled' do
+ before do
+ stub_feature_flags(merge_request_reviewer: false)
+ end
+
+ it 'contains no reviewers' do
+ expect(execute.reviewers).to eq []
+ end
+ end
+ end
+
+ context 'when the current_user cannot admin the merge_request' do
+ before do
+ project.add_developer(user)
+ end
+
+ it 'contains no reviewers' do
+ expect(execute.reviewers).to eq []
+ end
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/services/packages_shared_examples.rb b/spec/support/shared_examples/services/packages_shared_examples.rb
index 45a4c2bb151..7fd59c3d963 100644
--- a/spec/support/shared_examples/services/packages_shared_examples.rb
+++ b/spec/support/shared_examples/services/packages_shared_examples.rb
@@ -14,6 +14,14 @@ RSpec.shared_examples 'assigns build to package' do
end
end
+RSpec.shared_examples 'assigns the package creator' do
+ it 'assigns the package creator' do
+ subject
+
+ expect(package.creator).to eq user
+ end
+end
+
RSpec.shared_examples 'returns packages' do |container_type, user_type|
context "for #{user_type}" do
before do
@@ -161,6 +169,7 @@ RSpec.shared_examples 'filters on each package_type' do |is_project: false|
let_it_be(:package4) { create(:nuget_package, project: project) }
let_it_be(:package5) { create(:pypi_package, project: project) }
let_it_be(:package6) { create(:composer_package, project: project) }
+ let_it_be(:package7) { create(:generic_package, project: project) }
Packages::Package.package_types.keys.each do |package_type|
context "for package type #{package_type}" do
diff --git a/spec/support/shared_examples/services/snippets_shared_examples.rb b/spec/support/shared_examples/services/snippets_shared_examples.rb
index 51a4a8b1cd9..4a08c0d4365 100644
--- a/spec/support/shared_examples/services/snippets_shared_examples.rb
+++ b/spec/support/shared_examples/services/snippets_shared_examples.rb
@@ -40,3 +40,20 @@ RSpec.shared_examples 'snippets spam check is performed' do
end
end
end
+
+shared_examples 'invalid params error response' do
+ before do
+ allow_next_instance_of(described_class) do |service|
+ allow(service).to receive(:valid_params?).and_return false
+ end
+ end
+
+ it 'responds to errors appropriately' do
+ response = subject
+
+ aggregate_failures do
+ expect(response).to be_error
+ expect(response.http_status).to eq 422
+ 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
index db1b50fdf3c..ffdd0c36cfc 100644
--- a/spec/support/shared_examples/services/wiki_pages/destroy_service_shared_examples.rb
+++ b/spec/support/shared_examples/services/wiki_pages/destroy_service_shared_examples.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
RSpec.shared_examples 'WikiPages::DestroyService#execute' do |container_type|
- let(:container) { create(container_type) }
+ let(:container) { create(container_type) } # rubocop:disable Rails/SaveBang
let(:user) { create(:user) }
let(:page) { create(:wiki_page) }
@@ -32,9 +32,19 @@ RSpec.shared_examples 'WikiPages::DestroyService#execute' do |container_type|
)
end
- it 'does not increment the delete count if the deletion failed' do
- counter = Gitlab::UsageDataCounters::WikiPageCounter
+ context 'when the deletion fails' do
+ before do
+ expect(page).to receive(:delete).and_return(false)
+ end
+
+ it 'returns an error response' do
+ response = service.execute(page)
+ expect(response).to be_error
+ end
- expect { service.execute(nil) }.not_to change { counter.read(:delete) }
+ it 'does not increment the delete count if the deletion failed' do
+ counter = Gitlab::UsageDataCounters::WikiPageCounter
+ expect { service.execute(page) }.not_to change { counter.read(:delete) }
+ 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
index 0191a6dfbc9..fd10dd4367e 100644
--- a/spec/support/shared_examples/services/wiki_pages/update_service_shared_examples.rb
+++ b/spec/support/shared_examples/services/wiki_pages/update_service_shared_examples.rb
@@ -19,8 +19,10 @@ RSpec.shared_examples 'WikiPages::UpdateService#execute' do |container_type|
subject(:service) { described_class.new(container: container, current_user: user, params: opts) }
it 'updates the wiki page' do
- updated_page = service.execute(page)
+ response = service.execute(page)
+ updated_page = response.payload[:page]
+ expect(response).to be_success
expect(updated_page).to be_valid
expect(updated_page.message).to eq(opts[:message])
expect(updated_page.content).to eq(opts[:content])
@@ -81,7 +83,11 @@ RSpec.shared_examples 'WikiPages::UpdateService#execute' do |container_type|
end
it 'reports the error' do
- expect(service.execute(page)).to be_invalid
+ response = service.execute(page)
+ page = response.payload[:page]
+
+ expect(response).to be_error
+ expect(page).to be_invalid
.and have_attributes(errors: be_present)
end
end
diff --git a/spec/support/shared_examples/uploaders/workers/object_storage/migrate_uploads_shared_examples.rb b/spec/support/shared_examples/uploaders/workers/object_storage/migrate_uploads_shared_examples.rb
index f143cbc7165..5a9a3dfc2d2 100644
--- a/spec/support/shared_examples/uploaders/workers/object_storage/migrate_uploads_shared_examples.rb
+++ b/spec/support/shared_examples/uploaders/workers/object_storage/migrate_uploads_shared_examples.rb
@@ -63,7 +63,7 @@ RSpec.shared_examples 'uploads migration worker' do
if success > 0
it 'outputs the reports' do
- expect(Rails.logger).to receive(:info).with(%r{Migrated #{success}/#{total} files})
+ expect(Gitlab::AppLogger).to receive(:info).with(%r{Migrated #{success}/#{total} files})
perform(uploads)
end
@@ -71,7 +71,7 @@ RSpec.shared_examples 'uploads migration worker' do
if failures > 0
it 'outputs upload failures' do
- expect(Rails.logger).to receive(:warn).with(/Error .* I am a teapot/)
+ expect(Gitlab::AppLogger).to receive(:warn).with(/Error .* I am a teapot/)
perform(uploads)
end
diff --git a/spec/support/snowplow.rb b/spec/support/snowplow.rb
new file mode 100644
index 00000000000..58812b8f4e6
--- /dev/null
+++ b/spec/support/snowplow.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+RSpec.configure do |config|
+ config.before(:each, :snowplow) do
+ # Using a high buffer size to not cause early flushes
+ buffer_size = 100
+ # WebMock is set up to allow requests to `localhost`
+ host = 'localhost'
+
+ allow(Gitlab::Tracking)
+ .to receive(:emitter)
+ .and_return(SnowplowTracker::Emitter.new(host, buffer_size: buffer_size))
+
+ stub_application_setting(snowplow_enabled: true)
+
+ allow(Gitlab::Tracking).to receive(:event).and_call_original
+ end
+
+ config.after(:each, :snowplow) do
+ Gitlab::Tracking.send(:snowplow).flush
+ end
+end
diff --git a/spec/support/test_reports/test_reports_helper.rb b/spec/support/test_reports/test_reports_helper.rb
index 6ba50c83b25..ad9ecb6f460 100644
--- a/spec/support/test_reports/test_reports_helper.rb
+++ b/spec/support/test_reports/test_reports_helper.rb
@@ -10,12 +10,12 @@ module TestReportsHelper
status: Gitlab::Ci::Reports::TestCase::STATUS_SUCCESS)
end
- def create_test_case_rspec_failed(name = 'test_spec')
+ def create_test_case_rspec_failed(name = 'test_spec', execution_time = 2.22)
Gitlab::Ci::Reports::TestCase.new(
name: 'Test#sum when a is 1 and b is 3 returns summary',
classname: "spec.#{name}",
file: './spec/test_spec.rb',
- execution_time: 2.22,
+ execution_time: execution_time,
system_output: sample_rspec_failed_message,
status: Gitlab::Ci::Reports::TestCase::STATUS_FAILED)
end