Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-10-21 10:08:36 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2020-10-21 10:08:36 +0300
commit48aff82709769b098321c738f3444b9bdaa694c6 (patch)
treee00c7c43e2d9b603a5a6af576b1685e400410dee /spec/support
parent879f5329ee916a948223f8f43d77fba4da6cd028 (diff)
Add latest changes from gitlab-org/gitlab@13-5-stable-eev13.5.0-rc42
Diffstat (limited to 'spec/support')
-rw-r--r--spec/support/capybara.rb8
-rw-r--r--spec/support/counter_attribute.rb6
-rw-r--r--spec/support/factory_bot.rb4
-rw-r--r--spec/support/google_api/cloud_platform_helpers.rb12
-rw-r--r--spec/support/helpers/api_internal_base_helpers.rb85
-rw-r--r--spec/support/helpers/cycle_analytics_helpers.rb8
-rw-r--r--spec/support/helpers/drag_to_helper.rb7
-rw-r--r--spec/support/helpers/features/blob_spec_helpers.rb22
-rw-r--r--spec/support/helpers/features/canonical_link_helpers.rb28
-rw-r--r--spec/support/helpers/features/snippet_helpers.rb64
-rw-r--r--spec/support/helpers/git_http_helpers.rb26
-rw-r--r--spec/support/helpers/graphql_helpers.rb6
-rw-r--r--spec/support/helpers/javascript_fixtures_helpers.rb11
-rw-r--r--spec/support/helpers/kubernetes_helpers.rb87
-rw-r--r--spec/support/helpers/multipart_helpers.rb2
-rw-r--r--spec/support/helpers/rack_attack_spec_helpers.rb4
-rw-r--r--spec/support/helpers/search_helpers.rb10
-rw-r--r--spec/support/helpers/snippet_helpers.rb2
-rw-r--r--spec/support/helpers/snowplow_helpers.rb12
-rw-r--r--spec/support/helpers/stub_experiments.rb4
-rw-r--r--spec/support/helpers/stub_feature_flags.rb4
-rw-r--r--spec/support/helpers/stub_object_storage.rb18
-rw-r--r--spec/support/helpers/stubbed_feature.rb26
-rw-r--r--spec/support/helpers/usage_data_helpers.rb10
-rw-r--r--spec/support/helpers/wait_for_requests.rb11
-rw-r--r--spec/support/helpers/wiki_helpers.rb10
-rw-r--r--spec/support/matchers/be_sorted.rb21
-rw-r--r--spec/support/migrations_helpers/cluster_helpers.rb10
-rw-r--r--spec/support/migrations_helpers/namespaces_helper.rb2
-rw-r--r--spec/support/migrations_helpers/schema_version_finder.rb34
-rw-r--r--spec/support/models/merge_request_without_merge_request_diff.rb7
-rw-r--r--spec/support/shared_contexts/cache_allowed_users_in_namespace_shared_context.rb2
-rw-r--r--spec/support/shared_contexts/email_shared_context.rb2
-rw-r--r--spec/support/shared_contexts/finders/group_projects_finder_shared_contexts.rb6
-rw-r--r--spec/support/shared_contexts/lib/gitlab/import_export/relation_tree_restorer_shared_context.rb13
-rw-r--r--spec/support/shared_contexts/mailers/notify_shared_context.rb2
-rw-r--r--spec/support/shared_contexts/navbar_structure_context.rb15
-rw-r--r--spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/controllers/access_tokens_controller_shared_examples.rb106
-rw-r--r--spec/support/shared_examples/controllers/cache_control_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/controllers/destroy_hook_shared_examples.rb36
-rw-r--r--spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/controllers/known_sign_in_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/controllers/milestone_tabs_shared_examples.rb23
-rw-r--r--spec/support/shared_examples/controllers/sessionless_auth_controller_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/controllers/unique_hll_events_examples.rb49
-rw-r--r--spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb69
-rw-r--r--spec/support/shared_examples/features/editable_merge_request_shared_examples.rb28
-rw-r--r--spec/support/shared_examples/features/issuable_invite_members_shared_examples.rb68
-rw-r--r--spec/support/shared_examples/features/multiple_assignees_mr_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/features/multiple_reviewers_mr_shared_examples.rb47
-rw-r--r--spec/support/shared_examples/features/navbar_shared_examples.rb6
-rw-r--r--spec/support/shared_examples/features/packages_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/features/page_description_shared_examples.rb9
-rw-r--r--spec/support/shared_examples/features/protected_branches_with_deploy_keys_examples.rb95
-rw-r--r--spec/support/shared_examples/features/snippets_shared_examples.rb24
-rw-r--r--spec/support/shared_examples/features/wiki/file_attachments_shared_examples.rb (renamed from spec/support/shared_examples/features/wiki_file_attachments_shared_examples.rb)4
-rw-r--r--spec/support/shared_examples/features/wiki/user_creates_wiki_page_shared_examples.rb245
-rw-r--r--spec/support/shared_examples/features/wiki/user_deletes_wiki_page_shared_examples.rb24
-rw-r--r--spec/support/shared_examples/features/wiki/user_previews_wiki_changes_shared_examples.rb110
-rw-r--r--spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb231
-rw-r--r--spec/support/shared_examples/features/wiki/user_uses_wiki_shortcuts_shared_examples.rb20
-rw-r--r--spec/support/shared_examples/features/wiki/user_views_asciidoc_page_with_includes_shared_examples.rb77
-rw-r--r--spec/support/shared_examples/features/wiki/user_views_wiki_empty_shared_examples.rb62
-rw-r--r--spec/support/shared_examples/features/wiki/user_views_wiki_page_shared_examples.rb274
-rw-r--r--spec/support/shared_examples/features/wiki/user_views_wiki_pages_shared_examples.rb89
-rw-r--r--spec/support/shared_examples/features/wiki/user_views_wiki_sidebar_shared_examples.rb68
-rw-r--r--spec/support/shared_examples/graphql/mutations/boards_create_shared_examples.rb75
-rw-r--r--spec/support/shared_examples/graphql/mutations/spammable_mutation_fields_examples.rb47
-rw-r--r--spec/support/shared_examples/graphql/notes_creation_shared_examples.rb8
-rw-r--r--spec/support/shared_examples/lib/gitlab/background_migration/mentions_migration_shared_examples.rb24
-rw-r--r--spec/support/shared_examples/lib/gitlab/ci/ci_trace_shared_examples.rb6
-rw-r--r--spec/support/shared_examples/lib/gitlab/diff_file_collections_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/lib/gitlab/import_export/relation_factory_shared_examples.rb107
-rw-r--r--spec/support/shared_examples/lib/gitlab/repository_size_checker_shared_examples.rb51
-rw-r--r--spec/support/shared_examples/lib/gitlab/search/recent_items.rb43
-rw-r--r--spec/support/shared_examples/lib/gitlab/search_confidential_filter_shared_examples.rb69
-rw-r--r--spec/support/shared_examples/lib/gitlab/search_results_sorted_shared_examples.rb19
-rw-r--r--spec/support/shared_examples/lib/gitlab/search_state_filter_shared_examples.rb (renamed from spec/support/shared_examples/lib/gitlab/search_issue_state_filter_shared_examples.rb)0
-rw-r--r--spec/support/shared_examples/mailers/notify_shared_examples.rb6
-rw-r--r--spec/support/shared_examples/models/concerns/has_repository_shared_examples.rb22
-rw-r--r--spec/support/shared_examples/models/concerns/shardable_shared_examples.rb21
-rw-r--r--spec/support/shared_examples/models/concerns/timebox_shared_examples.rb86
-rw-r--r--spec/support/shared_examples/models/mentionable_shared_examples.rb23
-rw-r--r--spec/support/shared_examples/models/project_latest_successful_build_for_shared_examples.rb16
-rw-r--r--spec/support/shared_examples/models/relative_positioning_shared_examples.rb182
-rw-r--r--spec/support/shared_examples/models/resource_timebox_event_shared_examples.rb10
-rw-r--r--spec/support/shared_examples/models/snippet_shared_examples.rb10
-rw-r--r--spec/support/shared_examples/models/throttled_touch_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/models/update_project_statistics_shared_examples.rb260
-rw-r--r--spec/support/shared_examples/models/wiki_shared_examples.rb85
-rw-r--r--spec/support/shared_examples/policies/resource_access_token_shared_examples.rb32
-rw-r--r--spec/support/shared_examples/quick_actions/issuable/issuable_quick_actions_shared_examples.rb8
-rw-r--r--spec/support/shared_examples/quick_actions/merge_request/merge_quick_action_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/requests/api/composer_packages_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/requests/api/container_repositories_shared_examples.rb8
-rw-r--r--spec/support/shared_examples/requests/api/debian_packages_shared_examples.rb309
-rw-r--r--spec/support/shared_examples/requests/api/graphql/group_and_project_boards_query_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb8
-rw-r--r--spec/support/shared_examples/requests/api/packages_shared_examples.rb8
-rw-r--r--spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb18
-rw-r--r--spec/support/shared_examples/requests/api/snippets_shared_examples.rb179
-rw-r--r--spec/support/shared_examples/requests/api/tracking_shared_examples.rb9
-rw-r--r--spec/support/shared_examples/requests/rack_attack_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/requests/snippet_shared_examples.rb12
-rw-r--r--spec/support/shared_examples/requests/user_activity_shared_examples.rb97
-rw-r--r--spec/support/shared_examples/serializers/note_entity_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/services/boards/boards_create_service_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/services/boards/issues_move_service_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/services/incident_shared_examples.rb71
-rw-r--r--spec/support/shared_examples/services/merge_request_shared_examples.rb8
-rw-r--r--spec/support/shared_examples/services/packages_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/services/projects/urls_with_escaped_elements_shared_example.rb20
-rw-r--r--spec/support/shared_examples/validators/ip_address_validator_shared_examples.rb10
-rw-r--r--spec/support/test_reports/test_reports_helper.rb8
115 files changed, 3956 insertions, 428 deletions
diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb
index 66fce4fddf1..ab55cf97ab4 100644
--- a/spec/support/capybara.rb
+++ b/spec/support/capybara.rb
@@ -50,6 +50,10 @@ Capybara.register_driver :chrome do |app|
)
options = Selenium::WebDriver::Chrome::Options.new
+
+ # Force the browser's scale factor to prevent inconsistencies on high-res devices
+ options.add_argument('--force-device-scale-factor=1')
+
options.add_argument("window-size=#{CAPYBARA_WINDOW_SIZE.join(',')}")
# Chrome won't work properly in a Docker container in sandbox mode
@@ -123,6 +127,10 @@ RSpec.configure do |config|
port: session.server.port,
protocol: 'http')
+ # CSRF protection is disabled by default. We only enable this for JS specs because some forms
+ # require Javascript to set the CSRF token.
+ allow_any_instance_of(ActionController::Base).to receive(:protect_against_forgery?).and_return(true)
+
# reset window size between tests
unless session.current_window.size == CAPYBARA_WINDOW_SIZE
begin
diff --git a/spec/support/counter_attribute.rb b/spec/support/counter_attribute.rb
index ea71b25b4c0..8bd40b72dcf 100644
--- a/spec/support/counter_attribute.rb
+++ b/spec/support/counter_attribute.rb
@@ -9,6 +9,12 @@ RSpec.configure do |config|
counter_attribute :build_artifacts_size
counter_attribute :commit_count
+
+ attr_accessor :flushed
+
+ counter_attribute_after_flush do |subject|
+ subject.flushed = true
+ end
end
end
end
diff --git a/spec/support/factory_bot.rb b/spec/support/factory_bot.rb
index a86161bfded..c9d372993b5 100644
--- a/spec/support/factory_bot.rb
+++ b/spec/support/factory_bot.rb
@@ -3,7 +3,3 @@
FactoryBot::SyntaxRunner.class_eval do
include RSpec::Mocks::ExampleMethods
end
-
-# Use FactoryBot 4.x behavior:
-# https://github.com/thoughtbot/factory_bot/blob/master/GETTING_STARTED.md#associations
-FactoryBot.use_parent_strategy = false
diff --git a/spec/support/google_api/cloud_platform_helpers.rb b/spec/support/google_api/cloud_platform_helpers.rb
index 38ffca8c5ae..840f948e377 100644
--- a/spec/support/google_api/cloud_platform_helpers.rb
+++ b/spec/support/google_api/cloud_platform_helpers.rb
@@ -22,7 +22,7 @@ module GoogleApi
.to_return(cloud_platform_response(cloud_platform_projects_billing_info_body(project_id, billing_enabled)))
end
- def stub_cloud_platform_get_zone_cluster(project_id, zone, cluster_id, **options)
+ def stub_cloud_platform_get_zone_cluster(project_id, zone, cluster_id, options = {})
WebMock.stub_request(:get, cloud_platform_get_zone_cluster_url(project_id, zone, cluster_id))
.to_return(cloud_platform_response(cloud_platform_cluster_body(options)))
end
@@ -32,7 +32,7 @@ module GoogleApi
.to_return(status: [500, "Internal Server Error"])
end
- def stub_cloud_platform_create_cluster(project_id, zone, **options)
+ def stub_cloud_platform_create_cluster(project_id, zone, options = {})
WebMock.stub_request(:post, cloud_platform_create_cluster_url(project_id, zone))
.to_return(cloud_platform_response(cloud_platform_operation_body(options)))
end
@@ -42,7 +42,7 @@ module GoogleApi
.to_return(status: [500, "Internal Server Error"])
end
- def stub_cloud_platform_get_zone_operation(project_id, zone, operation_id, **options)
+ def stub_cloud_platform_get_zone_operation(project_id, zone, operation_id, options = {})
WebMock.stub_request(:get, cloud_platform_get_zone_operation_url(project_id, zone, operation_id))
.to_return(cloud_platform_response(cloud_platform_operation_body(options)))
end
@@ -86,7 +86,7 @@ module GoogleApi
# https://cloud.google.com/kubernetes-engine/docs/reference/rest/v1/projects.zones.clusters/create
# rubocop:disable Metrics/CyclomaticComplexity
# rubocop:disable Metrics/PerceivedComplexity
- def cloud_platform_cluster_body(**options)
+ def cloud_platform_cluster_body(options)
{
"name": options[:name] || 'string',
"description": options[:description] || 'string',
@@ -121,7 +121,7 @@ module GoogleApi
}
end
- def cloud_platform_operation_body(**options)
+ def cloud_platform_operation_body(options)
{
"name": options[:name] || 'operation-1234567891234-1234567',
"zone": options[:zone] || 'us-central1-a',
@@ -136,7 +136,7 @@ module GoogleApi
}
end
- def cloud_platform_projects_body(**options)
+ def cloud_platform_projects_body(options)
{
"projects": [
{
diff --git a/spec/support/helpers/api_internal_base_helpers.rb b/spec/support/helpers/api_internal_base_helpers.rb
new file mode 100644
index 00000000000..94996f7480e
--- /dev/null
+++ b/spec/support/helpers/api_internal_base_helpers.rb
@@ -0,0 +1,85 @@
+# frozen_string_literal: true
+
+module APIInternalBaseHelpers
+ def gl_repository_for(container)
+ case container
+ when ProjectWiki
+ Gitlab::GlRepository::WIKI.identifier_for_container(container)
+ when Project
+ Gitlab::GlRepository::PROJECT.identifier_for_container(container)
+ when Snippet
+ Gitlab::GlRepository::SNIPPET.identifier_for_container(container)
+ else
+ nil
+ end
+ end
+
+ def full_path_for(container)
+ case container
+ when PersonalSnippet
+ "snippets/#{container.id}"
+ when ProjectSnippet
+ "#{container.project.full_path}/snippets/#{container.id}"
+ else
+ container.full_path
+ end
+ end
+
+ def pull(key, container, protocol = 'ssh')
+ post(
+ api("/internal/allowed"),
+ params: {
+ key_id: key.id,
+ project: full_path_for(container),
+ gl_repository: gl_repository_for(container),
+ action: 'git-upload-pack',
+ secret_token: secret_token,
+ protocol: protocol
+ }
+ )
+ end
+
+ def push(key, container, protocol = 'ssh', env: nil, changes: nil)
+ push_with_path(key,
+ full_path: full_path_for(container),
+ gl_repository: gl_repository_for(container),
+ protocol: protocol,
+ env: env,
+ changes: changes)
+ end
+
+ def push_with_path(key, full_path:, gl_repository: nil, protocol: 'ssh', env: nil, changes: nil)
+ changes ||= 'd14d6c0abdd253381df51a723d58691b2ee1ab08 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/master'
+
+ params = {
+ changes: changes,
+ key_id: key.id,
+ project: full_path,
+ action: 'git-receive-pack',
+ secret_token: secret_token,
+ protocol: protocol,
+ env: env
+ }
+ params[:gl_repository] = gl_repository if gl_repository
+
+ post(
+ api("/internal/allowed"),
+ params: params
+ )
+ end
+
+ def archive(key, container)
+ post(
+ api("/internal/allowed"),
+ params: {
+ ref: 'master',
+ key_id: key.id,
+ project: full_path_for(container),
+ gl_repository: gl_repository_for(container),
+ action: 'git-upload-archive',
+ secret_token: secret_token,
+ protocol: 'ssh'
+ }
+ )
+ end
+end
diff --git a/spec/support/helpers/cycle_analytics_helpers.rb b/spec/support/helpers/cycle_analytics_helpers.rb
index f4343b8b783..6d3ac699a7c 100644
--- a/spec/support/helpers/cycle_analytics_helpers.rb
+++ b/spec/support/helpers/cycle_analytics_helpers.rb
@@ -126,17 +126,15 @@ module CycleAnalyticsHelpers
end
def mock_gitaly_multi_action_dates(repository, commit_time)
- allow(repository.raw).to receive(:multi_action).and_wrap_original do |m, *args|
+ allow(repository.raw).to receive(:multi_action).and_wrap_original do |m, user, kargs|
new_date = commit_time || Time.now
- branch_update = m.call(*args)
+ branch_update = m.call(user, **kargs)
if branch_update.newrev
- _, opts = args
-
commit = rugged_repo(repository).rev_parse(branch_update.newrev)
branch_update.newrev = commit.amend(
- update_ref: "#{Gitlab::Git::BRANCH_REF_PREFIX}#{opts[:branch_name]}",
+ update_ref: "#{Gitlab::Git::BRANCH_REF_PREFIX}#{kargs[:branch_name]}",
author: commit.author.merge(time: new_date),
committer: commit.committer.merge(time: new_date)
)
diff --git a/spec/support/helpers/drag_to_helper.rb b/spec/support/helpers/drag_to_helper.rb
index 2e9932f2e8a..692a4d2b30e 100644
--- a/spec/support/helpers/drag_to_helper.rb
+++ b/spec/support/helpers/drag_to_helper.rb
@@ -1,7 +1,8 @@
# frozen_string_literal: true
module DragTo
- def drag_to(list_from_index: 0, from_index: 0, to_index: 0, list_to_index: 0, selector: '', scrollable: 'body', duration: 1000, perform_drop: true)
+ # rubocop:disable Metrics/ParameterLists
+ def drag_to(list_from_index: 0, from_index: 0, to_index: 0, list_to_index: 0, selector: '', scrollable: 'body', duration: 1000, perform_drop: true, extra_height: 0)
js = <<~JS
simulateDrag({
scrollable: document.querySelector('#{scrollable}'),
@@ -14,7 +15,8 @@ module DragTo
el: document.querySelectorAll('#{selector}')[#{list_to_index}],
index: #{to_index}
},
- performDrop: #{perform_drop}
+ performDrop: #{perform_drop},
+ extraHeight: #{extra_height}
});
JS
evaluate_script(js)
@@ -23,6 +25,7 @@ module DragTo
loop while drag_active?
end
end
+ # rubocop:enable Metrics/ParameterLists
def drag_active?
page.evaluate_script('window.SIMULATE_DRAG_ACTIVE').nonzero?
diff --git a/spec/support/helpers/features/blob_spec_helpers.rb b/spec/support/helpers/features/blob_spec_helpers.rb
new file mode 100644
index 00000000000..880a7249284
--- /dev/null
+++ b/spec/support/helpers/features/blob_spec_helpers.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+# These helpers help you interact within the blobs page and blobs edit page (Single file editor).
+module BlobSpecHelpers
+ include ActionView::Helpers::JavaScriptHelper
+
+ def set_default_button(type)
+ evaluate_script("localStorage.setItem('gl-web-ide-button-selected', '#{type}')")
+ end
+
+ def unset_default_button
+ set_default_button('')
+ end
+
+ def editor_value
+ evaluate_script('monaco.editor.getModels()[0].getValue()')
+ end
+
+ def set_editor_value(value)
+ execute_script("monaco.editor.getModels()[0].setValue('#{value}')")
+ end
+end
diff --git a/spec/support/helpers/features/canonical_link_helpers.rb b/spec/support/helpers/features/canonical_link_helpers.rb
new file mode 100644
index 00000000000..da3a28f1cb2
--- /dev/null
+++ b/spec/support/helpers/features/canonical_link_helpers.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+# These helpers allow you to manipulate with notes.
+#
+# Usage:
+# describe "..." do
+# include Spec::Support::Helpers::Features::CanonicalLinkHelpers
+# ...
+#
+# expect(page).to have_canonical_link(url)
+#
+module Spec
+ module Support
+ module Helpers
+ module Features
+ module CanonicalLinkHelpers
+ def have_canonical_link(url)
+ have_xpath("//link[@rel=\"canonical\" and @href=\"#{url}\"]", visible: false)
+ end
+
+ def have_any_canonical_links
+ have_xpath('//link[@rel="canonical"]', visible: false)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/support/helpers/features/snippet_helpers.rb b/spec/support/helpers/features/snippet_helpers.rb
index c01d179770c..c26849a9680 100644
--- a/spec/support/helpers/features/snippet_helpers.rb
+++ b/spec/support/helpers/features/snippet_helpers.rb
@@ -10,39 +10,69 @@ module Spec
include ActionView::Helpers::JavaScriptHelper
include Spec::Support::Helpers::Features::EditorLiteSpecHelpers
+ def snippet_description_locator
+ 'snippet-description'
+ end
+
+ def snippet_blob_path_locator
+ 'snippet_file_name'
+ end
+
+ def snippet_description_view_selector
+ '.snippet-header .snippet-description'
+ end
+
+ def snippet_description_field_collapsed
+ find('.js-description-input').find('input,textarea')
+ end
+
def snippet_get_first_blob_path
- page.find_field(snippet_blob_path_field, match: :first).value
+ page.find_field('snippet_file_name', match: :first).value
end
def snippet_get_first_blob_value
- page.find(snippet_blob_content_selector, match: :first)
+ page.find('.gl-editor-lite', match: :first)
end
def snippet_description_value
- page.find_field(snippet_description_field).value
+ page.find_field(snippet_description_locator).value
+ end
+
+ def snippet_fill_in_visibility(text)
+ page.find('#visibility-level-setting').choose(text)
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
+ def snippet_fill_in_title(value)
+ fill_in 'snippet-title', with: value
+ end
- if description
- # Click placeholder first to expand full description field
- description_field.click
- fill_in snippet_description_field, with: description
- end
+ def snippet_fill_in_description(value)
+ # Click placeholder first to expand full description field
+ snippet_description_field_collapsed.click
+ fill_in snippet_description_locator, with: value
+ end
- page.within('.file-editor') do
+ def snippet_fill_in_content(value)
+ page.within('.gl-editor-lite') do
el = find('.inputarea')
- el.send_keys content
+ el.send_keys value
end
end
- private
+ def snippet_fill_in_file_name(value)
+ fill_in(snippet_blob_path_locator, match: :first, with: value)
+ end
+
+ def snippet_fill_in_form(title: nil, content: nil, file_name: nil, description: nil, visibility: nil)
+ snippet_fill_in_title(title) if title
- def description_field
- find('.js-description-input').find('input,textarea')
+ snippet_fill_in_description(description) if description
+
+ snippet_fill_in_file_name(file_name) if file_name
+
+ snippet_fill_in_content(content) if content
+
+ snippet_fill_in_visibility(visibility) if visibility
end
end
end
diff --git a/spec/support/helpers/git_http_helpers.rb b/spec/support/helpers/git_http_helpers.rb
index de8bb9ac8e3..c9c1c4dcfc9 100644
--- a/spec/support/helpers/git_http_helpers.rb
+++ b/spec/support/helpers/git_http_helpers.rb
@@ -5,45 +5,45 @@ require_relative 'workhorse_helpers'
module GitHttpHelpers
include WorkhorseHelpers
- def clone_get(project, options = {})
+ def clone_get(project, **options)
get "/#{project}/info/refs", params: { service: 'git-upload-pack' }, headers: auth_env(*options.values_at(:user, :password, :spnego_request_token))
end
- def clone_post(project, options = {})
+ def clone_post(project, **options)
post "/#{project}/git-upload-pack", headers: auth_env(*options.values_at(:user, :password, :spnego_request_token))
end
- def push_get(project, options = {})
+ def push_get(project, **options)
get "/#{project}/info/refs", params: { service: 'git-receive-pack' }, headers: auth_env(*options.values_at(:user, :password, :spnego_request_token))
end
- def push_post(project, options = {})
+ def push_post(project, **options)
post "/#{project}/git-receive-pack", headers: auth_env(*options.values_at(:user, :password, :spnego_request_token))
end
def download(project, user: nil, password: nil, spnego_request_token: nil)
- args = [project, { user: user, password: password, spnego_request_token: spnego_request_token }]
+ args = { user: user, password: password, spnego_request_token: spnego_request_token }
- clone_get(*args)
+ clone_get(project, **args)
yield response
- clone_post(*args)
+ clone_post(project, **args)
yield response
end
def upload(project, user: nil, password: nil, spnego_request_token: nil)
- args = [project, { user: user, password: password, spnego_request_token: spnego_request_token }]
+ args = { user: user, password: password, spnego_request_token: spnego_request_token }
- push_get(*args)
+ push_get(project, **args)
yield response
- push_post(*args)
+ push_post(project, **args)
yield response
end
- def download_or_upload(*args, &block)
- download(*args, &block)
- upload(*args, &block)
+ def download_or_upload(project, **args, &block)
+ download(project, **args, &block)
+ upload(project, **args, &block)
end
def auth_env(user, password, spnego_request_token)
diff --git a/spec/support/helpers/graphql_helpers.rb b/spec/support/helpers/graphql_helpers.rb
index 5635ba3df05..db769041f1e 100644
--- a/spec/support/helpers/graphql_helpers.rb
+++ b/spec/support/helpers/graphql_helpers.rb
@@ -23,7 +23,7 @@ module GraphqlHelpers
return early_return unless ready
- resolver.resolve(args)
+ resolver.resolve(**args)
end
# Eagerly run a loader's named resolver
@@ -219,6 +219,7 @@ module GraphqlHelpers
def as_graphql_literal(value)
case value
when Array then "[#{value.map { |v| as_graphql_literal(v) }.join(',')}]"
+ when Hash then "{#{attributes_to_graphql(value)}}"
when Integer, Float then value.to_s
when String then "\"#{value.gsub(/"/, '\\"')}\""
when Symbol then value
@@ -234,7 +235,8 @@ module GraphqlHelpers
end
def post_graphql(query, current_user: nil, variables: nil, headers: {})
- post api('/', current_user, version: 'graphql'), params: { query: query, variables: variables }, headers: headers
+ params = { query: query, variables: variables&.to_json }
+ post api('/', current_user, version: 'graphql'), params: params, headers: headers
end
def post_graphql_mutation(mutation, current_user: nil)
diff --git a/spec/support/helpers/javascript_fixtures_helpers.rb b/spec/support/helpers/javascript_fixtures_helpers.rb
index 4f11f8c6b24..2224af88ab9 100644
--- a/spec/support/helpers/javascript_fixtures_helpers.rb
+++ b/spec/support/helpers/javascript_fixtures_helpers.rb
@@ -39,6 +39,17 @@ module JavaScriptFixturesHelpers
Gitlab::Shell.new.remove_repository(project.repository_storage, project.disk_path)
end
+ # Public: Reads a GraphQL query from the filesystem as a string
+ #
+ # query_path - file path to the GraphQL query, relative to `app/assets/javascripts`
+ # fragment_paths - an optional array of file paths to any fragments the query uses,
+ # also relative to `app/assets/javascripts`
+ def get_graphql_query_as_string(query_path, fragment_paths = [])
+ [query_path, *fragment_paths].map do |path|
+ File.read(File.join(Rails.root, '/app/assets/javascripts', path))
+ end.join("\n")
+ end
+
private
# Private: Store a response object as fixture file
diff --git a/spec/support/helpers/kubernetes_helpers.rb b/spec/support/helpers/kubernetes_helpers.rb
index 8882f31e2f4..113bb31e4be 100644
--- a/spec/support/helpers/kubernetes_helpers.rb
+++ b/spec/support/helpers/kubernetes_helpers.rb
@@ -33,6 +33,10 @@ module KubernetesHelpers
kube_response(kube_deployments_body)
end
+ def kube_ingresses_response
+ kube_response(kube_ingresses_body)
+ end
+
def stub_kubeclient_discover_base(api_url)
WebMock.stub_request(:get, api_url + '/api/v1').to_return(kube_response(kube_v1_discovery_body))
WebMock
@@ -63,6 +67,9 @@ module KubernetesHelpers
WebMock
.stub_request(:get, api_url + '/apis/serving.knative.dev/v1alpha1')
.to_return(kube_response(kube_v1alpha1_serving_knative_discovery_body))
+ WebMock
+ .stub_request(:get, api_url + '/apis/networking.k8s.io/v1')
+ .to_return(kube_response(kube_v1_networking_discovery_body))
end
def stub_kubeclient_discover_knative_not_found(api_url)
@@ -148,12 +155,20 @@ module KubernetesHelpers
WebMock.stub_request(:get, deployments_url).to_return(response || kube_deployments_response)
end
+ def stub_kubeclient_ingresses(namespace, status: nil)
+ stub_kubeclient_discover(service.api_url)
+ ingresses_url = service.api_url + "/apis/extensions/v1beta1/namespaces/#{namespace}/ingresses"
+ response = { status: status } if status
+
+ WebMock.stub_request(:get, ingresses_url).to_return(response || kube_ingresses_response)
+ end
+
def stub_kubeclient_knative_services(options = {})
namespace_path = options[:namespace].present? ? "namespaces/#{options[:namespace]}/" : ""
options[:name] ||= "kubetest"
options[:domain] ||= "example.com"
- options[:response] ||= kube_response(kube_knative_services_body(options))
+ options[:response] ||= kube_response(kube_knative_services_body(**options))
stub_kubeclient_discover(service.api_url)
@@ -265,7 +280,7 @@ module KubernetesHelpers
.to_return(kube_response({}))
end
- def kube_v1_secret_body(**options)
+ def kube_v1_secret_body(options)
{
"kind" => "SecretList",
"apiVersion": "v1",
@@ -304,6 +319,14 @@ module KubernetesHelpers
}
end
+ # From Kubernetes 1.22+ Ingresses are no longer served from apis/extensions
+ def kube_1_22_extensions_v1beta1_discovery_body
+ {
+ "kind" => "APIResourceList",
+ "resources" => []
+ }
+ end
+
def kube_knative_discovery_body
{
"kind" => "APIResourceList",
@@ -416,6 +439,17 @@ module KubernetesHelpers
}
end
+ def kube_v1_networking_discovery_body
+ {
+ "kind" => "APIResourceList",
+ "apiVersion" => "v1",
+ "groupVersion" => "networking.k8s.io/v1",
+ "resources" => [
+ { "name" => "ingresses", "namespaced" => true, "kind" => "Ingress" }
+ ]
+ }
+ end
+
def kube_istio_gateway_body(name, namespace)
{
"apiVersion" => "networking.istio.io/v1alpha3",
@@ -507,6 +541,13 @@ module KubernetesHelpers
}
end
+ def kube_ingresses_body
+ {
+ "kind" => "List",
+ "items" => [kube_ingress]
+ }
+ end
+
def kube_knative_pods_body(name, namespace)
{
"kind" => "PodList",
@@ -517,7 +558,7 @@ module KubernetesHelpers
def kube_knative_services_body(**options)
{
"kind" => "List",
- "items" => [knative_09_service(options)]
+ "items" => [knative_09_service(**options)]
}
end
@@ -548,6 +589,38 @@ module KubernetesHelpers
}
end
+ def kube_ingress(track: :stable)
+ additional_annotations =
+ if track == :canary
+ {
+ "nginx.ingress.kubernetes.io/canary" => "true",
+ "nginx.ingress.kubernetes.io/canary-by-header" => "canary",
+ "nginx.ingress.kubernetes.io/canary-weight" => "50"
+ }
+ else
+ {}
+ end
+
+ {
+ "metadata" => {
+ "name" => "production-auto-deploy",
+ "labels" => {
+ "app" => "production",
+ "app.kubernetes.io/managed-by" => "Helm",
+ "chart" => "auto-deploy-app-2.0.0-beta.2",
+ "heritage" => "Helm",
+ "release" => "production"
+ },
+ "annotations" => {
+ "kubernetes.io/ingress.class" => "nginx",
+ "kubernetes.io/tls-acme" => "true",
+ "meta.helm.sh/release-name" => "production",
+ "meta.helm.sh/release-namespace" => "awesome-app-1-production"
+ }.merge(additional_annotations)
+ }
+ }
+ end
+
# This is a partial response, it will have many more elements in reality but
# these are the ones we care about at the moment
def kube_node
@@ -604,7 +677,7 @@ module KubernetesHelpers
}
end
- def kube_deployment(name: "kube-deployment", environment_slug: "production", project_slug: "project-path-slug", track: nil)
+ def kube_deployment(name: "kube-deployment", environment_slug: "production", project_slug: "project-path-slug", track: nil, replicas: 3)
{
"metadata" => {
"name" => name,
@@ -617,7 +690,7 @@ module KubernetesHelpers
"track" => track
}.compact
},
- "spec" => { "replicas" => 3 },
+ "spec" => { "replicas" => replicas },
"status" => {
"observedGeneration" => 4
}
@@ -862,8 +935,8 @@ module KubernetesHelpers
end
end
- def kube_deployment_rollout_status
- ::Gitlab::Kubernetes::RolloutStatus.from_deployments(kube_deployment)
+ def kube_deployment_rollout_status(ingresses: [])
+ ::Gitlab::Kubernetes::RolloutStatus.from_deployments(kube_deployment, ingresses: ingresses)
end
def empty_deployment_rollout_status
diff --git a/spec/support/helpers/multipart_helpers.rb b/spec/support/helpers/multipart_helpers.rb
index 043cb6e1420..2e8db0e9e42 100644
--- a/spec/support/helpers/multipart_helpers.rb
+++ b/spec/support/helpers/multipart_helpers.rb
@@ -31,7 +31,7 @@ module MultipartHelpers
raise ArgumentError, "can't handle #{mode} mode"
end
- return result if ::Feature.disabled?(:upload_middleware_jwt_params_handler)
+ return result if ::Feature.disabled?(:upload_middleware_jwt_params_handler, default_enabled: true)
# the HandlerForJWTParams expects a jwt token with the upload parameters
# *without* the "#{key}." prefix
diff --git a/spec/support/helpers/rack_attack_spec_helpers.rb b/spec/support/helpers/rack_attack_spec_helpers.rb
index 65082ec690f..a8ae69885d8 100644
--- a/spec/support/helpers/rack_attack_spec_helpers.rb
+++ b/spec/support/helpers/rack_attack_spec_helpers.rb
@@ -1,10 +1,6 @@
# frozen_string_literal: true
module RackAttackSpecHelpers
- def post_args_with_token_headers(url, token_headers)
- [url, params: nil, headers: token_headers]
- end
-
def api_get_args_with_token_headers(partial_url, token_headers)
["/api/#{API::API.version}#{partial_url}", params: nil, headers: token_headers]
end
diff --git a/spec/support/helpers/search_helpers.rb b/spec/support/helpers/search_helpers.rb
index db6e47459e9..328f272724a 100644
--- a/spec/support/helpers/search_helpers.rb
+++ b/spec/support/helpers/search_helpers.rb
@@ -1,6 +1,14 @@
# frozen_string_literal: true
module SearchHelpers
+ def fill_in_search(text)
+ page.within('.search-input-wrap') do
+ fill_in('search', with: text)
+ end
+
+ wait_for_all_requests
+ end
+
def submit_search(query, scope: nil)
page.within('.search-form, .search-page-form') do
field = find_field('search')
@@ -11,6 +19,8 @@ module SearchHelpers
else
click_button('Search')
end
+
+ wait_for_all_requests
end
end
diff --git a/spec/support/helpers/snippet_helpers.rb b/spec/support/helpers/snippet_helpers.rb
index de64ad7d3e2..1ec50bce070 100644
--- a/spec/support/helpers/snippet_helpers.rb
+++ b/spec/support/helpers/snippet_helpers.rb
@@ -8,7 +8,7 @@ module SnippetHelpers
def snippet_blob_file(blob)
{
"path" => blob.path,
- "raw_url" => gitlab_raw_snippet_blob_url(blob.container, blob.path)
+ "raw_url" => gitlab_raw_snippet_blob_url(blob.container, blob.path, host: 'localhost')
}
end
end
diff --git a/spec/support/helpers/snowplow_helpers.rb b/spec/support/helpers/snowplow_helpers.rb
index 83a5b7e48bc..3bde01c6fbf 100644
--- a/spec/support/helpers/snowplow_helpers.rb
+++ b/spec/support/helpers/snowplow_helpers.rb
@@ -32,8 +32,16 @@ module SnowplowHelpers
# end
# end
def expect_snowplow_event(category:, action:, **kwargs)
- expect(Gitlab::Tracking).to have_received(:event)
- .with(category, action, **kwargs).at_least(:once)
+ # This check will no longer be needed with Ruby 2.7 which
+ # would not pass any arguments when using **kwargs.
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/263430
+ if kwargs.present?
+ expect(Gitlab::Tracking).to have_received(:event)
+ .with(category, action, **kwargs).at_least(:once)
+ else
+ expect(Gitlab::Tracking).to have_received(:event)
+ .with(category, action).at_least(:once)
+ end
end
# Asserts that no call to `Gitlab::Tracking#event` was made.
diff --git a/spec/support/helpers/stub_experiments.rb b/spec/support/helpers/stub_experiments.rb
index ff3b02dc3f6..7a6154d5ef9 100644
--- a/spec/support/helpers/stub_experiments.rb
+++ b/spec/support/helpers/stub_experiments.rb
@@ -22,10 +22,10 @@ module StubExperiments
# Examples
# - `stub_experiment_for_user(signup_flow: false)` ... Disable `signup_flow` experiment for user.
def stub_experiment_for_user(experiments)
- allow(Gitlab::Experimentation).to receive(:enabled_for_user?).and_call_original
+ allow(Gitlab::Experimentation).to receive(:enabled_for_value?).and_call_original
experiments.each do |experiment_key, enabled|
- allow(Gitlab::Experimentation).to receive(:enabled_for_user?).with(experiment_key, anything) { enabled }
+ allow(Gitlab::Experimentation).to receive(:enabled_for_value?).with(experiment_key, anything) { enabled }
end
end
end
diff --git a/spec/support/helpers/stub_feature_flags.rb b/spec/support/helpers/stub_feature_flags.rb
index 792a1c21c31..7f30a2a70cd 100644
--- a/spec/support/helpers/stub_feature_flags.rb
+++ b/spec/support/helpers/stub_feature_flags.rb
@@ -62,4 +62,8 @@ module StubFeatureFlags
StubFeatureGate.new(object)
end
+
+ def skip_feature_flags_yaml_validation
+ allow(Feature::Definition).to receive(:valid_usage!)
+ end
end
diff --git a/spec/support/helpers/stub_object_storage.rb b/spec/support/helpers/stub_object_storage.rb
index 476b7d34ee5..dba3d2b137e 100644
--- a/spec/support/helpers/stub_object_storage.rb
+++ b/spec/support/helpers/stub_object_storage.rb
@@ -82,13 +82,27 @@ module StubObjectStorage
**params)
end
- def stub_terraform_state_object_storage(uploader = described_class, **params)
+ def stub_terraform_state_object_storage(**params)
stub_object_storage_uploader(config: Gitlab.config.terraform_state.object_store,
- uploader: uploader,
+ uploader: Terraform::VersionedStateUploader,
+ remote_directory: 'terraform',
+ **params)
+ end
+
+ def stub_terraform_state_version_object_storage(**params)
+ stub_object_storage_uploader(config: Gitlab.config.terraform_state.object_store,
+ uploader: Terraform::StateUploader,
remote_directory: 'terraform',
**params)
end
+ def stub_pages_object_storage(uploader = described_class, **params)
+ stub_object_storage_uploader(config: Gitlab.config.pages.object_store,
+ uploader: uploader,
+ remote_directory: 'pages',
+ **params)
+ end
+
def stub_object_storage_multipart_init(endpoint, upload_id = "upload_id")
stub_request(:post, %r{\A#{endpoint}tmp/uploads/[a-z0-9-]*\?uploads\z})
.to_return status: 200, body: <<-EOS.strip_heredoc
diff --git a/spec/support/helpers/stubbed_feature.rb b/spec/support/helpers/stubbed_feature.rb
index d4e9af7a031..67ceb7d9b35 100644
--- a/spec/support/helpers/stubbed_feature.rb
+++ b/spec/support/helpers/stubbed_feature.rb
@@ -4,6 +4,14 @@
module StubbedFeature
extend ActiveSupport::Concern
+ prepended do
+ cattr_reader(:persist_used) do
+ # persist feature flags in CI
+ # nil: indicates that we do not want to persist used feature flags
+ Gitlab::Utils.to_boolean(ENV['CI']) ? {} : nil
+ end
+ end
+
class_methods do
# Turn stubbed feature flags on or off.
def stub=(stub)
@@ -29,10 +37,12 @@ module StubbedFeature
end
# Replace #enabled? method with the optional stubbed/unstubbed version.
- def enabled?(*args)
- feature_flag = super(*args)
+ def enabled?(*args, **kwargs)
+ feature_flag = super
return feature_flag unless stub?
+ persist_used!(args.first)
+
# If feature flag is not persisted we mark the feature flag as enabled
# We do `m.call` as we want to validate the execution of method arguments
# and a feature flag state if it is not persisted
@@ -42,5 +52,17 @@ module StubbedFeature
feature_flag
end
+
+ # This method creates a temporary file in `tmp/feature_flags`
+ # if feature flag was touched during execution
+ def persist_used!(name)
+ return unless persist_used
+ return if persist_used[name]
+
+ persist_used[name] = true
+ FileUtils.touch(
+ Rails.root.join('tmp', 'feature_flags', name.to_s + ".used")
+ )
+ end
end
end
diff --git a/spec/support/helpers/usage_data_helpers.rb b/spec/support/helpers/usage_data_helpers.rb
index d92fcdc2d4a..2592d9f8b42 100644
--- a/spec/support/helpers/usage_data_helpers.rb
+++ b/spec/support/helpers/usage_data_helpers.rb
@@ -99,6 +99,7 @@ module UsageDataHelpers
projects_with_error_tracking_enabled
projects_with_alerts_service_enabled
projects_with_prometheus_alerts
+ projects_with_tracing_enabled
projects_with_expiration_policy_enabled
projects_with_expiration_policy_disabled
projects_with_expiration_policy_enabled_with_keep_n_unset
@@ -133,6 +134,7 @@ module UsageDataHelpers
todos
uploads
web_hooks
+ user_preferences_user_gitpod_enabled
).push(*SMAU_KEYS)
USAGE_DATA_KEYS = %i(
@@ -171,6 +173,10 @@ module UsageDataHelpers
allow(Gitlab::Prometheus::Internal).to receive(:prometheus_enabled?).and_return(false)
end
+ def clear_memoized_values(values)
+ values.each { |v| described_class.clear_memoization(v) }
+ end
+
def stub_object_store_settings
allow(Settings).to receive(:[]).with('artifacts')
.and_return(
@@ -228,9 +234,9 @@ module UsageDataHelpers
receive_matchers.each { |m| expect(prometheus_client).to m }
end
- def for_defined_days_back(days: [29, 2])
+ def for_defined_days_back(days: [31, 3])
days.each do |n|
- Timecop.travel(n.days.ago) do
+ travel_to(n.days.ago) do
yield
end
end
diff --git a/spec/support/helpers/wait_for_requests.rb b/spec/support/helpers/wait_for_requests.rb
index 2cfd47634ca..43060e571a9 100644
--- a/spec/support/helpers/wait_for_requests.rb
+++ b/spec/support/helpers/wait_for_requests.rb
@@ -48,17 +48,10 @@ module WaitForRequests
def finished_all_js_requests?
return true unless javascript_test?
- finished_all_ajax_requests? &&
- finished_all_axios_requests?
- end
-
- def finished_all_axios_requests?
- Capybara.page.evaluate_script('window.pendingRequests || 0').zero? # rubocop:disable Style/NumericPredicate
+ finished_all_ajax_requests?
end
def finished_all_ajax_requests?
- return true if Capybara.page.evaluate_script('typeof jQuery === "undefined"')
-
- Capybara.page.evaluate_script('jQuery.active').zero? # rubocop:disable Style/NumericPredicate
+ Capybara.page.evaluate_script('window.pendingRequests || window.pendingRailsUJSRequests || 0').zero? # rubocop:disable Style/NumericPredicate
end
end
diff --git a/spec/support/helpers/wiki_helpers.rb b/spec/support/helpers/wiki_helpers.rb
index e59c6bde264..8873a90579d 100644
--- a/spec/support/helpers/wiki_helpers.rb
+++ b/spec/support/helpers/wiki_helpers.rb
@@ -13,16 +13,16 @@ module WikiHelpers
find('.svg-content .js-lazy-loaded') if example.nil? || example.metadata.key?(:js)
end
- def upload_file_to_wiki(container, user, file_name)
- opts = {
+ def upload_file_to_wiki(wiki, user, file_name)
+ params = {
file_name: file_name,
file_content: File.read(expand_fixture_path(file_name))
}
::Wikis::CreateAttachmentService.new(
- container: container,
+ container: wiki.container,
current_user: user,
- params: opts
- ).execute[:result][:file_path]
+ params: params
+ ).execute.dig(:result, :file_path)
end
end
diff --git a/spec/support/matchers/be_sorted.rb b/spec/support/matchers/be_sorted.rb
new file mode 100644
index 00000000000..1455060fe71
--- /dev/null
+++ b/spec/support/matchers/be_sorted.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+# Assert that this collection is sorted by argument and order
+#
+# By default, this checks that the collection is sorted ascending
+# but you can check order by specific field and order by passing
+# them, eg:
+#
+# ```
+# expect(collection).to be_sorted(:field, :desc)
+# ```
+RSpec::Matchers.define :be_sorted do |by, order = :asc|
+ match do |actual|
+ next true unless actual.present? # emtpy collection is sorted
+
+ actual
+ .then { |collection| by ? collection.sort_by(&by) : collection.sort }
+ .then { |sorted_collection| order.to_sym == :desc ? sorted_collection.reverse : sorted_collection }
+ .then { |sorted_collection| sorted_collection == actual }
+ end
+end
diff --git a/spec/support/migrations_helpers/cluster_helpers.rb b/spec/support/migrations_helpers/cluster_helpers.rb
index b54af15c29e..03104e22bcf 100644
--- a/spec/support/migrations_helpers/cluster_helpers.rb
+++ b/spec/support/migrations_helpers/cluster_helpers.rb
@@ -4,7 +4,7 @@ module MigrationHelpers
module ClusterHelpers
# Creates a list of cluster projects.
def create_cluster_project_list(quantity)
- group = namespaces_table.create(name: 'gitlab-org', path: 'gitlab-org')
+ group = namespaces_table.create!(name: 'gitlab-org', path: 'gitlab-org')
quantity.times do |id|
create_cluster_project(group, id)
@@ -25,14 +25,14 @@ module MigrationHelpers
namespace_id: group.id
)
- cluster = clusters_table.create(
+ cluster = clusters_table.create!(
name: 'test-cluster',
cluster_type: 3,
provider_type: :gcp,
platform_type: :kubernetes
)
- cluster_projects_table.create(project_id: project.id, cluster_id: cluster.id)
+ cluster_projects_table.create!(project_id: project.id, cluster_id: cluster.id)
provider_gcp_table.create!(
gcp_project_id: "test-gcp-project-#{id}",
@@ -43,7 +43,7 @@ module MigrationHelpers
zone: 'us-central1-a'
)
- platform_kubernetes_table.create(
+ platform_kubernetes_table.create!(
cluster_id: cluster.id,
api_url: 'https://kubernetes.example.com',
encrypted_token: 'a' * 40,
@@ -58,7 +58,7 @@ module MigrationHelpers
project = projects_table.find(cluster_project.project_id)
namespace = "#{project.path}-#{project.id}"
- cluster_kubernetes_namespaces_table.create(
+ cluster_kubernetes_namespaces_table.create!(
cluster_project_id: cluster_project.id,
cluster_id: cluster.id,
project_id: cluster_project.project_id,
diff --git a/spec/support/migrations_helpers/namespaces_helper.rb b/spec/support/migrations_helpers/namespaces_helper.rb
index 4ca01c87568..c62ef6a4620 100644
--- a/spec/support/migrations_helpers/namespaces_helper.rb
+++ b/spec/support/migrations_helpers/namespaces_helper.rb
@@ -3,7 +3,7 @@
module MigrationHelpers
module NamespacesHelpers
def create_namespace(name, visibility, options = {})
- table(:namespaces).create({
+ table(:namespaces).create!({
name: name,
path: name,
type: 'Group',
diff --git a/spec/support/migrations_helpers/schema_version_finder.rb b/spec/support/migrations_helpers/schema_version_finder.rb
new file mode 100644
index 00000000000..b677db7ea26
--- /dev/null
+++ b/spec/support/migrations_helpers/schema_version_finder.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+# Sometimes data migration specs require adding invalid test data in order to test
+# the migration (e.g. adding a row with null foreign key). Certain db migrations that
+# add constraints (e.g. NOT NULL constraint) prevent invalid records from being added
+# and data migration from being tested. For this reason, SchemaVersionFinder can be used
+# to find and use schema prior to specified one.
+#
+# @example
+# RSpec.describe CleanupThings, :migration, schema: MigrationHelpers::SchemaVersionFinder.migration_prior(AddNotNullConstraint) do ...
+#
+# SchemaVersionFinder returns schema version prior to the one specified, which allows to then add
+# invalid records to the database, which in return allows to properly test data migration.
+module MigrationHelpers
+ class SchemaVersionFinder
+ def self.migrations_paths
+ ActiveRecord::Migrator.migrations_paths
+ end
+
+ def self.migration_context
+ ActiveRecord::MigrationContext.new(migrations_paths, ActiveRecord::SchemaMigration)
+ end
+
+ def self.migrations
+ migration_context.migrations
+ end
+
+ def self.migration_prior(migration_klass)
+ migrations.each_cons(2) do |previous, migration|
+ break previous.version if migration.name == migration_klass.name
+ end
+ end
+ end
+end
diff --git a/spec/support/models/merge_request_without_merge_request_diff.rb b/spec/support/models/merge_request_without_merge_request_diff.rb
new file mode 100644
index 00000000000..5cdf1feb7a5
--- /dev/null
+++ b/spec/support/models/merge_request_without_merge_request_diff.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class MergeRequestWithoutMergeRequestDiff < ::MergeRequest
+ self.inheritance_column = :_type_disabled
+
+ def ensure_merge_request_diff; end
+end
diff --git a/spec/support/shared_contexts/cache_allowed_users_in_namespace_shared_context.rb b/spec/support/shared_contexts/cache_allowed_users_in_namespace_shared_context.rb
index e3c1d0afa53..bfb719fd840 100644
--- a/spec/support/shared_contexts/cache_allowed_users_in_namespace_shared_context.rb
+++ b/spec/support/shared_contexts/cache_allowed_users_in_namespace_shared_context.rb
@@ -25,7 +25,7 @@ RSpec.shared_examples 'allowed user IDs are cached' do
expect(described_class.l1_cache_backend).to receive(:fetch).and_call_original
expect(described_class.l2_cache_backend).to receive(:fetch).and_call_original
expect(subject).to be_truthy
- end.not_to exceed_query_limit(2)
+ end.not_to exceed_query_limit(3)
end
end
end
diff --git a/spec/support/shared_contexts/email_shared_context.rb b/spec/support/shared_contexts/email_shared_context.rb
index b4d7722f03d..298e03162c4 100644
--- a/spec/support/shared_contexts/email_shared_context.rb
+++ b/spec/support/shared_contexts/email_shared_context.rb
@@ -21,7 +21,7 @@ end
RSpec.shared_examples :reply_processing_shared_examples do
context "when the user could not be found" do
before do
- user.destroy
+ user.destroy!
end
it "raises a UserNotFoundError" do
diff --git a/spec/support/shared_contexts/finders/group_projects_finder_shared_contexts.rb b/spec/support/shared_contexts/finders/group_projects_finder_shared_contexts.rb
index 58ee48a98f1..2b6edb4c07d 100644
--- a/spec/support/shared_contexts/finders/group_projects_finder_shared_contexts.rb
+++ b/spec/support/shared_contexts/finders/group_projects_finder_shared_contexts.rb
@@ -18,8 +18,8 @@ RSpec.shared_context 'GroupProjectsFinder context' do
let!(:subgroup_private_project) { create(:project, :private, path: '7', group: subgroup) }
before do
- shared_project_1.project_group_links.create(group_access: Gitlab::Access::MAINTAINER, group: group)
- shared_project_2.project_group_links.create(group_access: Gitlab::Access::MAINTAINER, group: group)
- shared_project_3.project_group_links.create(group_access: Gitlab::Access::MAINTAINER, group: group)
+ shared_project_1.project_group_links.create!(group_access: Gitlab::Access::MAINTAINER, group: group)
+ shared_project_2.project_group_links.create!(group_access: Gitlab::Access::MAINTAINER, group: group)
+ shared_project_3.project_group_links.create!(group_access: Gitlab::Access::MAINTAINER, group: group)
end
end
diff --git a/spec/support/shared_contexts/lib/gitlab/import_export/relation_tree_restorer_shared_context.rb b/spec/support/shared_contexts/lib/gitlab/import_export/relation_tree_restorer_shared_context.rb
new file mode 100644
index 00000000000..6b9ddc70691
--- /dev/null
+++ b/spec/support/shared_contexts/lib/gitlab/import_export/relation_tree_restorer_shared_context.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+RSpec.shared_context 'relation tree restorer shared context' do
+ include ImportExport::CommonUtil
+
+ let(:user) { create(:user) }
+ let(:shared) { Gitlab::ImportExport::Shared.new(importable) }
+ let(:attributes) { relation_reader.consume_attributes(importable_name) }
+
+ let(:members_mapper) do
+ Gitlab::ImportExport::MembersMapper.new(exported_members: {}, user: user, importable: importable)
+ end
+end
diff --git a/spec/support/shared_contexts/mailers/notify_shared_context.rb b/spec/support/shared_contexts/mailers/notify_shared_context.rb
index de8c0d5d2b4..4b7d028410a 100644
--- a/spec/support/shared_contexts/mailers/notify_shared_context.rb
+++ b/spec/support/shared_contexts/mailers/notify_shared_context.rb
@@ -11,7 +11,7 @@ RSpec.shared_context 'gitlab email notification' do
let(:new_user_address) { 'newguy@example.com' }
before do
- email = recipient.emails.create(email: "notifications@example.com")
+ email = recipient.emails.create!(email: "notifications@example.com")
recipient.update_attribute(:notification_email, email.email)
stub_incoming_email_setting(enabled: true, address: "reply+%{key}@#{Gitlab.config.gitlab.host}")
end
diff --git a/spec/support/shared_contexts/navbar_structure_context.rb b/spec/support/shared_contexts/navbar_structure_context.rb
index 747358fc1e0..9ebfdcb9522 100644
--- a/spec/support/shared_contexts/navbar_structure_context.rb
+++ b/spec/support/shared_contexts/navbar_structure_context.rb
@@ -44,7 +44,8 @@ RSpec.shared_context 'project navbar structure' do
_('Boards'),
_('Labels'),
_('Service Desk'),
- _('Milestones')
+ _('Milestones'),
+ (_('Iterations') if Gitlab.ee?)
]
},
{
@@ -64,14 +65,16 @@ RSpec.shared_context 'project navbar structure' do
nav_item: _('Operations'),
nav_sub_items: [
_('Metrics'),
+ _('Logs'),
+ _('Tracing'),
+ _('Error Tracking'),
_('Alerts'),
_('Incidents'),
- _('Environments'),
- _('Error Tracking'),
- _('Product Analytics'),
_('Serverless'),
- _('Logs'),
- _('Kubernetes')
+ _('Kubernetes'),
+ _('Environments'),
+ _('Feature Flags'),
+ _('Product Analytics')
]
},
analytics_nav_item,
diff --git a/spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb b/spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb
index 113252a6ab5..84910d0dfe4 100644
--- a/spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb
+++ b/spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb
@@ -85,7 +85,7 @@ RSpec.shared_examples 'multiple issue boards' do
wait_for_requests
- expect(page).to have_selector('.board', count: 3)
+ expect(page).to have_selector('.board', count: 5)
in_boards_switcher_dropdown do
click_link board.name
@@ -93,7 +93,7 @@ RSpec.shared_examples 'multiple issue boards' do
wait_for_requests
- expect(page).to have_selector('.board', count: 2)
+ expect(page).to have_selector('.board', count: 4)
end
it 'maintains sidebar state over board switch' do
diff --git a/spec/support/shared_examples/controllers/access_tokens_controller_shared_examples.rb b/spec/support/shared_examples/controllers/access_tokens_controller_shared_examples.rb
new file mode 100644
index 00000000000..54d41f9a68c
--- /dev/null
+++ b/spec/support/shared_examples/controllers/access_tokens_controller_shared_examples.rb
@@ -0,0 +1,106 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'project access tokens available #index' do
+ let_it_be(:active_project_access_token) { create(:personal_access_token, user: bot_user) }
+ let_it_be(:inactive_project_access_token) { create(:personal_access_token, :revoked, user: bot_user) }
+
+ it 'retrieves active project access tokens' do
+ subject
+
+ expect(assigns(:active_project_access_tokens)).to contain_exactly(active_project_access_token)
+ end
+
+ it 'retrieves inactive project access tokens' do
+ subject
+
+ expect(assigns(:inactive_project_access_tokens)).to contain_exactly(inactive_project_access_token)
+ end
+
+ it 'lists all available scopes' do
+ subject
+
+ expect(assigns(:scopes)).to eq(Gitlab::Auth.resource_bot_scopes)
+ end
+
+ it 'retrieves newly created personal access token value' do
+ token_value = 'random-value'
+ allow(PersonalAccessToken).to receive(:redis_getdel).with("#{user.id}:#{project.id}").and_return(token_value)
+
+ subject
+
+ expect(assigns(:new_project_access_token)).to eq(token_value)
+ end
+end
+
+RSpec.shared_examples 'project access tokens available #create' do
+ def created_token
+ PersonalAccessToken.order(:created_at).last
+ end
+
+ it 'returns success message' do
+ subject
+
+ expect(response.flash[:notice]).to match('Your new project access token has been created.')
+ end
+
+ it 'creates project access token' do
+ subject
+
+ expect(created_token.name).to eq(access_token_params[:name])
+ expect(created_token.scopes).to eq(access_token_params[:scopes])
+ expect(created_token.expires_at).to eq(access_token_params[:expires_at])
+ end
+
+ it 'creates project bot user' do
+ subject
+
+ expect(created_token.user).to be_project_bot
+ end
+
+ it 'stores newly created token redis store' do
+ expect(PersonalAccessToken).to receive(:redis_store!)
+
+ subject
+ end
+
+ it { expect { subject }.to change { User.count }.by(1) }
+ it { expect { subject }.to change { PersonalAccessToken.count }.by(1) }
+
+ context 'when unsuccessful' do
+ before do
+ allow_next_instance_of(ResourceAccessTokens::CreateService) do |service|
+ allow(service).to receive(:execute).and_return ServiceResponse.error(message: 'Failed!')
+ end
+ end
+
+ it { expect(subject).to render_template(:index) }
+ end
+end
+
+RSpec.shared_examples 'project access tokens available #revoke' do
+ it 'calls delete user worker' do
+ expect(DeleteUserWorker).to receive(:perform_async).with(user.id, bot_user.id, skip_authorization: true)
+
+ subject
+ end
+
+ it 'removes membership of bot user' do
+ subject
+
+ expect(project.reload.bots).not_to include(bot_user)
+ end
+
+ it 'converts issuables of the bot user to ghost user' do
+ issue = create(:issue, author: bot_user)
+
+ subject
+
+ expect(issue.reload.author.ghost?).to be true
+ end
+
+ it 'deletes project bot user' do
+ subject
+
+ expect(User.exists?(bot_user.id)).to be_falsy
+ end
+end
diff --git a/spec/support/shared_examples/controllers/cache_control_shared_examples.rb b/spec/support/shared_examples/controllers/cache_control_shared_examples.rb
index 426d7f95222..5496e04e26c 100644
--- a/spec/support/shared_examples/controllers/cache_control_shared_examples.rb
+++ b/spec/support/shared_examples/controllers/cache_control_shared_examples.rb
@@ -2,7 +2,7 @@
RSpec.shared_examples 'project cache control headers' do
before do
- project.update(visibility_level: visibility_level)
+ project.update!(visibility_level: visibility_level)
end
context 'when project is public' do
diff --git a/spec/support/shared_examples/controllers/destroy_hook_shared_examples.rb b/spec/support/shared_examples/controllers/destroy_hook_shared_examples.rb
new file mode 100644
index 00000000000..710aa333dec
--- /dev/null
+++ b/spec/support/shared_examples/controllers/destroy_hook_shared_examples.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'Web hook destroyer' do
+ it 'displays a message about synchronous delete', :aggregate_failures do
+ expect_next_instance_of(WebHooks::DestroyService) do |instance|
+ expect(instance).to receive(:execute).with(anything).and_call_original
+ end
+
+ delete :destroy, params: params
+
+ expect(response).to have_gitlab_http_status(:found)
+ expect(flash[:notice]).to eq("#{hook.model_name.human} was deleted")
+ end
+
+ it 'displays a message about async delete', :aggregate_failures do
+ expect_next_instance_of(WebHooks::DestroyService) do |instance|
+ expect(instance).to receive(:execute).with(anything).and_return({ status: :success, async: true } )
+ end
+
+ delete :destroy, params: params
+
+ expect(response).to have_gitlab_http_status(:found)
+ expect(flash[:notice]).to eq("#{hook.model_name.human} was scheduled for deletion")
+ end
+
+ it 'displays an error if deletion failed', :aggregate_failures do
+ expect_next_instance_of(WebHooks::DestroyService) do |instance|
+ expect(instance).to receive(:execute).with(anything).and_return({ status: :error, async: true, message: "failed" } )
+ end
+
+ delete :destroy, params: params
+
+ expect(response).to have_gitlab_http_status(:found)
+ expect(flash[:alert]).to eq("failed")
+ end
+end
diff --git a/spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb b/spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb
index 8bc91f72b8c..2fcc88ef36a 100644
--- a/spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb
+++ b/spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb
@@ -262,7 +262,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do
context "when the namespace is owned by the GitLab user" do
before do
user.username = other_username
- user.save
+ user.save!
end
it "takes the existing namespace" do
diff --git a/spec/support/shared_examples/controllers/known_sign_in_shared_examples.rb b/spec/support/shared_examples/controllers/known_sign_in_shared_examples.rb
index 7f26155f9d6..3f147f942ba 100644
--- a/spec/support/shared_examples/controllers/known_sign_in_shared_examples.rb
+++ b/spec/support/shared_examples/controllers/known_sign_in_shared_examples.rb
@@ -59,7 +59,7 @@ RSpec.shared_examples 'known sign in' do
it 'notifies the user when the cookie is expired' do
stub_cookie
- Timecop.freeze((KnownSignIn::KNOWN_SIGN_IN_COOKIE_EXPIRY + 1.day).from_now) do
+ travel_to((KnownSignIn::KNOWN_SIGN_IN_COOKIE_EXPIRY + 1.day).from_now) do
expect_next_instance_of(NotificationService) do |instance|
expect(instance).to receive(:unknown_sign_in)
end
diff --git a/spec/support/shared_examples/controllers/milestone_tabs_shared_examples.rb b/spec/support/shared_examples/controllers/milestone_tabs_shared_examples.rb
index 925c45005f0..2d35b1681ea 100644
--- a/spec/support/shared_examples/controllers/milestone_tabs_shared_examples.rb
+++ b/spec/support/shared_examples/controllers/milestone_tabs_shared_examples.rb
@@ -2,9 +2,28 @@
RSpec.shared_examples 'milestone tabs' do
def go(path, extra_params = {})
- params = { namespace_id: project.namespace.to_param, project_id: project, id: milestone.iid }
+ get path, params: request_params.merge(extra_params)
+ end
+
+ describe '#issues' do
+ context 'as html' do
+ before do
+ go(:issues, format: 'html')
+ end
- get path, params: params.merge(extra_params)
+ it 'redirects to milestone#show' do
+ expect(response).to redirect_to(milestone_path)
+ end
+ end
+
+ context 'as json' do
+ it 'renders the issues tab template to a string' do
+ go(:issues, format: 'json')
+
+ expect(response).to render_template('shared/milestones/_issues_tab')
+ expect(json_response).to have_key('html')
+ end
+ end
end
describe '#merge_requests' do
diff --git a/spec/support/shared_examples/controllers/sessionless_auth_controller_shared_examples.rb b/spec/support/shared_examples/controllers/sessionless_auth_controller_shared_examples.rb
index f2a97a86df6..b67eb0d99fd 100644
--- a/spec/support/shared_examples/controllers/sessionless_auth_controller_shared_examples.rb
+++ b/spec/support/shared_examples/controllers/sessionless_auth_controller_shared_examples.rb
@@ -44,7 +44,7 @@ RSpec.shared_examples 'authenticates sessionless user' do |path, format, params|
.to increment(:user_unauthenticated_counter)
end
- personal_access_token.update(scopes: [:read_user])
+ personal_access_token.update!(scopes: [:read_user])
get path, params: default_params.merge(private_token: personal_access_token.token)
diff --git a/spec/support/shared_examples/controllers/unique_hll_events_examples.rb b/spec/support/shared_examples/controllers/unique_hll_events_examples.rb
index 7e5a225f020..cf7ee17ea13 100644
--- a/spec/support/shared_examples/controllers/unique_hll_events_examples.rb
+++ b/spec/support/shared_examples/controllers/unique_hll_events_examples.rb
@@ -1,47 +1,24 @@
# frozen_string_literal: true
+#
+# Requires a context containing:
+# - request
+# - expected_type
+# - target_id
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)
- 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
+ request
end
- context 'when format is JSON' do
- let(:format) { :json }
+ context 'when feature flag is disabled' do
+ it 'does not track unique event' do
+ stub_feature_flags(feature_flag => false)
- 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)
+ expect(Gitlab::UsageDataCounters::HLLRedisCounter).not_to receive(:track_event)
- subject
+ request
end
end
end
diff --git a/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb b/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb
index 4ca400dd87b..a6ad8fc594c 100644
--- a/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb
+++ b/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb
@@ -15,10 +15,10 @@ RSpec.shared_examples 'wiki controller actions' do
end
describe 'GET #new' do
- subject { get :new, params: routing_params }
+ subject(:request) { get :new, params: routing_params }
it 'redirects to #show and appends a `random_title` param' do
- subject
+ request
expect(response).to be_redirect
expect(response.redirect_url).to match(%r{
@@ -35,7 +35,7 @@ RSpec.shared_examples 'wiki controller actions' do
end
it 'redirects to the wiki container and displays an error message' do
- subject
+ request
expect(response).to redirect_to(container)
expect(flash[:notice]).to eq('Could not create Wiki Repository at this time. Please try again later.')
@@ -146,13 +146,13 @@ RSpec.shared_examples 'wiki controller actions' do
let(:random_title) { nil }
- subject { get :show, params: routing_params.merge(id: id, random_title: random_title) }
+ subject(:request) { get :show, params: routing_params.merge(id: id, random_title: random_title) }
context 'when page exists' do
let(:id) { wiki_title }
it 'renders the page' do
- subject
+ request
expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template('shared/wikis/show')
@@ -161,19 +161,26 @@ RSpec.shared_examples 'wiki controller actions' do
expect(assigns(:sidebar_limited)).to be(false)
end
- it 'increases the page view counter' do
- expect do
- subject
+ context 'page view tracking' do
+ it_behaves_like 'tracking unique hll events', :track_unique_wiki_page_views do
+ let(:target_id) { 'wiki_action' }
+ let(:expected_type) { instance_of(String) }
+ end
- expect(response).to have_gitlab_http_status(:ok)
- end.to change { Gitlab::UsageDataCounters::WikiPageCounter.read(:view) }.by(1)
+ it 'increases the page view counter' do
+ expect do
+ request
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end.to change { Gitlab::UsageDataCounters::WikiPageCounter.read(:view) }.by(1)
+ end
end
context 'when page content encoding is invalid' do
it 'sets flash error' do
allow(controller).to receive(:valid_encoding?).and_return(false)
- subject
+ request
expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template('shared/wikis/show')
@@ -187,7 +194,7 @@ RSpec.shared_examples 'wiki controller actions' do
context 'when the user can create pages' do
before do
- subject
+ request
expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template('shared/wikis/edit')
@@ -212,7 +219,7 @@ RSpec.shared_examples 'wiki controller actions' do
end
it 'shows the empty state' do
- subject
+ request
expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template('shared/wikis/empty')
@@ -226,10 +233,10 @@ RSpec.shared_examples 'wiki controller actions' do
where(:file_name) { ['dk.png', 'unsanitized.svg', 'git-cheat-sheet.pdf'] }
with_them do
- let(:id) { upload_file_to_wiki(container, user, file_name) }
+ let(:id) { upload_file_to_wiki(wiki, user, file_name) }
it 'delivers the file with the correct headers' do
- subject
+ request
expect(response).to have_gitlab_http_status(:ok)
expect(response.headers['Content-Disposition']).to match(/^inline/)
@@ -255,7 +262,7 @@ RSpec.shared_examples 'wiki controller actions' do
let(:id_param) { 'invalid' }
it 'redirects to show' do
- subject
+ request
expect(response).to redirect_to_wiki(wiki, 'invalid')
end
@@ -265,7 +272,7 @@ RSpec.shared_examples 'wiki controller actions' do
let(:id_param) { ' ' }
it 'redirects to the home page' do
- subject
+ request
expect(response).to redirect_to_wiki(wiki, 'home')
end
@@ -275,7 +282,7 @@ RSpec.shared_examples 'wiki controller actions' do
it 'redirects to show' do
allow(controller).to receive(:valid_encoding?).and_return(false)
- subject
+ request
expect(response).to redirect_to_wiki(wiki, wiki.list_pages.first)
end
@@ -288,7 +295,7 @@ RSpec.shared_examples 'wiki controller actions' do
allow(page).to receive(:content).and_return(nil)
allow(controller).to receive(:page).and_return(page)
- subject
+ request
expect(response).to redirect_to_wiki(wiki, page)
end
@@ -298,7 +305,7 @@ RSpec.shared_examples 'wiki controller actions' do
describe 'GET #edit' do
let(:id_param) { wiki_title }
- subject { get(:edit, params: routing_params.merge(id: id_param)) }
+ subject(:request) { get(:edit, params: routing_params.merge(id: id_param)) }
it_behaves_like 'edit action'
@@ -306,7 +313,7 @@ RSpec.shared_examples 'wiki controller actions' do
render_views
it 'shows the edit page' do
- subject
+ request
expect(response).to have_gitlab_http_status(:ok)
expect(response.body).to include(s_('Wiki|Edit Page'))
@@ -319,7 +326,7 @@ RSpec.shared_examples 'wiki controller actions' do
let(:new_content) { 'New content' }
let(:id_param) { wiki_title }
- subject do
+ subject(:request) do
patch(:update,
params: routing_params.merge(
id: id_param,
@@ -333,7 +340,7 @@ RSpec.shared_examples 'wiki controller actions' do
render_views
it 'updates the page' do
- subject
+ request
wiki_page = wiki.list_pages(load_content: true).first
@@ -348,7 +355,7 @@ RSpec.shared_examples 'wiki controller actions' do
end
it 'renders the empty state' do
- subject
+ request
expect(response).to render_template('shared/wikis/empty')
end
@@ -359,7 +366,7 @@ RSpec.shared_examples 'wiki controller actions' do
let(:new_title) { 'New title' }
let(:new_content) { 'New content' }
- subject do
+ subject(:request) do
post(:create,
params: routing_params.merge(
wiki: { title: new_title, content: new_content }
@@ -369,7 +376,7 @@ RSpec.shared_examples 'wiki controller actions' do
context 'when page is valid' do
it 'creates the page' do
expect do
- subject
+ request
end.to change { wiki.list_pages.size }.by 1
wiki_page = wiki.find_page(new_title)
@@ -384,7 +391,7 @@ RSpec.shared_examples 'wiki controller actions' do
it 'renders the edit state' do
expect do
- subject
+ request
end.not_to change { wiki.list_pages.size }
expect(response).to render_template('shared/wikis/edit')
@@ -395,7 +402,7 @@ RSpec.shared_examples 'wiki controller actions' do
describe 'DELETE #destroy' do
let(:id_param) { wiki_title }
- subject do
+ subject(:request) do
delete(:destroy,
params: routing_params.merge(
id: id_param
@@ -405,7 +412,7 @@ RSpec.shared_examples 'wiki controller actions' do
context 'when page exists' do
it 'deletes the page' do
expect do
- subject
+ request
end.to change { wiki.list_pages.size }.by(-1)
end
@@ -418,7 +425,7 @@ RSpec.shared_examples 'wiki controller actions' do
it 'renders the edit state' do
expect do
- subject
+ request
end.not_to change { wiki.list_pages.size }
expect(response).to render_template('shared/wikis/edit')
@@ -432,7 +439,7 @@ RSpec.shared_examples 'wiki controller actions' do
it 'renders 404' do
expect do
- subject
+ request
end.not_to change { wiki.list_pages.size }
expect(response).to have_gitlab_http_status(:not_found)
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 c9910487798..2fff4137934 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
@@ -11,6 +11,15 @@ RSpec.shared_examples 'an editable merge request' do
expect(page).to have_content user.name
end
+ 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
+
click_button 'Milestone'
page.within '.issue-milestone' do
click_link milestone.title
@@ -38,6 +47,10 @@ RSpec.shared_examples 'an editable merge request' do
expect(page).to have_content user.name
end
+ page.within '.reviewer' do
+ expect(page).to have_content user.name
+ end
+
page.within '.milestone' do
expect(page).to have_content milestone.title
end
@@ -69,7 +82,7 @@ RSpec.shared_examples 'an editable merge request' do
end
it 'warns about version conflict' do
- merge_request.update(title: "New title")
+ merge_request.update!(title: "New title")
fill_in 'merge_request_title', with: 'bug 345'
fill_in 'merge_request_description', with: 'bug description'
@@ -124,16 +137,3 @@ 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/issuable_invite_members_shared_examples.rb b/spec/support/shared_examples/features/issuable_invite_members_shared_examples.rb
new file mode 100644
index 00000000000..ac1cc2da7e3
--- /dev/null
+++ b/spec/support/shared_examples/features/issuable_invite_members_shared_examples.rb
@@ -0,0 +1,68 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'issuable invite members experiments' do
+ context 'when invite_members_version_a experiment is enabled' do
+ before do
+ stub_experiment_for_user(invite_members_version_a: true)
+ end
+
+ it 'shows a link for inviting members and follows through to the members page' do
+ project.add_maintainer(user)
+ visit issuable_path
+
+ find('.block.assignee .edit-link').click
+
+ wait_for_requests
+
+ page.within '.dropdown-menu-user' do
+ expect(page).to have_link('Invite Members', href: project_project_members_path(project))
+ expect(page).to have_selector('[data-track-event="click_invite_members"]')
+ expect(page).to have_selector('[data-track-label="edit_assignee"]')
+ end
+
+ click_link 'Invite Members'
+
+ expect(current_path).to eq project_project_members_path(project)
+ end
+ end
+
+ context 'when invite_members_version_b experiment is enabled' do
+ before do
+ stub_experiment_for_user(invite_members_version_b: true)
+ end
+
+ it 'shows a link for inviting members and follows through to modal' do
+ project.add_developer(user)
+ visit issuable_path
+
+ find('.block.assignee .edit-link').click
+
+ wait_for_requests
+
+ page.within '.dropdown-menu-user' do
+ expect(page).to have_link('Invite Members', href: '#')
+ expect(page).to have_selector('[data-track-event="click_invite_members_version_b"]')
+ expect(page).to have_selector('[data-track-label="edit_assignee"]')
+ end
+
+ click_link 'Invite Members'
+
+ expect(page).to have_content("Oops, this feature isn't ready yet")
+ end
+ end
+
+ context 'when no invite members experiments are enabled' do
+ it 'shows author in assignee dropdown and no invite link' do
+ project.add_maintainer(user)
+ visit issuable_path
+
+ find('.block.assignee .edit-link').click
+
+ wait_for_requests
+
+ page.within '.dropdown-menu-user' do
+ expect(page).not_to have_link('Invite Members')
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/features/multiple_assignees_mr_shared_examples.rb b/spec/support/shared_examples/features/multiple_assignees_mr_shared_examples.rb
index 19a5750cf6d..9d023d9514a 100644
--- a/spec/support/shared_examples/features/multiple_assignees_mr_shared_examples.rb
+++ b/spec/support/shared_examples/features/multiple_assignees_mr_shared_examples.rb
@@ -38,7 +38,7 @@ RSpec.shared_examples 'multiple assignees merge request' do |action, save_button
page.within '.issuable-sidebar' do
page.within '.assignee' do
# Closing dropdown to persist
- click_link 'Edit'
+ click_link 'Apply'
expect(page).to have_content user2.name
end
diff --git a/spec/support/shared_examples/features/multiple_reviewers_mr_shared_examples.rb b/spec/support/shared_examples/features/multiple_reviewers_mr_shared_examples.rb
new file mode 100644
index 00000000000..48cde90bd9b
--- /dev/null
+++ b/spec/support/shared_examples/features/multiple_reviewers_mr_shared_examples.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'multiple reviewers merge request' do |action, save_button_title|
+ it "#{action} a MR with multiple reviewers", :js do
+ find('.js-reviewer-search').click
+ page.within '.dropdown-menu-user' do
+ click_link user.name
+ click_link user2.name
+ end
+
+ # Extra click needed in order to toggle the dropdown
+ find('.js-reviewer-search').click
+
+ expect(all('input[name="merge_request[reviewer_ids][]"]', visible: false).map(&:value))
+ .to match_array([user.id.to_s, user2.id.to_s])
+
+ page.within '.js-reviewer-search' do
+ expect(page).to have_content "#{user2.name} + 1 more"
+ end
+
+ click_button save_button_title
+
+ page.within '.issuable-sidebar' do
+ page.within '.reviewer' do
+ expect(page).to have_content '2 Reviewers'
+
+ click_link 'Edit'
+
+ expect(page).to have_content user.name
+ expect(page).to have_content user2.name
+ end
+ end
+
+ page.within '.dropdown-menu-user' do
+ click_link user.name
+ end
+
+ page.within '.issuable-sidebar' do
+ page.within '.reviewer' do
+ # Closing dropdown to persist
+ click_link 'Edit'
+
+ expect(page).to have_content user2.name
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/features/navbar_shared_examples.rb b/spec/support/shared_examples/features/navbar_shared_examples.rb
index 91a4048fa7c..c768e95c45a 100644
--- a/spec/support/shared_examples/features/navbar_shared_examples.rb
+++ b/spec/support/shared_examples/features/navbar_shared_examples.rb
@@ -3,12 +3,14 @@
RSpec.shared_examples 'verified navigation bar' do
let(:expected_structure) do
structure.compact!
- structure.each { |s| s[:nav_sub_items].compact! }
+ structure.each { |s| s[:nav_sub_items]&.compact! }
structure
end
it 'renders correctly' do
current_structure = page.all('.sidebar-top-level-items > li', class: ['!hidden']).map do |item|
+ next if item.find_all('a').empty?
+
nav_item = item.find_all('a').first.text.gsub(/\s+\d+$/, '') # remove counts at the end
nav_sub_items = item.all('.sidebar-sub-level-items > li', class: ['!fly-out-top-item']).map do |list_item|
@@ -16,7 +18,7 @@ RSpec.shared_examples 'verified navigation bar' do
end
{ nav_item: nav_item, nav_sub_items: nav_sub_items }
- end
+ end.compact
expect(current_structure).to eq(expected_structure)
end
diff --git a/spec/support/shared_examples/features/packages_shared_examples.rb b/spec/support/shared_examples/features/packages_shared_examples.rb
index f201421e827..4d2e13aa5bc 100644
--- a/spec/support/shared_examples/features/packages_shared_examples.rb
+++ b/spec/support/shared_examples/features/packages_shared_examples.rb
@@ -84,11 +84,11 @@ RSpec.shared_examples 'shared package sorting' do
let(:packages) { [package_two, package_one] }
end
- it_behaves_like 'correctly sorted packages list', 'Created' do
+ it_behaves_like 'correctly sorted packages list', 'Published' do
let(:packages) { [package_two, package_one] }
end
- it_behaves_like 'correctly sorted packages list', 'Created', ascending: true do
+ it_behaves_like 'correctly sorted packages list', 'Published', ascending: true do
let(:packages) { [package_one, package_two] }
end
end
diff --git a/spec/support/shared_examples/features/page_description_shared_examples.rb b/spec/support/shared_examples/features/page_description_shared_examples.rb
new file mode 100644
index 00000000000..81653220b4c
--- /dev/null
+++ b/spec/support/shared_examples/features/page_description_shared_examples.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'page meta description' do |expected_description|
+ it 'renders the page with description, og:description, and twitter:description meta tags that contains a plain-text version of the markdown', :aggregate_failures do
+ %w(name='description' property='og:description' property='twitter:description').each do |selector|
+ expect(page).to have_selector("meta[#{selector}][content='#{expected_description}']", visible: false)
+ end
+ end
+end
diff --git a/spec/support/shared_examples/features/protected_branches_with_deploy_keys_examples.rb b/spec/support/shared_examples/features/protected_branches_with_deploy_keys_examples.rb
new file mode 100644
index 00000000000..a2d2143271c
--- /dev/null
+++ b/spec/support/shared_examples/features/protected_branches_with_deploy_keys_examples.rb
@@ -0,0 +1,95 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'when the deploy_keys_on_protected_branches FF is turned on' do
+ before do
+ stub_feature_flags(deploy_keys_on_protected_branches: true)
+ project.add_maintainer(user)
+ sign_in(user)
+ end
+
+ let(:dropdown_sections_minus_deploy_keys) { all_dropdown_sections - ['Deploy Keys'] }
+
+ context 'when deploy keys are enabled to this project' do
+ let!(:deploy_key_1) { create(:deploy_key, title: 'title 1', projects: [project]) }
+ let!(:deploy_key_2) { create(:deploy_key, title: 'title 2', projects: [project]) }
+
+ context 'when only one deploy key can push' do
+ before do
+ deploy_key_1.deploy_keys_projects.first.update!(can_push: true)
+ end
+
+ it "shows all dropdown sections in the 'Allowed to push' main dropdown, with only one deploy key" do
+ visit project_protected_branches_path(project)
+
+ find(".js-allowed-to-push").click
+ wait_for_requests
+
+ within('.qa-allowed-to-push-dropdown') do
+ dropdown_headers = page.all('.dropdown-header').map(&:text)
+
+ expect(dropdown_headers).to contain_exactly(*all_dropdown_sections)
+ expect(page).to have_content('title 1')
+ expect(page).not_to have_content('title 2')
+ end
+ end
+
+ it "shows all sections but not deploy keys in the 'Allowed to merge' main dropdown" do
+ visit project_protected_branches_path(project)
+
+ find(".js-allowed-to-merge").click
+ wait_for_requests
+
+ within('.qa-allowed-to-merge-dropdown') do
+ dropdown_headers = page.all('.dropdown-header').map(&:text)
+
+ expect(dropdown_headers).to contain_exactly(*dropdown_sections_minus_deploy_keys)
+ end
+ end
+
+ it "shows all sections in the 'Allowed to push' update dropdown" do
+ create(:protected_branch, :no_one_can_push, project: project, name: 'master')
+
+ visit project_protected_branches_path(project)
+
+ within(".js-protected-branch-edit-form") do
+ find(".js-allowed-to-push").click
+ wait_for_requests
+
+ dropdown_headers = page.all('.dropdown-header').map(&:text)
+
+ expect(dropdown_headers).to contain_exactly(*all_dropdown_sections)
+ end
+ end
+ end
+
+ context 'when no deploy key can push' do
+ it "just shows all sections but not deploy keys in the 'Allowed to push' dropdown" do
+ visit project_protected_branches_path(project)
+
+ find(".js-allowed-to-push").click
+ wait_for_requests
+
+ within('.qa-allowed-to-push-dropdown') do
+ dropdown_headers = page.all('.dropdown-header').map(&:text)
+
+ expect(dropdown_headers).to contain_exactly(*dropdown_sections_minus_deploy_keys)
+ end
+ end
+
+ it "just shows all sections but not deploy keys in the 'Allowed to push' update dropdown" do
+ create(:protected_branch, :no_one_can_push, project: project, name: 'master')
+
+ visit project_protected_branches_path(project)
+
+ within(".js-protected-branch-edit-form") do
+ find(".js-allowed-to-push").click
+ wait_for_requests
+
+ dropdown_headers = page.all('.dropdown-header').map(&:text)
+
+ expect(dropdown_headers).to contain_exactly(*dropdown_sections_minus_deploy_keys)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/features/snippets_shared_examples.rb b/spec/support/shared_examples/features/snippets_shared_examples.rb
index 8d68b1e4c0a..bd1a67f3bb5 100644
--- a/spec/support/shared_examples/features/snippets_shared_examples.rb
+++ b/spec/support/shared_examples/features/snippets_shared_examples.rb
@@ -84,7 +84,7 @@ RSpec.shared_examples 'show and render proper snippet blob' do
expect(page).not_to have_selector('.js-blob-viewer-switcher')
# shows an enabled copy button
- expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)')
+ expect(page).to have_button('Copy file contents', disabled: false)
# shows a raw button
expect(page).to have_link('Open raw')
@@ -106,7 +106,6 @@ RSpec.shared_examples 'show and render proper snippet blob' do
it 'displays the blob using the rich viewer' do
aggregate_failures do
# hides the simple viewer
- expect(page).to have_selector('.blob-viewer[data-type="simple"]', visible: false)
expect(page).to have_selector('.blob-viewer[data-type="rich"]')
# shows rendered Markdown
@@ -116,7 +115,7 @@ RSpec.shared_examples 'show and render proper snippet blob' do
expect(page).to have_selector('.js-blob-viewer-switcher')
# shows a disabled copy button
- expect(page).to have_selector('.js-copy-blob-source-btn.disabled')
+ expect(page).to have_button('Copy file contents', disabled: true)
# shows a raw button
expect(page).to have_link('Open raw')
@@ -128,7 +127,7 @@ RSpec.shared_examples 'show and render proper snippet blob' do
context 'switching to the simple viewer' do
before do
- find('.js-blob-viewer-switch-btn[data-viewer=simple]').click
+ find_button('Display source').click
wait_for_requests
end
@@ -137,19 +136,18 @@ RSpec.shared_examples 'show and render proper snippet blob' do
aggregate_failures do
# hides the rich viewer
expect(page).to have_selector('.blob-viewer[data-type="simple"]')
- expect(page).to have_selector('.blob-viewer[data-type="rich"]', visible: false)
# shows highlighted Markdown code
expect(page).to have_content("[PEP-8](http://www.python.org/dev/peps/pep-0008/)")
# shows an enabled copy button
- expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)')
+ expect(page).to have_button('Copy file contents', disabled: false)
end
end
context 'switching to the rich viewer again' do
before do
- find('.js-blob-viewer-switch-btn[data-viewer=rich]').click
+ find_button('Display rendered file').click
wait_for_requests
end
@@ -157,11 +155,11 @@ RSpec.shared_examples 'show and render proper snippet blob' do
it 'displays the blob using the rich viewer' do
aggregate_failures do
# hides the simple viewer
- expect(page).to have_selector('.blob-viewer[data-type="simple"]', visible: false)
expect(page).to have_selector('.blob-viewer[data-type="rich"]')
- # shows an enabled copy button
- expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)')
+ # Used to show an enabled copy button since the code has already been fetched
+ # Will be resolved in https://gitlab.com/gitlab-org/gitlab/-/issues/262389
+ expect(page).to have_button('Copy file contents', disabled: true)
end
end
end
@@ -169,7 +167,8 @@ RSpec.shared_examples 'show and render proper snippet blob' do
end
context 'visiting with a line number anchor' do
- let(:anchor) { 'L1' }
+ # L1 used to work and will be revisited in https://gitlab.com/gitlab-org/gitlab/-/issues/262391
+ let(:anchor) { 'LC1' }
it 'displays the blob using the simple viewer' do
subject
@@ -177,7 +176,6 @@ RSpec.shared_examples 'show and render proper snippet blob' do
aggregate_failures do
# hides the rich viewer
expect(page).to have_selector('.blob-viewer[data-type="simple"]')
- expect(page).to have_selector('.blob-viewer[data-type="rich"]', visible: false)
# highlights the line in question
expect(page).to have_selector('#LC1.hll')
@@ -186,7 +184,7 @@ RSpec.shared_examples 'show and render proper snippet blob' do
expect(page).to have_content("[PEP-8](http://www.python.org/dev/peps/pep-0008/)")
# shows an enabled copy button
- expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)')
+ expect(page).to have_button('Copy file contents', disabled: false)
end
end
end
diff --git a/spec/support/shared_examples/features/wiki_file_attachments_shared_examples.rb b/spec/support/shared_examples/features/wiki/file_attachments_shared_examples.rb
index d30e8241da0..0ef1ccdfe57 100644
--- a/spec/support/shared_examples/features/wiki_file_attachments_shared_examples.rb
+++ b/spec/support/shared_examples/features/wiki/file_attachments_shared_examples.rb
@@ -1,14 +1,12 @@
# frozen_string_literal: true
# Requires a context containing:
-# project
+# wiki
RSpec.shared_examples 'wiki file attachments' do
include DropzoneHelper
context 'uploading attachments', :js do
- let(:wiki) { project.wiki }
-
def attach_with_dropzone(wait = false)
dropzone_file([Rails.root.join('spec', 'fixtures', 'dk.png')], 0, wait)
end
diff --git a/spec/support/shared_examples/features/wiki/user_creates_wiki_page_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_creates_wiki_page_shared_examples.rb
new file mode 100644
index 00000000000..44d82d2e753
--- /dev/null
+++ b/spec/support/shared_examples/features/wiki/user_creates_wiki_page_shared_examples.rb
@@ -0,0 +1,245 @@
+# frozen_string_literal: true
+
+# Requires a context containing:
+# wiki
+# user
+
+RSpec.shared_examples 'User creates wiki page' do
+ include WikiHelpers
+
+ before do
+ sign_in(user)
+ end
+
+ context "when wiki is empty" do
+ before do |example|
+ visit wiki_path(wiki)
+
+ wait_for_svg_to_be_loaded(example)
+
+ click_link "Create your first page"
+ end
+
+ it "shows validation error message" do
+ page.within(".wiki-form") do
+ fill_in(:wiki_content, with: "")
+
+ click_on("Create page")
+ end
+
+ expect(page).to have_content("The form contains the following error:").and have_content("Content can't be blank")
+
+ page.within(".wiki-form") do
+ fill_in(:wiki_content, with: "[link test](test)")
+
+ click_on("Create page")
+ end
+
+ expect(page).to have_content("Home").and have_content("link test")
+
+ click_link("link test")
+
+ expect(page).to have_content("Create New Page")
+ end
+
+ it "shows non-escaped link in the pages list" do
+ fill_in(:wiki_title, with: "one/two/three-test")
+
+ page.within(".wiki-form") do
+ fill_in(:wiki_content, with: "wiki content")
+
+ click_on("Create page")
+ end
+
+ expect(current_path).to include("one/two/three-test")
+ expect(page).to have_link(href: wiki_page_path(wiki, 'one/two/three-test'))
+ end
+
+ it "has `Create home` as a commit message", :js do
+ wait_for_requests
+
+ expect(page).to have_field("wiki[message]", with: "Create home")
+ end
+
+ it "creates a page from the home page" do
+ fill_in(:wiki_content, with: "[test](test)\n[GitLab API doc](api)\n[Rake tasks](raketasks)\n# Wiki header\n")
+ fill_in(:wiki_message, with: "Adding links to wiki")
+
+ page.within(".wiki-form") do
+ click_button("Create page")
+ end
+
+ expect(current_path).to eq(wiki_page_path(wiki, "home"))
+ expect(page).to have_content("test GitLab API doc Rake tasks Wiki header")
+ .and have_content("Home")
+ .and have_content("Last edited by #{user.name}")
+ .and have_header_with_correct_id_and_link(1, "Wiki header", "wiki-header")
+
+ click_link("test")
+
+ expect(current_path).to eq(wiki_page_path(wiki, "test"))
+
+ page.within(:css, ".nav-text") do
+ expect(page).to have_content("Create New Page")
+ end
+
+ click_link("Home")
+
+ expect(current_path).to eq(wiki_page_path(wiki, "home"))
+
+ click_link("GitLab API")
+
+ expect(current_path).to eq(wiki_page_path(wiki, "api"))
+
+ page.within(:css, ".nav-text") do
+ expect(page).to have_content("Create")
+ end
+
+ click_link("Home")
+
+ expect(current_path).to eq(wiki_page_path(wiki, "home"))
+
+ click_link("Rake tasks")
+
+ expect(current_path).to eq(wiki_page_path(wiki, "raketasks"))
+
+ page.within(:css, ".nav-text") do
+ expect(page).to have_content("Create")
+ end
+ end
+
+ it "creates ASCII wiki with LaTeX blocks", :js do
+ stub_application_setting(plantuml_url: "http://localhost", plantuml_enabled: true)
+
+ ascii_content = <<~MD
+ :stem: latexmath
+
+ [stem]
+ ++++
+ \\sqrt{4} = 2
+ ++++
+
+ another part
+
+ [latexmath]
+ ++++
+ \\beta_x \\gamma
+ ++++
+
+ stem:[2+2] is 4
+ MD
+
+ find("#wiki_format option[value=asciidoc]").select_option
+
+ fill_in(:wiki_content, with: ascii_content)
+
+ page.within(".wiki-form") do
+ click_button("Create page")
+ end
+
+ page.within ".md" do
+ expect(page).to have_selector(".katex", count: 3).and have_content("2+2 is 4")
+ end
+ end
+
+ it 'creates a wiki page with Org markup', :aggregate_failures do
+ org_content = <<~ORG
+ * Heading
+ ** Subheading
+ [[home][Link to Home]]
+ ORG
+
+ page.within('.wiki-form') do
+ find('#wiki_format option[value=org]').select_option
+ fill_in(:wiki_content, with: org_content)
+ click_button('Create page')
+ end
+
+ expect(page).to have_selector('h1', text: 'Heading')
+ expect(page).to have_selector('h2', text: 'Subheading')
+ expect(page).to have_link(href: wiki_page_path(wiki, 'home'))
+ end
+
+ it_behaves_like 'wiki file attachments'
+ end
+
+ context "when wiki is not empty", :js do
+ before do
+ create(:wiki_page, wiki: wiki, title: 'home', content: 'Home page')
+
+ visit wiki_path(wiki)
+ end
+
+ context "via the `new wiki page` page" do
+ it "creates a page with a single word" do
+ click_link("New page")
+
+ page.within(".wiki-form") do
+ fill_in(:wiki_title, with: "foo")
+ fill_in(:wiki_content, with: "My awesome wiki!")
+ end
+
+ # Commit message field should have correct value.
+ expect(page).to have_field("wiki[message]", with: "Create foo")
+
+ click_button("Create page")
+
+ expect(page).to have_content("foo")
+ .and have_content("Last edited by #{user.name}")
+ .and have_content("My awesome wiki!")
+ end
+
+ it "creates a page with spaces in the name" do
+ click_link("New page")
+
+ page.within(".wiki-form") do
+ fill_in(:wiki_title, with: "Spaces in the name")
+ fill_in(:wiki_content, with: "My awesome wiki!")
+ end
+
+ # Commit message field should have correct value.
+ expect(page).to have_field("wiki[message]", with: "Create Spaces in the name")
+
+ click_button("Create page")
+
+ expect(page).to have_content("Spaces in the name")
+ .and have_content("Last edited by #{user.name}")
+ .and have_content("My awesome wiki!")
+ end
+
+ it "creates a page with hyphens in the name" do
+ click_link("New page")
+
+ page.within(".wiki-form") do
+ fill_in(:wiki_title, with: "hyphens-in-the-name")
+ fill_in(:wiki_content, with: "My awesome wiki!")
+ end
+
+ # Commit message field should have correct value.
+ expect(page).to have_field("wiki[message]", with: "Create hyphens in the name")
+
+ page.within(".wiki-form") do
+ fill_in(:wiki_content, with: "My awesome wiki!")
+
+ click_button("Create page")
+ end
+
+ expect(page).to have_content("hyphens in the name")
+ .and have_content("Last edited by #{user.name}")
+ .and have_content("My awesome wiki!")
+ end
+ end
+
+ it "shows the emoji autocompletion dropdown" do
+ click_link("New page")
+
+ page.within(".wiki-form") do
+ find("#wiki_content").native.send_keys("")
+
+ fill_in(:wiki_content, with: ":")
+ end
+
+ expect(page).to have_selector(".atwho-view")
+ end
+ end
+end
diff --git a/spec/support/shared_examples/features/wiki/user_deletes_wiki_page_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_deletes_wiki_page_shared_examples.rb
new file mode 100644
index 00000000000..e1fd9c8dbec
--- /dev/null
+++ b/spec/support/shared_examples/features/wiki/user_deletes_wiki_page_shared_examples.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+# Requires a context containing:
+# wiki
+# user
+
+RSpec.shared_examples 'User deletes wiki page' do
+ include WikiHelpers
+
+ let(:wiki_page) { create(:wiki_page, wiki: wiki) }
+
+ before do
+ sign_in(user)
+ visit wiki_page_path(wiki, wiki_page)
+ end
+
+ it 'deletes a page', :js do
+ click_on('Edit')
+ click_on('Delete')
+ find('.modal-footer .btn-danger').click
+
+ expect(page).to have_content('Page was successfully deleted')
+ end
+end
diff --git a/spec/support/shared_examples/features/wiki/user_previews_wiki_changes_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_previews_wiki_changes_shared_examples.rb
new file mode 100644
index 00000000000..a22d98f20c4
--- /dev/null
+++ b/spec/support/shared_examples/features/wiki/user_previews_wiki_changes_shared_examples.rb
@@ -0,0 +1,110 @@
+# frozen_string_literal: true
+
+# Requires a context containing:
+# wiki
+# user
+
+RSpec.shared_examples 'User previews wiki changes' do
+ let(:wiki_page) { build(:wiki_page, wiki: wiki) }
+
+ before do
+ sign_in(user)
+ end
+
+ shared_examples 'relative links' do
+ let_it_be(:page_content) do
+ <<~HEREDOC
+ Some text so key event for [ does not trigger an incorrect replacement.
+ [regular link](regular)
+ [relative link 1](../relative)
+ [relative link 2](./relative)
+ [relative link 3](./e/f/relative)
+ [spaced link](title with spaces)
+ HEREDOC
+ end
+
+ def relative_path(path)
+ (Pathname.new(wiki.wiki_base_path) + File.dirname(wiki_page.path).tr(' ', '-') + path).to_s
+ end
+
+ shared_examples "rewrites relative links" do
+ specify do
+ expect(element).to have_link('regular link', href: wiki.wiki_base_path + '/regular')
+ expect(element).to have_link('spaced link', href: wiki.wiki_base_path + '/title%20with%20spaces')
+
+ expect(element).to have_link('relative link 1', href: relative_path('../relative'))
+ expect(element).to have_link('relative link 2', href: relative_path('./relative'))
+ expect(element).to have_link('relative link 3', href: relative_path('./e/f/relative'))
+ end
+ end
+
+ context "when there are no spaces or hyphens in the page name" do
+ let(:wiki_page) { build(:wiki_page, wiki: wiki, title: 'a/b/c/d', content: page_content) }
+
+ it_behaves_like 'rewrites relative links'
+ end
+
+ context "when there are spaces in the page name" do
+ let(:wiki_page) { build(:wiki_page, wiki: wiki, title: 'a page/b page/c page/d page', content: page_content) }
+
+ it_behaves_like 'rewrites relative links'
+ end
+
+ context "when there are hyphens in the page name" do
+ let(:wiki_page) { build(:wiki_page, wiki: wiki, title: 'a-page/b-page/c-page/d-page', content: page_content) }
+
+ it_behaves_like 'rewrites relative links'
+ end
+ end
+
+ context "when rendering a new wiki page", :js do
+ before do
+ wiki_page.create # rubocop:disable Rails/SaveBang
+ visit wiki_page_path(wiki, wiki_page)
+ end
+
+ it_behaves_like 'relative links' do
+ let(:element) { page.find('[data-testid="wiki_page_content"]') }
+ end
+ end
+
+ context "when previewing an existing wiki page", :js do
+ let(:preview) { page.find('.md-preview-holder') }
+
+ before do
+ wiki_page.create # rubocop:disable Rails/SaveBang
+ visit wiki_page_path(wiki, wiki_page, action: :edit)
+ end
+
+ it_behaves_like 'relative links' do
+ before do
+ click_on 'Preview'
+ end
+
+ let(:element) { preview }
+ end
+
+ it 'renders content with CommonMark' do
+ fill_in :wiki_content, with: "1. one\n - sublist\n"
+ click_on "Preview"
+
+ # the above generates two separate lists (not embedded) in CommonMark
+ expect(preview).to have_content("sublist")
+ expect(preview).not_to have_xpath("//ol//li//ul")
+ end
+
+ it "does not linkify double brackets inside code blocks as expected" do
+ fill_in :wiki_content, with: <<-HEREDOC
+ `[[do_not_linkify]]`
+ ```
+ [[also_do_not_linkify]]
+ ```
+ HEREDOC
+ click_on "Preview"
+
+ expect(preview).to have_content("do_not_linkify")
+ expect(preview).to have_content('[[do_not_linkify]]')
+ expect(preview).to have_content('[[also_do_not_linkify]]')
+ end
+ end
+end
diff --git a/spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb
new file mode 100644
index 00000000000..1a5f8d7d8df
--- /dev/null
+++ b/spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb
@@ -0,0 +1,231 @@
+# frozen_string_literal: true
+
+# Requires a context containing:
+# wiki
+# user
+
+RSpec.shared_examples 'User updates wiki page' do
+ include WikiHelpers
+
+ before do
+ sign_in(user)
+ end
+
+ context 'when wiki is empty' do
+ before do |example|
+ visit(wiki_path(wiki))
+
+ wait_for_svg_to_be_loaded(example)
+
+ click_link "Create your first page"
+ end
+
+ it 'redirects back to the home edit page' do
+ page.within(:css, '.wiki-form .form-actions') do
+ click_on('Cancel')
+ end
+
+ expect(current_path).to eq wiki_path(wiki)
+ end
+
+ it 'updates a page that has a path', :js do
+ fill_in(:wiki_title, with: 'one/two/three-test')
+
+ page.within '.wiki-form' do
+ fill_in(:wiki_content, with: 'wiki content')
+ click_on('Create page')
+ end
+
+ expect(current_path).to include('one/two/three-test')
+ expect(find('.wiki-pages')).to have_content('three')
+
+ first(:link, text: 'three').click
+
+ expect(find('[data-testid="wiki_page_title"]')).to have_content('three')
+
+ click_on('Edit')
+
+ expect(current_path).to include('one/two/three-test')
+ expect(page).to have_content('Edit Page')
+
+ fill_in('Content', with: 'Updated Wiki Content')
+ click_on('Save changes')
+
+ expect(page).to have_content('Updated Wiki Content')
+ end
+
+ it_behaves_like 'wiki file attachments'
+ end
+
+ context 'when wiki is not empty' do
+ let!(:wiki_page) { create(:wiki_page, wiki: wiki, title: 'home', content: 'Home page') }
+
+ before do
+ visit(wiki_path(wiki))
+
+ click_link('Edit')
+ end
+
+ it 'updates a page', :js do
+ # Commit message field should have correct value.
+ expect(page).to have_field('wiki[message]', with: 'Update home')
+
+ fill_in(:wiki_content, with: 'My awesome wiki!')
+ click_button('Save changes')
+
+ expect(page).to have_content('Home')
+ expect(page).to have_content("Last edited by #{user.name}")
+ expect(page).to have_content('My awesome wiki!')
+ end
+
+ it 'updates the commit message as the title is changed', :js do
+ fill_in(:wiki_title, with: '& < > \ \ { } &')
+
+ expect(page).to have_field('wiki[message]', with: 'Update & < > \ \ { } &')
+ end
+
+ it 'correctly escapes the commit message entities', :js do
+ fill_in(:wiki_title, with: 'Wiki title')
+
+ expect(page).to have_field('wiki[message]', with: 'Update Wiki title')
+ end
+
+ it 'shows a validation error message' do
+ fill_in(:wiki_content, with: '')
+ click_button('Save changes')
+
+ expect(page).to have_selector('.wiki-form')
+ expect(page).to have_content('Edit Page')
+ expect(page).to have_content('The form contains the following error:')
+ expect(page).to have_content("Content can't be blank")
+ expect(find('textarea#wiki_content').value).to eq('')
+ end
+
+ it 'shows the emoji autocompletion dropdown', :js do
+ find('#wiki_content').native.send_keys('')
+ fill_in(:wiki_content, with: ':')
+
+ expect(page).to have_selector('.atwho-view')
+ end
+
+ it 'shows the error message' do
+ wiki_page.update(content: 'Update') # rubocop:disable Rails/SaveBang
+
+ click_button('Save changes')
+
+ expect(page).to have_content('Someone edited the page the same time you did.')
+ end
+
+ it 'updates a page' do
+ fill_in('Content', with: 'Updated Wiki Content')
+ click_on('Save changes')
+
+ expect(page).to have_content('Updated Wiki Content')
+ end
+
+ it 'cancels editing of a page' do
+ page.within(:css, '.wiki-form .form-actions') do
+ click_on('Cancel')
+ end
+
+ expect(current_path).to eq(wiki_page_path(wiki, wiki_page))
+ end
+
+ it_behaves_like 'wiki file attachments'
+ end
+
+ context 'when the page is in a subdir' do
+ let(:page_name) { 'page_name' }
+ let(:page_dir) { "foo/bar/#{page_name}" }
+ let!(:wiki_page) { create(:wiki_page, wiki: wiki, title: page_dir, content: 'Home page') }
+
+ before do
+ visit wiki_page_path(wiki, wiki_page, action: :edit)
+ end
+
+ it 'moves the page to the root folder' do
+ fill_in(:wiki_title, with: "/#{page_name}")
+
+ click_button('Save changes')
+
+ expect(current_path).to eq(wiki_page_path(wiki, page_name))
+ end
+
+ it 'moves the page to other dir' do
+ new_page_dir = "foo1/bar1/#{page_name}"
+
+ fill_in(:wiki_title, with: new_page_dir)
+
+ click_button('Save changes')
+
+ expect(current_path).to eq(wiki_page_path(wiki, new_page_dir))
+ end
+
+ it 'remains in the same place if title has not changed' do
+ original_path = wiki_page_path(wiki, wiki_page)
+
+ fill_in(:wiki_title, with: page_name)
+
+ click_button('Save changes')
+
+ expect(current_path).to eq(original_path)
+ end
+
+ it 'can be moved to a different dir with a different name' do
+ new_page_dir = "foo1/bar1/new_page_name"
+
+ fill_in(:wiki_title, with: new_page_dir)
+
+ click_button('Save changes')
+
+ expect(current_path).to eq(wiki_page_path(wiki, new_page_dir))
+ end
+
+ it 'can be renamed and moved to the root folder' do
+ new_name = 'new_page_name'
+
+ fill_in(:wiki_title, with: "/#{new_name}")
+
+ click_button('Save changes')
+
+ expect(current_path).to eq(wiki_page_path(wiki, new_name))
+ end
+
+ it 'squishes the title before creating the page' do
+ new_page_dir = " foo1 / bar1 / #{page_name} "
+
+ fill_in(:wiki_title, with: new_page_dir)
+
+ click_button('Save changes')
+
+ expect(current_path).to eq(wiki_page_path(wiki, "foo1/bar1/#{page_name}"))
+ end
+
+ it_behaves_like 'wiki file attachments'
+ end
+
+ context 'when an existing page exceeds the content size limit' do
+ let!(:wiki_page) { create(:wiki_page, wiki: wiki, content: "one\ntwo\nthree") }
+
+ before do
+ stub_application_setting(wiki_page_max_content_bytes: 10)
+
+ visit wiki_page_path(wiki_page.wiki, wiki_page, action: :edit)
+ end
+
+ it 'allows changing the title if the content does not change' do
+ fill_in 'Title', with: 'new title'
+ click_on 'Save changes'
+
+ expect(page).to have_content('Wiki was successfully updated.')
+ end
+
+ it 'shows a validation error when trying to change the content' do
+ fill_in 'Content', with: 'new content'
+ click_on 'Save changes'
+
+ expect(page).to have_content('The form contains the following error:')
+ expect(page).to have_content('Content is too long (11 Bytes). The maximum size is 10 Bytes.')
+ end
+ end
+end
diff --git a/spec/support/shared_examples/features/wiki/user_uses_wiki_shortcuts_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_uses_wiki_shortcuts_shared_examples.rb
new file mode 100644
index 00000000000..0330b345a18
--- /dev/null
+++ b/spec/support/shared_examples/features/wiki/user_uses_wiki_shortcuts_shared_examples.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+# Requires a context containing:
+# wiki
+# user
+
+RSpec.shared_examples 'User uses wiki shortcuts' do
+ let(:wiki_page) { create(:wiki_page, wiki: wiki, title: 'home', content: 'Home page') }
+
+ before do
+ sign_in(user)
+ visit wiki_page_path(wiki, wiki_page)
+ end
+
+ it 'Visit edit wiki page using "e" keyboard shortcut', :js do
+ find('body').native.send_key('e')
+
+ expect(find('.wiki-page-title')).to have_content('Edit Page')
+ end
+end
diff --git a/spec/support/shared_examples/features/wiki/user_views_asciidoc_page_with_includes_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_views_asciidoc_page_with_includes_shared_examples.rb
new file mode 100644
index 00000000000..3b2fda4e05b
--- /dev/null
+++ b/spec/support/shared_examples/features/wiki/user_views_asciidoc_page_with_includes_shared_examples.rb
@@ -0,0 +1,77 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'User views AsciiDoc page with includes' do
+ let_it_be(:wiki_content_selector) { '[data-qa-selector=wiki_page_content]' }
+ let!(:included_wiki_page) { create_wiki_page('included_page', content: 'Content from the included page')}
+ let!(:wiki_page) { create_wiki_page('home', content: "Content from the main page.\ninclude::included_page.asciidoc[]") }
+
+ def create_wiki_page(title, content:)
+ attrs = {
+ title: title,
+ content: content,
+ format: :asciidoc
+ }
+
+ create(:wiki_page, wiki: wiki, **attrs)
+ end
+
+ before do
+ sign_in(user)
+ end
+
+ context 'when the file being included exists', :js do
+ it 'includes the file contents' do
+ visit(wiki_page_path(wiki, wiki_page))
+
+ page.within(:css, wiki_content_selector) do
+ expect(page).to have_content('Content from the main page. Content from the included page')
+ end
+ end
+
+ context 'when there are multiple versions of the wiki pages' do
+ before do
+ # rubocop:disable Rails/SaveBang
+ included_wiki_page.update(message: 'updated included file', content: 'Updated content from the included page')
+ wiki_page.update(message: 'updated wiki page', content: "Updated content from the main page.\ninclude::included_page.asciidoc[]")
+ # rubocop:enable Rails/SaveBang
+ end
+
+ let(:latest_version_id) { wiki_page.versions.first.id }
+ let(:oldest_version_id) { wiki_page.versions.last.id }
+
+ context 'viewing the latest version' do
+ it 'includes the latest content' do
+ visit(wiki_page_path(wiki, wiki_page, version_id: latest_version_id))
+
+ page.within(:css, wiki_content_selector) do
+ expect(page).to have_content('Updated content from the main page. Updated content from the included page')
+ end
+ end
+ end
+
+ context 'viewing the original version' do
+ it 'includes the content from the original version' do
+ visit(wiki_page_path(wiki, wiki_page, version_id: oldest_version_id))
+
+ page.within(:css, wiki_content_selector) do
+ expect(page).to have_content('Content from the main page. Content from the included page')
+ end
+ end
+ end
+ end
+ end
+
+ context 'when the file being included does not exist', :js do
+ before do
+ included_wiki_page.delete
+ end
+
+ it 'outputs an error' do
+ visit(wiki_page_path(wiki, wiki_page))
+
+ page.within(:css, wiki_content_selector) do
+ expect(page).to have_content('Content from the main page. [ERROR: include::included_page.asciidoc[] - unresolved directive]')
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/features/wiki/user_views_wiki_empty_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_views_wiki_empty_shared_examples.rb
new file mode 100644
index 00000000000..d7f5b485a82
--- /dev/null
+++ b/spec/support/shared_examples/features/wiki/user_views_wiki_empty_shared_examples.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+# Requires a context containing:
+# wiki
+
+RSpec.shared_examples 'User views empty wiki' do
+ let(:element) { page.find('.row.empty-state') }
+ let(:container_name) { wiki.container.class.name.humanize(capitalize: false) }
+ let(:confluence_link) { 'Enable the Confluence Wiki integration' }
+
+ shared_examples 'wiki is not found' do
+ it 'shows an error message' do
+ visit wiki_path(wiki)
+
+ if @current_user
+ expect(page).to have_content('Page Not Found')
+ else
+ expect(page).to have_content('You need to sign in')
+ end
+ end
+ end
+
+ shared_examples 'empty wiki message' do |writable: false, issuable: false, confluence: false|
+ # This mirrors the logic in:
+ # - app/views/shared/empty_states/_wikis.html.haml
+ # - WikiHelper#wiki_empty_state_messages
+ it 'shows the empty state message with the expected elements' do
+ visit wiki_path(wiki)
+
+ if writable
+ expect(element).to have_content("The wiki lets you write documentation for your #{container_name}")
+ else
+ expect(element).to have_content("This #{container_name} has no wiki pages")
+ expect(element).to have_content("You must be a #{container_name} member")
+ end
+
+ if issuable && !writable
+ expect(element).to have_content("improve the wiki for this #{container_name}")
+ expect(element).to have_link("issue tracker", href: project_issues_path(project))
+ expect(element).to have_link("Suggest wiki improvement", href: new_project_issue_path(project))
+ else
+ expect(element).not_to have_content("improve the wiki for this #{container_name}")
+ expect(element).not_to have_link("issue tracker")
+ expect(element).not_to have_link("Suggest wiki improvement")
+ end
+
+ if confluence
+ expect(element).to have_link(confluence_link)
+ else
+ expect(element).not_to have_link(confluence_link)
+ end
+
+ if writable
+ element.click_link 'Create your first page'
+
+ expect(page).to have_button('Create page')
+ else
+ expect(element).not_to have_link('Create your first page')
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/features/wiki/user_views_wiki_page_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_views_wiki_page_shared_examples.rb
new file mode 100644
index 00000000000..85eedbf4cc5
--- /dev/null
+++ b/spec/support/shared_examples/features/wiki/user_views_wiki_page_shared_examples.rb
@@ -0,0 +1,274 @@
+# frozen_string_literal: true
+
+# Requires a context containing:
+# wiki
+# user
+
+RSpec.shared_examples 'User views a wiki page' do
+ include WikiHelpers
+
+ let(:path) { 'image.png' }
+ let(:wiki_page) do
+ create(:wiki_page,
+ wiki: wiki,
+ title: 'home', content: "Look at this [image](#{path})\n\n ![alt text](#{path})")
+ end
+
+ before do
+ sign_in(user)
+ end
+
+ context 'when wiki is empty', :js do
+ before do
+ visit wiki_path(wiki)
+
+ wait_for_svg_to_be_loaded
+
+ click_link "Create your first page"
+
+ fill_in(:wiki_title, with: 'one/two/three-test')
+
+ page.within('.wiki-form') do
+ fill_in(:wiki_content, with: 'wiki content')
+ click_on('Create page')
+ end
+
+ expect(page).to have_content('Wiki was successfully updated.')
+ end
+
+ it 'shows the history of a page that has a path' do
+ expect(current_path).to include('one/two/three-test')
+
+ first(:link, text: 'three').click
+ click_on('Page history')
+
+ expect(current_path).to include('one/two/three-test')
+
+ page.within(:css, '.nav-text') do
+ expect(page).to have_content('History')
+ end
+ end
+
+ it 'shows an old version of a page' do
+ expect(current_path).to include('one/two/three-test')
+ expect(find('.wiki-pages')).to have_content('three')
+
+ first(:link, text: 'three').click
+
+ expect(find('[data-testid="wiki_page_title"]')).to have_content('three')
+
+ click_on('Edit')
+
+ expect(current_path).to include('one/two/three-test')
+ expect(page).to have_content('Edit Page')
+
+ fill_in('Content', with: 'Updated Wiki Content')
+ click_on('Save changes')
+
+ expect(page).to have_content('Wiki was successfully updated.')
+
+ click_on('Page history')
+
+ within('.nav-text') do
+ expect(page).to have_content('History')
+ end
+
+ within('.wiki-history') do
+ expect(page).to have_css('a[href*="?version_id"]', count: 4)
+ end
+ end
+ end
+
+ context 'when a page does not have history' do
+ before do
+ visit(wiki_page_path(wiki, wiki_page))
+ end
+
+ it 'shows all the pages' do
+ expect(page).to have_content(user.name)
+ expect(find('.wiki-pages')).to have_content(wiki_page.title.capitalize)
+ end
+
+ context 'shows a file stored in a page' do
+ let(:path) { upload_file_to_wiki(wiki, user, 'dk.png') }
+
+ it do
+ expect(page).to have_xpath("//img[@data-src='#{wiki.wiki_base_path}/#{path}']")
+ expect(page).to have_link('image', href: "#{wiki.wiki_base_path}/#{path}")
+
+ click_on('image')
+
+ expect(current_path).to match("wikis/#{path}")
+ expect(page).not_to have_xpath('/html') # Page should render the image which means there is no html involved
+ end
+ end
+
+ it 'shows the creation page if file does not exist' do
+ expect(page).to have_link('image', href: "#{wiki.wiki_base_path}/#{path}")
+
+ click_on('image')
+
+ expect(current_path).to match("wikis/#{path}")
+ expect(page).to have_content('Create New Page')
+ end
+ end
+
+ context 'when a page has history' do
+ before do
+ wiki_page.update(message: 'updated home', content: 'updated [some link](other-page)') # rubocop:disable Rails/SaveBang
+ end
+
+ it 'shows the page history' do
+ visit(wiki_page_path(wiki, wiki_page))
+
+ expect(page).to have_selector('[data-testid="wiki_edit_button"]')
+
+ click_on('Page history')
+
+ expect(page).to have_content(user.name)
+ expect(page).to have_content("#{user.username} created page: home")
+ expect(page).to have_content('updated home')
+ end
+
+ it 'does not show the "Edit" button' do
+ visit(wiki_page_path(wiki, wiki_page, version_id: wiki_page.versions.last.id))
+
+ expect(page).not_to have_selector('[data-testid="wiki_edit_button"]')
+ end
+
+ context 'show the diff' do
+ def expect_diff_links(commit)
+ diff_path = wiki_page_path(wiki, wiki_page, version_id: commit, action: :diff)
+
+ expect(page).to have_link('Hide whitespace changes', href: "#{diff_path}&w=1")
+ expect(page).to have_link('Inline', href: "#{diff_path}&view=inline")
+ expect(page).to have_link('Side-by-side', href: "#{diff_path}&view=parallel")
+ expect(page).to have_link("View page @ #{commit.short_id}", href: wiki_page_path(wiki, wiki_page, version_id: commit))
+ expect(page).to have_css('.diff-file[data-blob-diff-path="%s"]' % diff_path)
+ end
+
+ it 'links to the correct diffs' do
+ visit wiki_page_path(wiki, wiki_page, action: :history)
+
+ commit1 = wiki.commit('HEAD^')
+ commit2 = wiki.commit
+
+ expect(page).to have_link('created page: home', href: wiki_page_path(wiki, wiki_page, version_id: commit1, action: :diff))
+ expect(page).to have_link('updated home', href: wiki_page_path(wiki, wiki_page, version_id: commit2, action: :diff))
+ end
+
+ it 'between the current and the previous version of a page' do
+ commit = wiki.commit
+ visit wiki_page_path(wiki, wiki_page, version_id: commit, action: :diff)
+
+ expect(page).to have_content('by John Doe')
+ expect(page).to have_content('updated home')
+ expect(page).to have_content('Showing 1 changed file with 1 addition and 3 deletions')
+ expect(page).to have_content('some link')
+
+ expect_diff_links(commit)
+ end
+
+ it 'between two old versions of a page' do
+ wiki_page.update(message: 'latest home change', content: 'updated [another link](other-page)') # rubocop:disable Rails/SaveBang:
+ commit = wiki.commit('HEAD^')
+ visit wiki_page_path(wiki, wiki_page, version_id: commit, action: :diff)
+
+ expect(page).to have_content('by John Doe')
+ expect(page).to have_content('updated home')
+ expect(page).to have_content('Showing 1 changed file with 1 addition and 3 deletions')
+ expect(page).to have_content('some link')
+ expect(page).not_to have_content('latest home change')
+ expect(page).not_to have_content('another link')
+
+ expect_diff_links(commit)
+ end
+
+ it 'for the oldest version of a page' do
+ commit = wiki.commit('HEAD^')
+ visit wiki_page_path(wiki, wiki_page, version_id: commit, action: :diff)
+
+ expect(page).to have_content('by John Doe')
+ expect(page).to have_content('created page: home')
+ expect(page).to have_content('Showing 1 changed file with 4 additions and 0 deletions')
+ expect(page).to have_content('Look at this')
+
+ expect_diff_links(commit)
+ end
+ end
+ end
+
+ context 'when a page has special characters in its title' do
+ let(:title) { '<foo> !@#$%^&*()[]{}=_+\'"\\|<>? <bar>' }
+
+ before do
+ wiki_page.update(title: title ) # rubocop:disable Rails/SaveBang
+ end
+
+ it 'preserves the special characters' do
+ visit(wiki_page_path(wiki, wiki_page))
+
+ expect(page).to have_css('[data-testid="wiki_page_title"]', text: title)
+ expect(page).to have_css('.wiki-pages li', text: title)
+ end
+ end
+
+ context 'when a page has XSS in its title or content' do
+ let(:title) { '<script>alert("title")<script>' }
+
+ before do
+ wiki_page.update(title: title, content: 'foo <script>alert("content")</script> bar') # rubocop:disable Rails/SaveBang
+ end
+
+ it 'safely displays the page' do
+ visit(wiki_page_path(wiki, wiki_page))
+
+ expect(page).to have_selector('[data-testid="wiki_page_title"]', text: title)
+ expect(page).to have_content('foo bar')
+ end
+ end
+
+ context 'when a page has XSS in its message' do
+ before do
+ wiki_page.update(message: '<script>alert(true)<script>', content: 'XSS update') # rubocop:disable Rails/SaveBang
+ end
+
+ it 'safely displays the message' do
+ visit(wiki_page_path(wiki, wiki_page, action: :history))
+
+ expect(page).to have_content('<script>alert(true)<script>')
+ end
+ end
+
+ context 'when page has invalid content encoding' do
+ let(:content) { (+'whatever').force_encoding('ISO-8859-1') }
+
+ before do
+ allow(Gitlab::EncodingHelper).to receive(:encode!).and_return(content)
+
+ visit(wiki_page_path(wiki, wiki_page))
+ end
+
+ it 'does not show "Edit" button' do
+ expect(page).not_to have_selector('[data-testid="wiki_edit_button"]')
+ end
+
+ it 'shows error' do
+ page.within(:css, '.flash-notice') do
+ expect(page).to have_content('The content of this page is not encoded in UTF-8. Edits can only be made via the Git repository.')
+ end
+ end
+ end
+
+ it 'opens a default wiki page', :js do
+ visit wiki.container.web_url
+
+ find('.shortcuts-wiki').click
+
+ wait_for_svg_to_be_loaded
+
+ click_link "Create your first page"
+
+ expect(page).to have_content('Create New Page')
+ end
+end
diff --git a/spec/support/shared_examples/features/wiki/user_views_wiki_pages_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_views_wiki_pages_shared_examples.rb
new file mode 100644
index 00000000000..314c2074eee
--- /dev/null
+++ b/spec/support/shared_examples/features/wiki/user_views_wiki_pages_shared_examples.rb
@@ -0,0 +1,89 @@
+# frozen_string_literal: true
+
+# Requires a context containing:
+# wiki
+# user
+
+RSpec.shared_examples 'User views wiki pages' do
+ include WikiHelpers
+
+ let!(:wiki_page1) do
+ create(:wiki_page, wiki: wiki, title: '3 home', content: '3')
+ end
+
+ let!(:wiki_page2) do
+ create(:wiki_page, wiki: wiki, title: '1 home', content: '1')
+ end
+
+ let!(:wiki_page3) do
+ create(:wiki_page, wiki: wiki, title: '2 home', content: '2')
+ end
+
+ let(:pages) do
+ page.find('.wiki-pages-list').all('li').map { |li| li.find('a') }
+ end
+
+ before do
+ sign_in(user)
+ visit(wiki_path(wiki, action: :pages))
+ end
+
+ context 'ordered by title' do
+ let(:pages_ordered_by_title) { [wiki_page2, wiki_page3, wiki_page1] }
+
+ context 'asc' do
+ it 'pages are displayed in direct order' do
+ pages.each.with_index do |page_title, index|
+ expect(page_title.text).to eq(pages_ordered_by_title[index].title)
+ end
+ end
+ end
+
+ context 'desc' do
+ before do
+ page.within('.wiki-sort-dropdown') do
+ page.find('.rspec-reverse-sort').click
+ end
+ end
+
+ it 'pages are displayed in reversed order' do
+ pages.reverse_each.with_index do |page_title, index|
+ expect(page_title.text).to eq(pages_ordered_by_title[index].title)
+ end
+ end
+ end
+ end
+
+ context 'ordered by created_at' do
+ let(:pages_ordered_by_created_at) { [wiki_page1, wiki_page2, wiki_page3] }
+
+ before do
+ page.within('.wiki-sort-dropdown') do
+ click_button('Title')
+ click_link('Created date')
+ end
+ end
+
+ context 'asc' do
+ it 'pages are displayed in direct order' do
+ pages.each.with_index do |page_title, index|
+ expect(page_title.text).to eq(pages_ordered_by_created_at[index].title)
+ end
+ end
+ end
+
+ context 'desc' do
+ before do
+ page.within('.wiki-sort-dropdown') do
+ page.find('.rspec-reverse-sort').click
+ end
+ end
+
+ it 'pages are displayed in reversed order' do
+ pages.reverse_each.with_index do |page_title, index|
+ expect(page_title.text).to eq(pages_ordered_by_created_at[index].title)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/features/wiki/user_views_wiki_sidebar_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_views_wiki_sidebar_shared_examples.rb
new file mode 100644
index 00000000000..a7ba7a8ad07
--- /dev/null
+++ b/spec/support/shared_examples/features/wiki/user_views_wiki_sidebar_shared_examples.rb
@@ -0,0 +1,68 @@
+# frozen_string_literal: true
+
+# Requires a context containing:
+# wiki
+# user
+
+RSpec.shared_examples 'User views wiki sidebar' do
+ include WikiHelpers
+
+ before do
+ sign_in(user)
+ end
+
+ context 'when there are some existing pages' do
+ before do
+ create(:wiki_page, wiki: wiki, title: 'home', content: 'home')
+ create(:wiki_page, wiki: wiki, title: 'another', content: 'another')
+ end
+
+ it 'renders a default sidebar when there is no customized sidebar' do
+ visit wiki_path(wiki)
+
+ expect(page).to have_content('another')
+ expect(page).not_to have_link('View All Pages')
+ end
+
+ context 'when there is a customized sidebar' do
+ before do
+ create(:wiki_page, wiki: wiki, title: '_sidebar', content: 'My customized sidebar')
+ end
+
+ it 'renders my customized sidebar instead of the default one' do
+ visit wiki_path(wiki)
+
+ expect(page).to have_content('My customized sidebar')
+ expect(page).not_to have_content('Another')
+ end
+ end
+ end
+
+ context 'when there are 15 existing pages' do
+ before do
+ (1..5).each { |i| create(:wiki_page, wiki: wiki, title: "my page #{i}") }
+ (6..10).each { |i| create(:wiki_page, wiki: wiki, title: "parent/my page #{i}") }
+ (11..15).each { |i| create(:wiki_page, wiki: wiki, title: "grandparent/parent/my page #{i}") }
+ end
+
+ it 'shows all pages in the sidebar' do
+ visit wiki_path(wiki)
+
+ (1..15).each { |i| expect(page).to have_content("my page #{i}") }
+ expect(page).not_to have_link('View All Pages')
+ end
+
+ context 'when there are more than 15 existing pages' do
+ before do
+ create(:wiki_page, wiki: wiki, title: 'my page 16')
+ end
+
+ it 'shows the first 15 pages in the sidebar' do
+ visit wiki_path(wiki)
+
+ expect(page).to have_text('my page', count: 15)
+ expect(page).to have_link('View All Pages')
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/graphql/mutations/boards_create_shared_examples.rb b/spec/support/shared_examples/graphql/mutations/boards_create_shared_examples.rb
new file mode 100644
index 00000000000..ec64519cd9c
--- /dev/null
+++ b/spec/support/shared_examples/graphql/mutations/boards_create_shared_examples.rb
@@ -0,0 +1,75 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'boards create mutation' do
+ include GraphqlHelpers
+
+ let_it_be(:current_user, reload: true) { create(:user) }
+ let(:name) { 'board name' }
+ let(:mutation) { graphql_mutation(:create_board, params) }
+
+ subject { post_graphql_mutation(mutation, current_user: current_user) }
+
+ def mutation_response
+ graphql_mutation_response(:create_board)
+ end
+
+ context 'when the user does not have permission' do
+ it_behaves_like 'a mutation that returns a top-level access error'
+
+ it 'does not create the board' do
+ expect { subject }.not_to change { Board.count }
+ end
+ end
+
+ context 'when the user has permission' do
+ before do
+ parent.add_maintainer(current_user)
+ end
+
+ context 'when the parent (project_path or group_path) param is given' do
+ context 'when everything is ok' do
+ it 'creates the board' do
+ expect { subject }.to change { Board.count }.from(0).to(1)
+ end
+
+ it 'returns the created board' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(mutation_response).to have_key('board')
+ expect(mutation_response['board']['name']).to eq(name)
+ end
+ end
+
+ context 'when the Boards::CreateService returns an error response' do
+ before do
+ allow_next_instance_of(Boards::CreateService) do |service|
+ allow(service).to receive(:execute).and_return(ServiceResponse.error(message: 'There was an error.'))
+ end
+ end
+
+ it 'does not create a board' do
+ expect { subject }.not_to change { Board.count }
+ end
+
+ it 'returns an error' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(mutation_response).to have_key('board')
+ expect(mutation_response['board']).to be_nil
+ expect(mutation_response['errors'].first).to eq('There was an error.')
+ end
+ end
+ end
+
+ context 'when neither project_path nor group_path param is given' do
+ let(:params) { { name: name } }
+
+ it_behaves_like 'a mutation that returns top-level errors',
+ errors: ['group_path or project_path arguments are required']
+
+ it 'does not create the board' do
+ expect { subject }.not_to change { Board.count }
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/graphql/mutations/spammable_mutation_fields_examples.rb b/spec/support/shared_examples/graphql/mutations/spammable_mutation_fields_examples.rb
new file mode 100644
index 00000000000..54b3f84a6e6
--- /dev/null
+++ b/spec/support/shared_examples/graphql/mutations/spammable_mutation_fields_examples.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.shared_examples 'spam flag is present' do
+ specify :aggregate_failures do
+ subject
+
+ expect(mutation_response).to have_key('spam')
+ expect(mutation_response['spam']).to be_falsey
+ end
+end
+
+RSpec.shared_examples 'can raise spam flag' do
+ it 'spam parameters are passed to the service' do
+ expect(service).to receive(:new).with(anything, anything, hash_including(api: true, request: instance_of(ActionDispatch::Request)))
+
+ subject
+ end
+
+ context 'when the snippet is detected as spam' do
+ it 'raises spam flag' do
+ allow_next_instance_of(service) do |instance|
+ allow(instance).to receive(:spam_check) do |snippet, user, _|
+ snippet.spam!
+ end
+ end
+
+ subject
+
+ expect(mutation_response['spam']).to be true
+ expect(mutation_response['errors']).to include("Your snippet has been recognized as spam and has been discarded.")
+ end
+ end
+
+ context 'when :snippet_spam flag is disabled' do
+ before do
+ stub_feature_flags(snippet_spam: false)
+ end
+
+ it 'request parameter is not passed to the service' do
+ expect(service).to receive(:new).with(anything, anything, hash_not_including(request: instance_of(ActionDispatch::Request)))
+
+ subject
+ end
+ end
+end
diff --git a/spec/support/shared_examples/graphql/notes_creation_shared_examples.rb b/spec/support/shared_examples/graphql/notes_creation_shared_examples.rb
index 522211340ea..24c8a247c93 100644
--- a/spec/support/shared_examples/graphql/notes_creation_shared_examples.rb
+++ b/spec/support/shared_examples/graphql/notes_creation_shared_examples.rb
@@ -52,11 +52,15 @@ RSpec.shared_examples 'a Note mutation when the given resource id is not for a N
it_behaves_like 'a Note mutation that does not create a Note'
- it_behaves_like 'a mutation that returns top-level errors', errors: ['Cannot add notes to this resource']
+ it_behaves_like 'a mutation that returns top-level errors' do
+ let(:match_errors) { include(/ does not represent an instance of Noteable/) }
+ end
end
RSpec.shared_examples 'a Note mutation when the given resource id is not for a Note' do
let(:note) { create(:issue) }
- it_behaves_like 'a mutation that returns top-level errors', errors: ['Resource is not a note']
+ it_behaves_like 'a mutation that returns top-level errors' do
+ let(:match_errors) { include(/does not represent an instance of Note/) }
+ 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 e93077c42e1..7707e79386c 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
@@ -1,12 +1,13 @@
# frozen_string_literal: true
-RSpec.shared_examples 'resource mentions migration' do |migration_class, resource_class|
+RSpec.shared_examples 'resource mentions migration' do |migration_class, resource_class_name|
it 'migrates resource mentions' do
join = migration_class::JOIN
conditions = migration_class::QUERY_CONDITIONS
+ resource_class = "#{Gitlab::BackgroundMigration::UserMentions::Models}::#{resource_class_name}".constantize
expect do
- subject.perform(resource_class.name, join, conditions, false, resource_class.minimum(:id), resource_class.maximum(:id))
+ subject.perform(resource_class_name, join, conditions, false, resource_class.minimum(:id), resource_class.maximum(:id))
end.to change { user_mentions.count }.by(1)
user_mention = user_mentions.last
@@ -16,23 +17,23 @@ RSpec.shared_examples 'resource mentions migration' do |migration_class, resourc
# check that performing the same job twice does not fail and does not change counts
expect do
- subject.perform(resource_class.name, join, conditions, false, resource_class.minimum(:id), resource_class.maximum(:id))
+ subject.perform(resource_class_name, join, conditions, false, resource_class.minimum(:id), resource_class.maximum(:id))
end.to change { user_mentions.count }.by(0)
end
end
-RSpec.shared_examples 'resource notes mentions migration' do |migration_class, resource_class|
+RSpec.shared_examples 'resource notes mentions migration' do |migration_class, resource_class_name|
it 'migrates mentions from note' do
join = migration_class::JOIN
conditions = migration_class::QUERY_CONDITIONS
# there are 5 notes for each noteable_type, but two do not have mentions and
# another one's noteable_id points to an inexistent resource
- expect(notes.where(noteable_type: resource_class.to_s).count).to eq 5
+ expect(notes.where(noteable_type: resource_class_name).count).to eq 5
expect(user_mentions.count).to eq 0
expect do
- subject.perform(resource_class.name, join, conditions, true, Note.minimum(:id), Note.maximum(:id))
+ subject.perform(resource_class_name, join, conditions, true, Note.minimum(:id), Note.maximum(:id))
end.to change { user_mentions.count }.by(2)
# check that the user_mention for regular note is created
@@ -51,7 +52,7 @@ RSpec.shared_examples 'resource notes mentions migration' do |migration_class, r
# check that performing the same job twice does not fail and does not change counts
expect do
- subject.perform(resource_class.name, join, conditions, true, Note.minimum(:id), Note.maximum(:id))
+ subject.perform(resource_class_name, join, conditions, true, Note.minimum(:id), Note.maximum(:id))
end.to change { user_mentions.count }.by(0)
end
end
@@ -83,24 +84,25 @@ RSpec.shared_examples 'schedules resource mentions migration' do |resource_class
end
end
-RSpec.shared_examples 'resource migration not run' do |migration_class, resource_class|
+RSpec.shared_examples 'resource migration not run' do |migration_class, resource_class_name|
it 'does not migrate mentions' do
join = migration_class::JOIN
conditions = migration_class::QUERY_CONDITIONS
+ resource_class = "#{Gitlab::BackgroundMigration::UserMentions::Models}::#{resource_class_name}".constantize
expect do
- subject.perform(resource_class.name, join, conditions, false, resource_class.minimum(:id), resource_class.maximum(:id))
+ subject.perform(resource_class_name, join, conditions, false, resource_class.minimum(:id), resource_class.maximum(:id))
end.to change { user_mentions.count }.by(0)
end
end
-RSpec.shared_examples 'resource notes migration not run' do |migration_class, resource_class|
+RSpec.shared_examples 'resource notes migration not run' do |migration_class, resource_class_name|
it 'does not migrate mentions' do
join = migration_class::JOIN
conditions = migration_class::QUERY_CONDITIONS
expect do
- subject.perform(resource_class.name, join, conditions, true, Note.minimum(:id), Note.maximum(:id))
+ subject.perform(resource_class_name, join, conditions, true, Note.minimum(:id), Note.maximum(:id))
end.to change { user_mentions.count }.by(0)
end
end
diff --git a/spec/support/shared_examples/lib/gitlab/ci/ci_trace_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/ci/ci_trace_shared_examples.rb
index db5e9461f3f..0df1af3b10a 100644
--- a/spec/support/shared_examples/lib/gitlab/ci/ci_trace_shared_examples.rb
+++ b/spec/support/shared_examples/lib/gitlab/ci/ci_trace_shared_examples.rb
@@ -227,7 +227,7 @@ RSpec.shared_examples 'common trace features' do
let(:token) { 'my_secret_token' }
before do
- build.project.update(runners_token: token)
+ build.project.update!(runners_token: token)
trace.append(token, 0)
end
@@ -240,7 +240,7 @@ RSpec.shared_examples 'common trace features' do
let(:token) { 'my_secret_token' }
before do
- build.update(token: token)
+ build.update!(token: token)
trace.append(token, 0)
end
@@ -531,7 +531,7 @@ RSpec.shared_examples 'trace with disabled live trace feature' do
context "when erase old trace with 'save'" do
before do
build.send(:write_attribute, :trace, nil)
- build.save
+ build.save # rubocop:disable Rails/SaveBang
end
it 'old trace is not deleted' do
diff --git a/spec/support/shared_examples/lib/gitlab/diff_file_collections_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/diff_file_collections_shared_examples.rb
index e43ce936b90..469c0c287b1 100644
--- a/spec/support/shared_examples/lib/gitlab/diff_file_collections_shared_examples.rb
+++ b/spec/support/shared_examples/lib/gitlab/diff_file_collections_shared_examples.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
RSpec.shared_examples 'diff statistics' do |test_include_stats_flag: true|
- subject { described_class.new(diffable, collection_default_args) }
+ subject { described_class.new(diffable, **collection_default_args) }
def stub_stats_find_by_path(path, stats_mock)
expect_next_instance_of(Gitlab::Git::DiffStatsCollection) do |collection|
diff --git a/spec/support/shared_examples/lib/gitlab/import_export/relation_factory_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/import_export/relation_factory_shared_examples.rb
new file mode 100644
index 00000000000..33061f17bde
--- /dev/null
+++ b/spec/support/shared_examples/lib/gitlab/import_export/relation_factory_shared_examples.rb
@@ -0,0 +1,107 @@
+# frozen_string_literal: true
+
+# required context:
+# - importable: group or project
+# - relation_hash: a note relation that's being imported
+# - created_object: the object created with the relation factory
+RSpec.shared_examples 'Notes user references' do
+ let(:relation_sym) { :notes }
+ let(:mapped_user) { create(:user) }
+ let(:exported_member) do
+ {
+ 'id' => 111,
+ 'access_level' => 30,
+ 'source_id' => 1,
+ 'source_type' => importable.class.name == 'Project' ? 'Project' : 'Namespace',
+ 'user_id' => 3,
+ 'notification_level' => 3,
+ 'created_at' => '2016-11-18T09:29:42.634Z',
+ 'updated_at' => '2016-11-18T09:29:42.634Z',
+ 'user' => {
+ 'id' => 999,
+ 'email' => mapped_user.email,
+ 'username' => mapped_user.username
+ }
+ }
+ end
+
+ let(:members_mapper) do
+ Gitlab::ImportExport::MembersMapper.new(
+ exported_members: [exported_member].compact,
+ user: importer_user,
+ importable: importable
+ )
+ end
+
+ shared_examples 'sets the note author to the importer user' do
+ it { expect(created_object.author).to eq(importer_user) }
+ end
+
+ shared_examples 'sets the note author to the mapped user' do
+ it { expect(created_object.author).to eq(mapped_user) }
+ end
+
+ shared_examples 'does not add original autor note' do
+ it { expect(created_object.note).not_to include('*By Administrator') }
+ end
+
+ shared_examples 'adds original autor note' do
+ it { expect(created_object.note).to include('*By Administrator') }
+ end
+
+ context 'when the importer is admin' do
+ let(:importer_user) { create(:admin) }
+
+ context 'and the note author is not mapped' do
+ let(:exported_member) { nil }
+
+ include_examples 'sets the note author to the importer user'
+
+ include_examples 'adds original autor note'
+ end
+
+ context 'and the note author is the importer user' do
+ let(:mapped_user) { importer_user }
+
+ include_examples 'sets the note author to the mapped user'
+
+ include_examples 'does not add original autor note'
+ end
+
+ context 'and the note author exists in the target instance' do
+ let(:mapped_user) { create(:user) }
+
+ include_examples 'sets the note author to the mapped user'
+
+ include_examples 'does not add original autor note'
+ end
+ end
+
+ context 'when the importer is not admin' do
+ let(:importer_user) { create(:user) }
+
+ context 'and the note author is not mapped' do
+ let(:exported_member) { nil }
+
+ include_examples 'sets the note author to the importer user'
+
+ include_examples 'adds original autor note'
+ end
+
+ context 'and the note author is the importer user' do
+ let(:mapped_user) { importer_user }
+
+ include_examples 'sets the note author to the importer user'
+
+ include_examples 'adds original autor note'
+ end
+
+ context 'and the note author exists in the target instance' do
+ let(:mapped_user) { create(:user) }
+
+ include_examples 'sets the note author to the importer user'
+
+ include_examples 'adds original autor note'
+ end
+ end
+end
diff --git a/spec/support/shared_examples/lib/gitlab/repository_size_checker_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/repository_size_checker_shared_examples.rb
new file mode 100644
index 00000000000..bb909ffe82a
--- /dev/null
+++ b/spec/support/shared_examples/lib/gitlab/repository_size_checker_shared_examples.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'checker size above limit' do
+ context 'when size is above the limit' do
+ let(:current_size) { 100 }
+
+ it 'returns true' do
+ expect(subject.above_size_limit?).to eq(true)
+ end
+ end
+end
+
+RSpec.shared_examples 'checker size not over limit' do
+ it 'returns false when not over the limit' do
+ expect(subject.above_size_limit?).to eq(false)
+ end
+end
+
+RSpec.shared_examples 'checker size exceeded' do
+ context 'when current size is below or equal to the limit' do
+ let(:current_size) { 50 }
+
+ it 'returns zero' do
+ expect(subject.exceeded_size).to eq(0)
+ end
+ end
+
+ context 'when current size is over the limit' do
+ let(:current_size) { 51 }
+
+ it 'returns zero' do
+ expect(subject.exceeded_size).to eq(1.megabytes)
+ end
+ end
+
+ context 'when change size will be over the limit' do
+ let(:current_size) { 50 }
+
+ it 'returns zero' do
+ expect(subject.exceeded_size(1.megabytes)).to eq(1.megabytes)
+ end
+ end
+
+ context 'when change size will not be over the limit' do
+ let(:current_size) { 49 }
+
+ it 'returns zero' do
+ expect(subject.exceeded_size(1.megabytes)).to eq(0)
+ 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
index f96ff4b101e..b3b33e434b9 100644
--- a/spec/support/shared_examples/lib/gitlab/search/recent_items.rb
+++ b/spec/support/shared_examples/lib/gitlab/search/recent_items.rb
@@ -1,12 +1,11 @@
# 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) }
+ let_it_be(:recent_items) { described_class.new(user: user) }
+ let(:item) { create_item(content: 'hello world 1', parent: parent) }
+ let(:parent) { create(parent_type, :public) }
describe '#log_view', :clean_gitlab_redis_shared_state do
it 'adds the item to the recent items' do
@@ -18,13 +17,15 @@ RSpec.shared_examples 'search recent items' do
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))
+ recent_items = described_class.new(user: user, items_limit: 3)
+
+ 4.times do |i|
+ recent_items.log_view(create_item(content: "item #{i}", parent: parent))
end
results = recent_items.search('item')
- expect(results.map(&:title)).to contain_exactly('item 6', 'item 5', 'item 4', 'item 3', 'item 2')
+ expect(results.map(&:title)).to contain_exactly('item 3', 'item 2', 'item 1')
end
it 'expires the items after expires_after' do
@@ -39,7 +40,7 @@ RSpec.shared_examples 'search recent items' do
it 'does not include results logged for another user' do
another_user = create(:user)
- another_item = create_item(content: 'hello world 2', project: project)
+ another_item = create_item(content: 'hello world 2', parent: parent)
described_class.new(user: another_user).log_view(another_item)
recent_items.log_view(item)
@@ -50,11 +51,11 @@ RSpec.shared_examples 'search recent items' do
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) }
+ let(:item1) { create_item(content: "matching item 1", parent: parent) }
+ let(:item2) { create_item(content: "matching item 2", parent: parent) }
+ let(:item3) { create_item(content: "matching item 3", parent: parent) }
+ let(:non_matching_item) { create_item(content: "different item", parent: parent) }
+ let!(:non_viewed_item) { create_item(content: "matching but not viewed item", parent: parent) }
before do
recent_items.log_view(item1)
@@ -74,14 +75,24 @@ RSpec.shared_examples 'search recent items' do
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)
+ private_parent = create(parent_type, :public)
+ private_item = create_item(content: 'matching item title', parent: private_parent)
recent_items.log_view(private_item)
- private_project.update!(visibility_level: Project::PRIVATE)
+ private_parent.update!(visibility_level: ::Gitlab::VisibilityLevel::PRIVATE)
expect(recent_items.search('matching')).not_to include(private_item)
end
+
+ it "limits results to #{Gitlab::Search::RecentItems::SEARCH_LIMIT} items" do
+ (Gitlab::Search::RecentItems::SEARCH_LIMIT + 1).times do |i|
+ recent_items.log_view(create_item(content: "item #{i}", parent: parent))
+ end
+
+ results = recent_items.search('item')
+
+ expect(results.count).to eq(Gitlab::Search::RecentItems::SEARCH_LIMIT)
+ end
end
end
diff --git a/spec/support/shared_examples/lib/gitlab/search_confidential_filter_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/search_confidential_filter_shared_examples.rb
new file mode 100644
index 00000000000..d0bef2ad730
--- /dev/null
+++ b/spec/support/shared_examples/lib/gitlab/search_confidential_filter_shared_examples.rb
@@ -0,0 +1,69 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'search results filtered by confidential' do
+ context 'filter not provided (all behavior)' do
+ let(:filters) { {} }
+
+ context 'when Feature search_filter_by_confidential enabled' do
+ it 'returns confidential and not confidential results', :aggregate_failures do
+ expect(results.objects('issues')).to include confidential_result
+ expect(results.objects('issues')).to include opened_result
+ end
+ end
+
+ context 'when Feature search_filter_by_confidential not enabled' do
+ before do
+ stub_feature_flags(search_filter_by_confidential: false)
+ end
+
+ it 'returns confidential and not confidential results', :aggregate_failures do
+ expect(results.objects('issues')).to include confidential_result
+ expect(results.objects('issues')).to include opened_result
+ end
+ end
+ end
+
+ context 'confidential filter' do
+ let(:filters) { { confidential: true } }
+
+ context 'when Feature search_filter_by_confidential enabled' do
+ it 'returns only confidential results', :aggregate_failures do
+ expect(results.objects('issues')).to include confidential_result
+ expect(results.objects('issues')).not_to include opened_result
+ end
+ end
+
+ context 'when Feature search_filter_by_confidential not enabled' do
+ before do
+ stub_feature_flags(search_filter_by_confidential: false)
+ end
+
+ it 'returns confidential and not confidential results', :aggregate_failures do
+ expect(results.objects('issues')).to include confidential_result
+ expect(results.objects('issues')).to include opened_result
+ end
+ end
+ end
+
+ context 'not confidential filter' do
+ let(:filters) { { confidential: false } }
+
+ context 'when Feature search_filter_by_confidential enabled' do
+ it 'returns not confidential results', :aggregate_failures do
+ expect(results.objects('issues')).not_to include confidential_result
+ expect(results.objects('issues')).to include opened_result
+ end
+ end
+
+ context 'when Feature search_filter_by_confidential not enabled' do
+ before do
+ stub_feature_flags(search_filter_by_confidential: false)
+ end
+
+ it 'returns confidential and not confidential results', :aggregate_failures do
+ expect(results.objects('issues')).to include confidential_result
+ expect(results.objects('issues')).to include opened_result
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/lib/gitlab/search_results_sorted_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/search_results_sorted_shared_examples.rb
new file mode 100644
index 00000000000..765279a78fe
--- /dev/null
+++ b/spec/support/shared_examples/lib/gitlab/search_results_sorted_shared_examples.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'search results sorted' do
+ context 'sort: newest' do
+ let(:sort) { 'newest' }
+
+ it 'sorts results by created_at' do
+ expect(results.objects(scope).map(&:id)).to eq([new_result.id, old_result.id, very_old_result.id])
+ end
+ end
+
+ context 'sort: oldest' do
+ let(:sort) { 'oldest' }
+
+ it 'sorts results by created_at' do
+ expect(results.objects(scope).map(&:id)).to eq([very_old_result.id, old_result.id, new_result.id])
+ 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_state_filter_shared_examples.rb
index e80ec516407..e80ec516407 100644
--- a/spec/support/shared_examples/lib/gitlab/search_issue_state_filter_shared_examples.rb
+++ b/spec/support/shared_examples/lib/gitlab/search_state_filter_shared_examples.rb
diff --git a/spec/support/shared_examples/mailers/notify_shared_examples.rb b/spec/support/shared_examples/mailers/notify_shared_examples.rb
index 1f5803b90a0..7ce7b2161f6 100644
--- a/spec/support/shared_examples/mailers/notify_shared_examples.rb
+++ b/spec/support/shared_examples/mailers/notify_shared_examples.rb
@@ -267,3 +267,9 @@ RSpec.shared_examples 'appearance header and footer not enabled' do
end
end
end
+
+RSpec.shared_examples 'no email is sent' do
+ it 'does not send an email' do
+ expect(subject.message).to be_a_kind_of(ActionMailer::Base::NullMail)
+ end
+end
diff --git a/spec/support/shared_examples/models/concerns/has_repository_shared_examples.rb b/spec/support/shared_examples/models/concerns/has_repository_shared_examples.rb
index f37ef3533c3..826ee453919 100644
--- a/spec/support/shared_examples/models/concerns/has_repository_shared_examples.rb
+++ b/spec/support/shared_examples/models/concerns/has_repository_shared_examples.rb
@@ -6,6 +6,14 @@ RSpec.shared_examples 'model with repository' do
let(:expected_full_path) { raise NotImplementedError }
let(:expected_web_url_path) { expected_full_path }
let(:expected_repo_url_path) { expected_full_path }
+ let(:expected_lfs_enabled) { false }
+
+ it 'container class includes HasRepository' do
+ # NOTE: This is not enforced at runtime, since we also need to support Geo::DeletedProject
+ expect(described_class).to include_module(HasRepository)
+ expect(container).to be_kind_of(HasRepository)
+ expect(stubbed_container).to be_kind_of(HasRepository)
+ end
describe '#commits_by' do
let(:commits) { container.repository.commits('HEAD', limit: 3).commits }
@@ -74,6 +82,10 @@ RSpec.shared_examples 'model with repository' do
it 'returns valid repo' do
expect(container.repository).to be_kind_of(Repository)
end
+
+ it 'uses the same container' do
+ expect(container.repository.container).to be(container)
+ end
end
describe '#storage' do
@@ -88,6 +100,16 @@ RSpec.shared_examples 'model with repository' do
end
end
+ describe '#lfs_enabled?' do
+ before do
+ stub_lfs_setting(enabled: true)
+ end
+
+ it 'returns the expected value' do
+ expect(container.lfs_enabled?).to eq(expected_lfs_enabled)
+ end
+ end
+
describe '#empty_repo?' do
context 'when the repo does not exist' do
it 'returns true' do
diff --git a/spec/support/shared_examples/models/concerns/shardable_shared_examples.rb b/spec/support/shared_examples/models/concerns/shardable_shared_examples.rb
new file mode 100644
index 00000000000..fa929d5b791
--- /dev/null
+++ b/spec/support/shared_examples/models/concerns/shardable_shared_examples.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'shardable scopes' do
+ let_it_be(:secondary_shard) { create(:shard, name: 'test_second_storage') }
+
+ before do
+ record_2.update!(shard: secondary_shard)
+ end
+
+ describe '.for_repository_storage' do
+ it 'returns the objects for a given repository storage' do
+ expect(described_class.for_repository_storage('default')).to eq([record_1])
+ end
+ end
+
+ describe '.excluding_repository_storage' do
+ it 'returns the objects excluding the given repository storage' do
+ expect(described_class.excluding_repository_storage('default')).to eq([record_2])
+ end
+ end
+end
diff --git a/spec/support/shared_examples/models/concerns/timebox_shared_examples.rb b/spec/support/shared_examples/models/concerns/timebox_shared_examples.rb
index d199bae4170..f91e4bd8cf7 100644
--- a/spec/support/shared_examples/models/concerns/timebox_shared_examples.rb
+++ b/spec/support/shared_examples/models/concerns/timebox_shared_examples.rb
@@ -9,6 +9,11 @@ RSpec.shared_examples 'a timebox' do |timebox_type|
let(:user) { create(:user) }
let(:timebox_table_name) { timebox_type.to_s.pluralize.to_sym }
+ # Values implementions can override
+ let(:mid_point) { Time.now.utc.to_date }
+ let(:open_on_left) { nil }
+ let(:open_on_right) { nil }
+
describe 'modules' do
context 'with a project' do
it_behaves_like 'AtomicInternalId' do
@@ -240,4 +245,85 @@ RSpec.shared_examples 'a timebox' do |timebox_type|
expect(timebox.to_ability_name).to eq(timebox_type.to_s)
end
end
+
+ describe '.within_timeframe' do
+ let(:factory) { timebox_type }
+ let(:min_date) { mid_point - 10.days }
+ let(:max_date) { mid_point + 10.days }
+
+ def box(from, to)
+ create(factory, *timebox_args,
+ start_date: from || open_on_left,
+ due_date: to || open_on_right)
+ end
+
+ it 'can find overlapping timeboxes' do
+ fully_open = box(nil, nil)
+ # ----| ................ # Not overlapping
+ non_overlapping_open_on_left = box(nil, min_date - 1.day)
+ # |--| ................ # Not overlapping
+ non_overlapping_closed_on_left = box(min_date - 2.days, min_date - 1.day)
+ # ------|............... # Overlapping
+ overlapping_open_on_left_just = box(nil, min_date)
+ # -----------------------| # Overlapping
+ overlapping_open_on_left_fully = box(nil, max_date + 1.day)
+ # ---------|............ # Overlapping
+ overlapping_open_on_left_partial = box(nil, min_date + 1.day)
+ # |-----|............ # Overlapping
+ overlapping_closed_partial = box(min_date - 1.day, min_date + 1.day)
+ # |--------------| # Overlapping
+ exact_match = box(min_date, max_date)
+ # |--------------------| # Overlapping
+ larger = box(min_date - 1.day, max_date + 1.day)
+ # ...|-----|...... # Overlapping
+ smaller = box(min_date + 1.day, max_date - 1.day)
+ # .........|-----| # Overlapping
+ at_end = box(max_date - 1.day, max_date)
+ # .........|--------- # Overlapping
+ at_end_open = box(max_date - 1.day, nil)
+ # |-------------------- # Overlapping
+ cover_from_left = box(min_date - 1.day, nil)
+ # .........|--------| # Overlapping
+ cover_from_middle_closed = box(max_date - 1.day, max_date + 1.day)
+ # ...............|--| # Overlapping
+ overlapping_at_end_just = box(max_date, max_date + 1.day)
+ # ............... |-| # Not Overlapping
+ not_overlapping_at_right_closed = box(max_date + 1.day, max_date + 2.days)
+ # ............... |-- # Not Overlapping
+ not_overlapping_at_right_open = box(max_date + 1.day, nil)
+
+ matches = described_class.within_timeframe(min_date, max_date)
+
+ expect(matches).to include(
+ overlapping_open_on_left_just,
+ overlapping_open_on_left_fully,
+ overlapping_open_on_left_partial,
+ overlapping_closed_partial,
+ exact_match,
+ larger,
+ smaller,
+ at_end,
+ at_end_open,
+ cover_from_left,
+ cover_from_middle_closed,
+ overlapping_at_end_just
+ )
+
+ expect(matches).not_to include(
+ non_overlapping_open_on_left,
+ non_overlapping_closed_on_left,
+ not_overlapping_at_right_closed,
+ not_overlapping_at_right_open
+ )
+
+ # Whether we match the 'fully-open' range depends on whether
+ # it is in fact open (i.e. whether the class allows infinite
+ # ranges)
+ if open_on_left.nil? && open_on_right.nil?
+ expect(matches).not_to include(fully_open)
+ else
+ expect(matches).to include(fully_open)
+ end
+ end
+ 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 94c52bdaaa6..0ee0b7e6d88 100644
--- a/spec/support/shared_examples/models/mentionable_shared_examples.rb
+++ b/spec/support/shared_examples/models/mentionable_shared_examples.rb
@@ -207,29 +207,8 @@ RSpec.shared_examples 'an editable mentionable' do
end
RSpec.shared_examples 'mentions in description' do |mentionable_type|
- describe 'when store_mentioned_users_to_db feature disabled' do
+ describe 'when storing user mentions' do
before do
- stub_feature_flags(store_mentioned_users_to_db: false)
- mentionable.store_mentions!
- end
-
- context 'when mentionable description contains mentions' do
- let(:user) { create(:user) }
- let(:mentionable) { create(mentionable_type, description: "#{user.to_reference} some description") }
-
- it 'stores no mentions' do
- expect(mentionable.user_mentions.count).to eq 0
- end
-
- it 'renders description_html correctly' do
- expect(mentionable.description_html).to include("<a href=\"/#{user.username}\" data-user=\"#{user.id}\"")
- end
- end
- end
-
- describe 'when store_mentioned_users_to_db feature enabled' do
- before do
- stub_feature_flags(store_mentioned_users_to_db: true)
mentionable.store_mentions!
end
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 7701ab42007..66cd8d1df12 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
@@ -60,4 +60,20 @@ RSpec.shared_examples 'latest successful build for sha or ref' do
expect(subject).to be_nil
end
end
+
+ context 'with build belonging to a child pipeline' do
+ let(:child_pipeline) { create_pipeline(project) }
+ let(:parent_bridge) { create(:ci_bridge, pipeline: pipeline, project: pipeline.project) }
+ let!(:pipeline_source) { create(:ci_sources_pipeline, source_job: parent_bridge, pipeline: child_pipeline)}
+ let!(:child_build) { create_build(child_pipeline, 'child-build') }
+ let(:build_name) { child_build.name }
+
+ before do
+ child_pipeline.update!(source: :parent_pipeline)
+ end
+
+ it 'returns the child build' do
+ expect(subject).to eq(child_build)
+ end
+ end
end
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 d1437244082..b8d12a6da59 100644
--- a/spec/support/shared_examples/models/relative_positioning_shared_examples.rb
+++ b/spec/support/shared_examples/models/relative_positioning_shared_examples.rb
@@ -31,6 +31,41 @@ RSpec.shared_examples 'a class that supports relative positioning' do
end
end
+ def as_item(item)
+ item # Override to perform a transformation, if necessary
+ end
+
+ def as_items(items)
+ items.map { |item| as_item(item) }
+ end
+
+ describe '#scoped_items' do
+ it 'includes all items with the same scope' do
+ scope = as_items([item1, item2, new_item, create_item])
+ irrelevant = create(factory, {}) # This should not share the scope
+ context = RelativePositioning.mover.context(item1)
+
+ same_scope = as_items(context.scoped_items)
+
+ expect(same_scope).to include(*scope)
+ expect(same_scope).not_to include(as_item(irrelevant))
+ end
+ end
+
+ describe '#relative_siblings' do
+ it 'includes all items with the same scope, except self' do
+ scope = as_items([item2, new_item, create_item])
+ irrelevant = create(factory, {}) # This should not share the scope
+ context = RelativePositioning.mover.context(item1)
+
+ siblings = as_items(context.relative_siblings)
+
+ expect(siblings).to include(*scope)
+ expect(siblings).not_to include(as_item(item1))
+ expect(siblings).not_to include(as_item(irrelevant))
+ end
+ end
+
describe '.move_nulls_to_end' do
let(:item3) { create_item }
let(:sibling_query) { item1.class.relative_positioning_query_base(item1) }
@@ -47,7 +82,7 @@ RSpec.shared_examples 'a class that supports relative positioning' do
expect(item1.relative_position).to be(1000)
expect(sibling_query.where(relative_position: nil)).not_to exist
- expect(sibling_query.reorder(:relative_position, :id)).to eq([item1, item2, item3])
+ expect(as_items(sibling_query.reorder(:relative_position, :id))).to eq(as_items([item1, item2, item3]))
end
it 'preserves relative position' do
@@ -117,19 +152,36 @@ RSpec.shared_examples 'a class that supports relative positioning' do
expect(bunch.map(&:relative_position)).to all(be < nils.map(&:relative_position).min)
end
+ it 'manages to move nulls found in the relative scope' do
+ nils = create_items_with_positions([nil] * 4)
+
+ described_class.move_nulls_to_end(sibling_query.to_a)
+ positions = nils.map { |item| item.reset.relative_position }
+
+ expect(positions).to all(be_present)
+ expect(positions).to all(be_valid_position)
+ end
+
+ it 'can move many nulls' do
+ nils = create_items_with_positions([nil] * 101)
+
+ described_class.move_nulls_to_end(nils)
+
+ expect(nils.map(&:relative_position)).to all(be_valid_position)
+ end
+
it 'does not have an N+1 issue' do
create_items_with_positions(10..12)
-
- a, b, c, d, e, f = create_items_with_positions([nil, nil, nil, nil, nil, nil])
+ a, b, c, d, e, f, *xs = create_items_with_positions([nil] * 10)
baseline = ActiveRecord::QueryRecorder.new do
- described_class.move_nulls_to_end([a, e])
+ described_class.move_nulls_to_end([a, b])
end
- expect { described_class.move_nulls_to_end([b, c, d]) }
+ expect { described_class.move_nulls_to_end([c, d, e, f]) }
.not_to exceed_query_limit(baseline)
- expect { described_class.move_nulls_to_end([f]) }
+ expect { described_class.move_nulls_to_end(xs) }
.not_to exceed_query_limit(baseline.count)
end
end
@@ -149,7 +201,7 @@ RSpec.shared_examples 'a class that supports relative positioning' do
expect(items.sort_by(&:relative_position)).to eq(items)
expect(sibling_query.where(relative_position: nil)).not_to exist
- expect(sibling_query.reorder(:relative_position, :id)).to eq(items)
+ expect(as_items(sibling_query.reorder(:relative_position, :id))).to eq(as_items(items))
expect(item3.relative_position).to be(1000)
end
@@ -652,3 +704,119 @@ RSpec.shared_examples 'a class that supports relative positioning' do
(RelativePositioning::MIN_POSITION..).take(size)
end
end
+
+RSpec.shared_examples 'no-op relative positioning' do
+ def create_item(**params)
+ create(factory, params.merge(default_params))
+ end
+
+ let_it_be(:item1) { create_item }
+ let_it_be(:item2) { create_item }
+ let_it_be(:new_item) { create_item(relative_position: nil) }
+
+ def any_relative_positions
+ new_item.class.reorder(:relative_position, :id).pluck(:id, :relative_position)
+ end
+
+ shared_examples 'a no-op method' do
+ it 'does not raise errors' do
+ expect { perform }.not_to raise_error
+ end
+
+ it 'does not perform any DB queries' do
+ expect { perform }.not_to exceed_query_limit(0)
+ end
+
+ it 'does not change any relative_position' do
+ expect { perform }.not_to change { any_relative_positions }
+ end
+ end
+
+ describe '.scoped_items' do
+ subject { RelativePositioning.mover.context(item1).scoped_items }
+
+ it 'is empty' do
+ expect(subject).to be_empty
+ end
+ end
+
+ describe '.relative_siblings' do
+ subject { RelativePositioning.mover.context(item1).relative_siblings }
+
+ it 'is empty' do
+ expect(subject).to be_empty
+ end
+ end
+
+ describe '.move_nulls_to_end' do
+ subject { item1.class.move_nulls_to_end([new_item, item1]) }
+
+ it_behaves_like 'a no-op method' do
+ def perform
+ subject
+ end
+ end
+
+ it 'does not move any items' do
+ expect(subject).to eq(0)
+ end
+ end
+
+ describe '.move_nulls_to_start' do
+ subject { item1.class.move_nulls_to_start([new_item, item1]) }
+
+ it_behaves_like 'a no-op method' do
+ def perform
+ subject
+ end
+ end
+
+ it 'does not move any items' do
+ expect(subject).to eq(0)
+ end
+ end
+
+ describe 'instance methods' do
+ subject { new_item }
+
+ describe '#move_to_start' do
+ it_behaves_like 'a no-op method' do
+ def perform
+ subject.move_to_start
+ end
+ end
+ end
+
+ describe '#move_to_end' do
+ it_behaves_like 'a no-op method' do
+ def perform
+ subject.move_to_end
+ end
+ end
+ end
+
+ describe '#move_between' do
+ it_behaves_like 'a no-op method' do
+ def perform
+ subject.move_between(item1, item2)
+ end
+ end
+ end
+
+ describe '#move_before' do
+ it_behaves_like 'a no-op method' do
+ def perform
+ subject.move_before(item1)
+ end
+ end
+ end
+
+ describe '#move_after' do
+ it_behaves_like 'a no-op method' do
+ def perform
+ subject.move_after(item1)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/models/resource_timebox_event_shared_examples.rb b/spec/support/shared_examples/models/resource_timebox_event_shared_examples.rb
index 07552b62cdd..5198508d48b 100644
--- a/spec/support/shared_examples/models/resource_timebox_event_shared_examples.rb
+++ b/spec/support/shared_examples/models/resource_timebox_event_shared_examples.rb
@@ -73,3 +73,13 @@ RSpec.shared_examples 'timebox resource event actions' do
end
end
end
+
+RSpec.shared_examples 'timebox resource tracks issue metrics' do |type|
+ describe '#usage_metrics' do
+ it 'tracks usage' do
+ expect(Gitlab::UsageDataCounters::IssueActivityUniqueCounter).to receive(:"track_issue_#{type}_changed_action")
+
+ create(described_class.name.underscore.to_sym, issue: create(:issue))
+ end
+ end
+end
diff --git a/spec/support/shared_examples/models/snippet_shared_examples.rb b/spec/support/shared_examples/models/snippet_shared_examples.rb
new file mode 100644
index 00000000000..a8fdf9bb81e
--- /dev/null
+++ b/spec/support/shared_examples/models/snippet_shared_examples.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'size checker for snippet' do |action|
+ it 'sets up size checker', :aggregate_failures do
+ expect(checker.current_size).to eq(current_size.megabytes)
+ expect(checker.limit).to eq(Gitlab::CurrentSettings.snippet_size_limit)
+ expect(checker.enabled?).to eq(true)
+ expect(checker.instance_variable_get(:@namespace)).to eq(namespace)
+ end
+end
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 14b851d2828..e869cbce6ae 100644
--- a/spec/support/shared_examples/models/throttled_touch_shared_examples.rb
+++ b/spec/support/shared_examples/models/throttled_touch_shared_examples.rb
@@ -13,8 +13,8 @@ RSpec.shared_examples 'throttled touch' do
first_updated_at = Time.zone.now - (ThrottledTouch::TOUCH_INTERVAL * 2)
second_updated_at = Time.zone.now - (ThrottledTouch::TOUCH_INTERVAL * 1.5)
- Timecop.freeze(first_updated_at) { subject.touch }
- Timecop.freeze(second_updated_at) { subject.touch }
+ travel_to(first_updated_at) { subject.touch }
+ travel_to(second_updated_at) { subject.touch }
expect(subject.updated_at).to be_like_time(first_updated_at)
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 557025569b8..7b591ad84d1 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
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-RSpec.shared_examples 'UpdateProjectStatistics' do
+RSpec.shared_examples 'UpdateProjectStatistics' do |with_counter_attribute|
let(:project) { subject.project }
let(:project_statistics_name) { described_class.project_statistics_name }
let(:statistic_attribute) { described_class.statistic_attribute }
@@ -13,108 +13,230 @@ RSpec.shared_examples 'UpdateProjectStatistics' do
subject.read_attribute(statistic_attribute).to_i
end
- it { is_expected.to be_new_record }
+ def read_pending_increment
+ Gitlab::Redis::SharedState.with do |redis|
+ key = project.statistics.counter_key(project_statistics_name)
+ redis.get(key).to_i
+ end
+ end
- context 'when creating' do
- it 'updates the project statistics' do
- delta0 = reload_stat
+ it { is_expected.to be_new_record }
- subject.save!
+ context 'when feature flag efficient_counter_attribute is disabled' do
+ before do
+ stub_feature_flags(efficient_counter_attribute: false)
+ end
- delta1 = reload_stat
+ context 'when creating' do
+ it 'updates the project statistics' do
+ delta0 = reload_stat
- expect(delta1).to eq(delta0 + read_attribute)
- expect(delta1).to be > delta0
- end
+ subject.save!
- it 'schedules a namespace statistics worker' do
- expect(Namespaces::ScheduleAggregationWorker)
- .to receive(:perform_async).once
+ delta1 = reload_stat
- subject.save!
- end
- end
+ expect(delta1).to eq(delta0 + read_attribute)
+ expect(delta1).to be > delta0
+ end
- context 'when updating' do
- let(:delta) { 42 }
+ it 'schedules a namespace statistics worker' do
+ expect(Namespaces::ScheduleAggregationWorker)
+ .to receive(:perform_async).once
- before do
- subject.save!
+ subject.save!
+ end
end
- it 'updates project statistics' do
- expect(ProjectStatistics)
- .to receive(:increment_statistic)
- .and_call_original
+ context 'when updating' do
+ let(:delta) { 42 }
- subject.write_attribute(statistic_attribute, read_attribute + delta)
+ before do
+ subject.save!
+ end
- expect { subject.save! }
- .to change { reload_stat }
- .by(delta)
- end
+ it 'updates project statistics' do
+ expect(ProjectStatistics)
+ .to receive(:increment_statistic)
+ .and_call_original
- it 'schedules a namespace statistics worker' do
- expect(Namespaces::ScheduleAggregationWorker)
- .to receive(:perform_async).once
+ subject.write_attribute(statistic_attribute, read_attribute + delta)
- subject.write_attribute(statistic_attribute, read_attribute + delta)
- subject.save!
- end
+ expect { subject.save! }
+ .to change { reload_stat }
+ .by(delta)
+ end
- it 'avoids N + 1 queries' do
- subject.write_attribute(statistic_attribute, read_attribute + delta)
+ it 'schedules a namespace statistics worker' do
+ expect(Namespaces::ScheduleAggregationWorker)
+ .to receive(:perform_async).once
- control_count = ActiveRecord::QueryRecorder.new do
+ subject.write_attribute(statistic_attribute, read_attribute + delta)
subject.save!
end
- subject.write_attribute(statistic_attribute, read_attribute + delta)
+ it 'avoids N + 1 queries' do
+ subject.write_attribute(statistic_attribute, read_attribute + delta)
- expect do
- subject.save!
- end.not_to exceed_query_limit(control_count)
- end
- end
+ control_count = ActiveRecord::QueryRecorder.new do
+ subject.save!
+ end
- context 'when destroying' do
- before do
- subject.save!
+ subject.write_attribute(statistic_attribute, read_attribute + delta)
+
+ expect do
+ subject.save!
+ end.not_to exceed_query_limit(control_count)
+ end
end
- it 'updates the project statistics' do
- delta0 = reload_stat
+ context 'when destroying' do
+ before do
+ subject.save!
+ end
- subject.destroy!
+ it 'updates the project statistics' do
+ delta0 = reload_stat
- delta1 = reload_stat
+ subject.destroy!
- expect(delta1).to eq(delta0 - read_attribute)
- expect(delta1).to be < delta0
- end
+ delta1 = reload_stat
+
+ expect(delta1).to eq(delta0 - read_attribute)
+ expect(delta1).to be < delta0
+ end
+
+ it 'schedules a namespace statistics worker' do
+ expect(Namespaces::ScheduleAggregationWorker)
+ .to receive(:perform_async).once
- it 'schedules a namespace statistics worker' do
- expect(Namespaces::ScheduleAggregationWorker)
- .to receive(:perform_async).once
+ subject.destroy!
+ end
+
+ context 'when it is destroyed from the project level' do
+ it 'does not update the project statistics' do
+ expect(ProjectStatistics)
+ .not_to receive(:increment_statistic)
+
+ project.update!(pending_delete: true)
+ project.destroy!
+ end
+
+ it 'does not schedule a namespace statistics worker' do
+ expect(Namespaces::ScheduleAggregationWorker)
+ .not_to receive(:perform_async)
- subject.destroy!
+ project.update!(pending_delete: true)
+ project.destroy!
+ end
+ end
end
+ end
- context 'when it is destroyed from the project level' do
- it 'does not update the project statistics' do
- expect(ProjectStatistics)
- .not_to receive(:increment_statistic)
+ def expect_flush_counter_increments_worker_performed
+ expect(FlushCounterIncrementsWorker)
+ .to receive(:perform_in)
+ .with(CounterAttribute::WORKER_DELAY, project.statistics.class.name, project.statistics.id, project_statistics_name)
+ expect(FlushCounterIncrementsWorker)
+ .to receive(:perform_in)
+ .with(CounterAttribute::WORKER_DELAY, project.statistics.class.name, project.statistics.id, :storage_size)
- project.update!(pending_delete: true)
- project.destroy!
+ yield
+
+ # simulate worker running now
+ expect(Namespaces::ScheduleAggregationWorker).to receive(:perform_async)
+ FlushCounterIncrementsWorker.new.perform(project.statistics.class.name, project.statistics.id, project_statistics_name)
+ end
+
+ if with_counter_attribute
+ context 'when statistic is a counter attribute', :clean_gitlab_redis_shared_state do
+ context 'when creating' do
+ it 'stores pending increments for async update' do
+ initial_stat = reload_stat
+ expected_increment = read_attribute
+
+ expect_flush_counter_increments_worker_performed do
+ subject.save!
+
+ expect(read_pending_increment).to eq(expected_increment)
+ expect(expected_increment).to be > initial_stat
+ expect(expected_increment).to be_positive
+ end
+ end
end
- it 'does not schedule a namespace statistics worker' do
- expect(Namespaces::ScheduleAggregationWorker)
- .not_to receive(:perform_async)
+ context 'when updating' do
+ let(:delta) { 42 }
+
+ before do
+ subject.save!
+ redis_shared_state_cleanup!
+ end
+
+ it 'stores pending increments for async update' do
+ expect(ProjectStatistics)
+ .to receive(:increment_statistic)
+ .and_call_original
+
+ subject.write_attribute(statistic_attribute, read_attribute + delta)
+
+ expect_flush_counter_increments_worker_performed do
+ subject.save!
+
+ expect(read_pending_increment).to eq(delta)
+ end
+ end
+
+ it 'avoids N + 1 queries' do
+ subject.write_attribute(statistic_attribute, read_attribute + delta)
+
+ control_count = ActiveRecord::QueryRecorder.new do
+ subject.save!
+ end
+
+ subject.write_attribute(statistic_attribute, read_attribute + delta)
+
+ expect do
+ subject.save!
+ end.not_to exceed_query_limit(control_count)
+ end
+ end
- project.update!(pending_delete: true)
- project.destroy!
+ context 'when destroying' do
+ before do
+ subject.save!
+ redis_shared_state_cleanup!
+ end
+
+ it 'stores pending increment for async update' do
+ initial_stat = reload_stat
+ expected_increment = -read_attribute
+
+ expect_flush_counter_increments_worker_performed do
+ subject.destroy!
+
+ expect(read_pending_increment).to eq(expected_increment)
+ expect(expected_increment).to be < initial_stat
+ expect(expected_increment).to be_negative
+ end
+ end
+
+ context 'when it is destroyed from the project level' do
+ it 'does not update the project statistics' do
+ expect(ProjectStatistics)
+ .not_to receive(:increment_statistic)
+
+ project.update!(pending_delete: true)
+ project.destroy!
+ end
+
+ it 'does not schedule a namespace statistics worker' do
+ expect(Namespaces::ScheduleAggregationWorker)
+ .not_to receive(:perform_async)
+
+ project.update!(pending_delete: true)
+ project.destroy!
+ end
+ end
end
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 b87f7fe97e1..62da9e15259 100644
--- a/spec/support/shared_examples/models/wiki_shared_examples.rb
+++ b/spec/support/shared_examples/models/wiki_shared_examples.rb
@@ -4,21 +4,99 @@ RSpec.shared_examples 'wiki model' do
let_it_be(:user) { create(:user, :commit_email) }
let(:wiki_container) { raise NotImplementedError }
let(:wiki_container_without_repo) { raise NotImplementedError }
+ let(:wiki_lfs_enabled) { false }
let(:wiki) { described_class.new(wiki_container, user) }
let(:commit) { subject.repository.head_commit }
subject { wiki }
+ it 'container class includes HasWiki' do
+ # NOTE: This is not enforced at runtime, since we also need to support Geo::DeletedProject
+ expect(wiki_container).to be_kind_of(HasWiki)
+ expect(wiki_container_without_repo).to be_kind_of(HasWiki)
+ end
+
it_behaves_like 'model with repository' do
let(:container) { wiki }
let(:stubbed_container) { described_class.new(wiki_container_without_repo, user) }
let(:expected_full_path) { "#{container.container.full_path}.wiki" }
let(:expected_web_url_path) { "#{container.container.web_url(only_path: true).sub(%r{^/}, '')}/-/wikis/home" }
+ let(:expected_lfs_enabled) { wiki_lfs_enabled }
+ end
+
+ describe '.container_class' do
+ it 'is set to the container class' do
+ expect(described_class.container_class).to eq(wiki_container.class)
+ end
+ end
+
+ describe '.find_by_id' do
+ it 'returns a wiki instance if the container is found' do
+ wiki = described_class.find_by_id(wiki_container.id)
+
+ expect(wiki).to be_a(described_class)
+ expect(wiki.container).to eq(wiki_container)
+ end
+
+ it 'returns nil if the container is not found' do
+ expect(described_class.find_by_id(-1)).to be_nil
+ end
+ end
+
+ describe '#initialize' do
+ it 'accepts a valid user' do
+ expect do
+ described_class.new(wiki_container, user)
+ end.not_to raise_error
+ end
+
+ it 'accepts a blank user' do
+ expect do
+ described_class.new(wiki_container, nil)
+ end.not_to raise_error
+ end
+
+ it 'raises an error for invalid users' do
+ expect do
+ described_class.new(wiki_container, Object.new)
+ end.to raise_error(ArgumentError, 'user must be a User, got Object')
+ end
+ end
+
+ describe '#run_after_commit' do
+ it 'delegates to the container' do
+ expect(wiki_container).to receive(:run_after_commit)
+
+ wiki.run_after_commit
+ end
+ end
+
+ describe '#==' do
+ it 'returns true for wikis from the same container' do
+ expect(wiki).to eq(described_class.new(wiki_container))
+ end
+
+ it 'returns false for wikis from different containers' do
+ expect(wiki).not_to eq(described_class.new(wiki_container_without_repo))
+ end
+ end
+
+ describe '#id' do
+ it 'returns the ID of the container' do
+ expect(wiki.id).to eq(wiki_container.id)
+ end
+ end
+
+ describe '#to_global_id' do
+ it 'returns a global ID' do
+ expect(wiki.to_global_id.to_s).to eq("gid://gitlab/#{wiki.class.name}/#{wiki.id}")
+ end
end
describe '#repository' do
it 'returns a wiki repository' do
expect(subject.repository.repo_type).to be_wiki
+ expect(subject.repository.container).to be(subject)
end
end
@@ -164,7 +242,7 @@ RSpec.shared_examples 'wiki model' do
def total_pages(entries)
entries.sum do |entry|
- entry.is_a?(WikiDirectory) ? entry.pages.size : 1
+ entry.is_a?(WikiDirectory) ? total_pages(entry.entries) : 1
end
end
@@ -204,8 +282,9 @@ RSpec.shared_examples 'wiki model' do
expect(page.title).to eq('index page')
end
- it 'returns nil if the page does not exist' do
- expect(subject.find_page('non-existent')).to eq(nil)
+ it 'returns nil if the page or version does not exist' do
+ expect(subject.find_page('non-existent')).to be_nil
+ expect(subject.find_page('index page', 'non-existent')).to be_nil
end
it 'can find a page by slug' do
diff --git a/spec/support/shared_examples/policies/resource_access_token_shared_examples.rb b/spec/support/shared_examples/policies/resource_access_token_shared_examples.rb
new file mode 100644
index 00000000000..7710e756e5b
--- /dev/null
+++ b/spec/support/shared_examples/policies/resource_access_token_shared_examples.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'Self-managed Core resource access tokens' do
+ before do
+ allow(::Gitlab).to receive(:com?).and_return(false)
+ end
+
+ context 'with owner' do
+ let(:current_user) { owner }
+
+ it { is_expected.to be_allowed(:admin_resource_access_tokens) }
+ end
+
+ context 'with developer' do
+ let(:current_user) { developer }
+
+ it { is_expected.not_to be_allowed(:admin_resource_access_tokens) }
+ end
+end
+
+RSpec.shared_examples 'GitLab.com Core resource access tokens' do
+ before do
+ allow(::Gitlab).to receive(:com?).and_return(true)
+ stub_ee_application_setting(should_check_namespace_plan: true)
+ end
+
+ context 'with owner' do
+ let(:current_user) { owner }
+
+ it { is_expected.not_to be_allowed(:admin_resource_access_tokens) }
+ end
+end
diff --git a/spec/support/shared_examples/quick_actions/issuable/issuable_quick_actions_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/issuable_quick_actions_shared_examples.rb
index 50a8b81b518..3cdba315d1f 100644
--- a/spec/support/shared_examples/quick_actions/issuable/issuable_quick_actions_shared_examples.rb
+++ b/spec/support/shared_examples/quick_actions/issuable/issuable_quick_actions_shared_examples.rb
@@ -109,7 +109,7 @@ RSpec.shared_examples 'issuable quick actions' do
QuickAction.new(
action_text: "/unlock",
before_action: -> {
- issuable.update(discussion_locked: true)
+ issuable.update!(discussion_locked: true)
},
expectation: ->(noteable, can_use_quick_action) {
if can_use_quick_action
@@ -128,7 +128,7 @@ RSpec.shared_examples 'issuable quick actions' do
QuickAction.new(
action_text: "/remove_milestone",
before_action: -> {
- issuable.update(milestone_id: milestone.id)
+ issuable.update!(milestone_id: milestone.id)
},
expectation: ->(noteable, can_use_quick_action) {
if can_use_quick_action
@@ -171,7 +171,7 @@ RSpec.shared_examples 'issuable quick actions' do
QuickAction.new(
action_text: "/remove_estimate",
before_action: -> {
- issuable.update(time_estimate: 30000)
+ issuable.update!(time_estimate: 30000)
},
expectation: ->(noteable, can_use_quick_action) {
if can_use_quick_action
@@ -228,7 +228,7 @@ RSpec.shared_examples 'issuable quick actions' do
before do
project.add_developer(old_assignee)
- issuable.update(assignees: [old_assignee])
+ issuable.update!(assignees: [old_assignee])
end
context 'when user can update issuable' do
diff --git a/spec/support/shared_examples/quick_actions/merge_request/merge_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/merge_request/merge_quick_action_shared_examples.rb
index 258d9ab85e4..acbc6429646 100644
--- a/spec/support/shared_examples/quick_actions/merge_request/merge_quick_action_shared_examples.rb
+++ b/spec/support/shared_examples/quick_actions/merge_request/merge_quick_action_shared_examples.rb
@@ -52,7 +52,7 @@ RSpec.shared_examples 'merge quick action' do
context 'when the head diff changes in the meanwhile' do
before do
merge_request.source_branch = 'another_branch'
- merge_request.save
+ merge_request.save!
sign_in(user)
visit project_merge_request_path(project, merge_request)
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 5c122b4b5d6..4b5299cebec 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
@@ -75,7 +75,7 @@ RSpec.shared_examples 'Composer package creation' do |user_type, status, add_mem
expect(response).to have_gitlab_http_status(status)
end
- it_behaves_like 'a gitlab tracking event', described_class.name, 'push_package'
+ it_behaves_like 'a package tracking event', described_class.name, 'push_package'
end
end
diff --git a/spec/support/shared_examples/requests/api/container_repositories_shared_examples.rb b/spec/support/shared_examples/requests/api/container_repositories_shared_examples.rb
index 3e058838773..e776cf13217 100644
--- a/spec/support/shared_examples/requests/api/container_repositories_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/container_repositories_shared_examples.rb
@@ -79,11 +79,3 @@ RSpec.shared_examples 'returns repositories for allowed users' do |user_type, sc
end
end
end
-
-RSpec.shared_examples 'a gitlab tracking event' do |category, action|
- it "creates a gitlab tracking event #{action}" do
- expect(Gitlab::Tracking).to receive(:event).with(category, action, {})
-
- subject
- end
-end
diff --git a/spec/support/shared_examples/requests/api/debian_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/debian_packages_shared_examples.rb
new file mode 100644
index 00000000000..ec32cb4b2ff
--- /dev/null
+++ b/spec/support/shared_examples/requests/api/debian_packages_shared_examples.rb
@@ -0,0 +1,309 @@
+# frozen_string_literal: true
+
+RSpec.shared_context 'Debian repository shared context' do |object_type|
+ before do
+ stub_feature_flags(debian_packages: true)
+ end
+
+ if object_type == :project
+ let(:project) { create(:project, :public) }
+ elsif object_type == :group
+ let(:group) { create(:group, :public) }
+ end
+
+ let(:user) { create(:user) }
+ let(:personal_access_token) { create(:personal_access_token, user: user) }
+
+ let(:distribution) { 'bullseye' }
+ let(:component) { 'main' }
+ let(:architecture) { 'amd64' }
+ let(:source_package) { 'sample' }
+ let(:letter) { source_package[0..2] == 'lib' ? source_package[0..3] : source_package[0] }
+ let(:package_name) { 'libsample0' }
+ let(:package_version) { '1.2.3~alpha2-1' }
+ let(:file_name) { "#{package_name}_#{package_version}_#{architecture}.deb" }
+
+ let(:method) { :get }
+
+ let(:workhorse_params) do
+ if method == :put
+ file_upload = fixture_file_upload("spec/fixtures/packages/debian/#{file_name}")
+ { file: file_upload }
+ else
+ {}
+ end
+ end
+
+ let(:params) { workhorse_params }
+
+ let(:auth_headers) { {} }
+ let(:workhorse_headers) do
+ if method == :put
+ workhorse_token = JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256')
+ { 'GitLab-Workhorse' => '1.0', Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER => workhorse_token }
+ else
+ {}
+ end
+ end
+
+ let(:headers) { auth_headers.merge(workhorse_headers) }
+
+ let(:send_rewritten_field) { true }
+
+ subject do
+ if method == :put
+ workhorse_finalize(
+ api(url),
+ method: method,
+ file_key: :file,
+ params: params,
+ headers: headers,
+ send_rewritten_field: send_rewritten_field
+ )
+ else
+ send method, api(url), headers: headers, params: params
+ end
+ end
+end
+
+RSpec.shared_context 'Debian repository auth headers' do |user_role, user_token, auth_method = :token|
+ let(:token) { user_token ? personal_access_token.token : 'wrong' }
+
+ let(:auth_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 'Debian repository project access' do |project_visibility_level, user_role, user_token, auth_method|
+ include_context 'Debian repository auth headers', user_role, user_token, auth_method do
+ before do
+ project.update_column(:visibility_level, Gitlab::VisibilityLevel.const_get(project_visibility_level, false))
+ end
+ end
+end
+
+RSpec.shared_examples 'Debian project repository GET request' do |user_role, add_member, status, body|
+ context "for user type #{user_role}" do
+ before do
+ project.send("add_#{user_role}", user) if add_member && user_role != :anonymous
+ end
+
+ and_body = body.nil? ? '' : ' and expected body'
+
+ it "returns #{status}#{and_body}" do
+ subject
+
+ expect(response).to have_gitlab_http_status(status)
+
+ unless body.nil?
+ expect(response.body).to eq(body)
+ end
+ end
+ end
+end
+
+RSpec.shared_examples 'Debian project repository PUT request' do |user_role, add_member, status, body|
+ context "for user type #{user_role}" do
+ before do
+ project.send("add_#{user_role}", user) if add_member && user_role != :anonymous
+ end
+
+ and_body = body.nil? ? '' : ' and expected body'
+
+ if status == :created
+ it 'creates package files' do
+ pending "Debian package creation not implemented"
+ expect { subject }
+ .to change { project.packages.debian.count }.by(1)
+
+ expect(response).to have_gitlab_http_status(status)
+
+ unless body.nil?
+ expect(response.body).to eq(body)
+ end
+ end
+ it_behaves_like 'a package tracking event', described_class.name, 'push_package'
+ else
+ it "returns #{status}#{and_body}" do
+ subject
+
+ expect(response).to have_gitlab_http_status(status)
+
+ unless body.nil?
+ expect(response.body).to eq(body)
+ end
+ end
+ end
+ end
+end
+
+RSpec.shared_examples 'rejects Debian access with unknown project id' do
+ context 'with an unknown project' do
+ let(:project) { double(id: non_existing_record_id) }
+
+ context 'as anonymous' do
+ it_behaves_like 'Debian project repository GET request', :anonymous, true, :not_found, nil
+ end
+
+ context 'as authenticated user' do
+ subject { get api(url), headers: basic_auth_header(user.username, personal_access_token.token) }
+
+ it_behaves_like 'Debian project repository GET request', :anonymous, true, :not_found, nil
+ end
+ end
+end
+
+RSpec.shared_examples 'Debian project repository GET endpoint' do |success_status, success_body|
+ context 'with valid project' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:project_visibility_level, :user_role, :member, :user_token, :expected_status, :expected_body) do
+ 'PUBLIC' | :developer | true | true | success_status | success_body
+ 'PUBLIC' | :guest | true | true | success_status | success_body
+ 'PUBLIC' | :developer | true | false | success_status | success_body
+ 'PUBLIC' | :guest | true | false | success_status | success_body
+ 'PUBLIC' | :developer | false | true | success_status | success_body
+ 'PUBLIC' | :guest | false | true | success_status | success_body
+ 'PUBLIC' | :developer | false | false | success_status | success_body
+ 'PUBLIC' | :guest | false | false | success_status | success_body
+ 'PUBLIC' | :anonymous | false | true | success_status | success_body
+ 'PRIVATE' | :developer | true | true | success_status | success_body
+ 'PRIVATE' | :guest | true | true | :forbidden | nil
+ 'PRIVATE' | :developer | true | false | :not_found | nil
+ 'PRIVATE' | :guest | true | false | :not_found | nil
+ 'PRIVATE' | :developer | false | true | :not_found | nil
+ 'PRIVATE' | :guest | false | true | :not_found | nil
+ 'PRIVATE' | :developer | false | false | :not_found | nil
+ 'PRIVATE' | :guest | false | false | :not_found | nil
+ 'PRIVATE' | :anonymous | false | true | :not_found | nil
+ end
+
+ with_them do
+ include_context 'Debian repository project access', params[:project_visibility_level], params[:user_role], params[:user_token], :basic do
+ it_behaves_like 'Debian project repository GET request', params[:user_role], params[:member], params[:expected_status], params[:expected_body]
+ end
+ end
+ end
+
+ it_behaves_like 'rejects Debian access with unknown project id'
+end
+
+RSpec.shared_examples 'Debian project repository PUT endpoint' do |success_status, success_body|
+ context 'with valid project' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:project_visibility_level, :user_role, :member, :user_token, :expected_status, :expected_body) do
+ 'PUBLIC' | :developer | true | true | success_status | nil
+ 'PUBLIC' | :guest | true | true | :forbidden | nil
+ 'PUBLIC' | :developer | true | false | :unauthorized | nil
+ 'PUBLIC' | :guest | true | false | :unauthorized | nil
+ 'PUBLIC' | :developer | false | true | :forbidden | nil
+ 'PUBLIC' | :guest | false | true | :forbidden | nil
+ 'PUBLIC' | :developer | false | false | :unauthorized | nil
+ 'PUBLIC' | :guest | false | false | :unauthorized | nil
+ 'PUBLIC' | :anonymous | false | true | :unauthorized | nil
+ 'PRIVATE' | :developer | true | true | success_status | nil
+ 'PRIVATE' | :guest | true | true | :forbidden | nil
+ 'PRIVATE' | :developer | true | false | :not_found | nil
+ 'PRIVATE' | :guest | true | false | :not_found | nil
+ 'PRIVATE' | :developer | false | true | :not_found | nil
+ 'PRIVATE' | :guest | false | true | :not_found | nil
+ 'PRIVATE' | :developer | false | false | :not_found | nil
+ 'PRIVATE' | :guest | false | false | :not_found | nil
+ 'PRIVATE' | :anonymous | false | true | :not_found | nil
+ end
+
+ with_them do
+ include_context 'Debian repository project access', params[:project_visibility_level], params[:user_role], params[:user_token], :basic do
+ it_behaves_like 'Debian project repository PUT request', params[:user_role], params[:member], params[:expected_status], params[:expected_body]
+ end
+ end
+ end
+
+ it_behaves_like 'rejects Debian access with unknown project id'
+end
+
+RSpec.shared_context 'Debian repository group access' do |group_visibility_level, user_role, user_token, auth_method|
+ include_context 'Debian repository auth headers', user_role, user_token, auth_method do
+ before do
+ group.update_column(:visibility_level, Gitlab::VisibilityLevel.const_get(group_visibility_level, false))
+ end
+ end
+end
+
+RSpec.shared_examples 'Debian group repository GET request' do |user_role, add_member, status, body|
+ context "for user type #{user_role}" do
+ before do
+ group.send("add_#{user_role}", user) if add_member && user_role != :anonymous
+ end
+
+ and_body = body.nil? ? '' : ' and expected body'
+
+ it "returns #{status}#{and_body}" do
+ subject
+
+ expect(response).to have_gitlab_http_status(status)
+
+ unless body.nil?
+ expect(response.body).to eq(body)
+ end
+ end
+ end
+end
+
+RSpec.shared_examples 'rejects Debian access with unknown group id' do
+ context 'with an unknown group' do
+ let(:group) { double(id: non_existing_record_id) }
+
+ context 'as anonymous' do
+ it_behaves_like 'Debian group repository GET request', :anonymous, true, :not_found, nil
+ end
+
+ context 'as authenticated user' do
+ subject { get api(url), headers: basic_auth_header(user.username, personal_access_token.token) }
+
+ it_behaves_like 'Debian group repository GET request', :anonymous, true, :not_found, nil
+ end
+ end
+end
+
+RSpec.shared_examples 'Debian group repository GET endpoint' do |success_status, success_body|
+ context 'with valid group' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:group_visibility_level, :user_role, :member, :user_token, :expected_status, :expected_body) do
+ 'PUBLIC' | :developer | true | true | success_status | success_body
+ 'PUBLIC' | :guest | true | true | success_status | success_body
+ 'PUBLIC' | :developer | true | false | success_status | success_body
+ 'PUBLIC' | :guest | true | false | success_status | success_body
+ 'PUBLIC' | :developer | false | true | success_status | success_body
+ 'PUBLIC' | :guest | false | true | success_status | success_body
+ 'PUBLIC' | :developer | false | false | success_status | success_body
+ 'PUBLIC' | :guest | false | false | success_status | success_body
+ 'PUBLIC' | :anonymous | false | true | success_status | success_body
+ 'PRIVATE' | :developer | true | true | success_status | success_body
+ 'PRIVATE' | :guest | true | true | :forbidden | nil
+ 'PRIVATE' | :developer | true | false | :not_found | nil
+ 'PRIVATE' | :guest | true | false | :not_found | nil
+ 'PRIVATE' | :developer | false | true | :not_found | nil
+ 'PRIVATE' | :guest | false | true | :not_found | nil
+ 'PRIVATE' | :developer | false | false | :not_found | nil
+ 'PRIVATE' | :guest | false | false | :not_found | nil
+ 'PRIVATE' | :anonymous | false | true | :not_found | nil
+ end
+
+ with_them do
+ include_context 'Debian repository group access', params[:group_visibility_level], params[:user_role], params[:user_token], :basic do
+ it_behaves_like 'Debian group repository GET request', params[:user_role], params[:member], params[:expected_status], params[:expected_body]
+ end
+ end
+ end
+
+ it_behaves_like 'rejects Debian access with unknown group id'
+end
diff --git a/spec/support/shared_examples/requests/api/graphql/group_and_project_boards_query_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/group_and_project_boards_query_shared_examples.rb
index f26af6cb766..5145880ef9a 100644
--- a/spec/support/shared_examples/requests/api/graphql/group_and_project_boards_query_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/graphql/group_and_project_boards_query_shared_examples.rb
@@ -90,7 +90,7 @@ RSpec.shared_examples 'group and project boards query' do
it_behaves_like 'a working graphql query' do
before do
- post_graphql(query_single_board, current_user: current_user)
+ post_graphql(query_single_board("id: \"gid://gitlab/Board/1\""), current_user: current_user)
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 6aac51a5903..58e99776fd9 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
@@ -26,7 +26,7 @@ RSpec.shared_examples 'process nuget service index request' do |user_type, statu
it_behaves_like 'returning response status', status
- it_behaves_like 'a gitlab tracking event', described_class.name, 'nuget_service_index'
+ it_behaves_like 'a package tracking event', described_class.name, 'cli_metadata'
it 'returns a valid json response' do
subject
@@ -169,7 +169,7 @@ RSpec.shared_examples 'process nuget upload' do |user_type, status, add_member =
context 'with correct params' do
it_behaves_like 'package workhorse uploads'
it_behaves_like 'creates nuget package files'
- it_behaves_like 'a gitlab tracking event', described_class.name, 'push_package'
+ it_behaves_like 'a package tracking event', described_class.name, 'push_package'
end
end
@@ -286,7 +286,7 @@ RSpec.shared_examples 'process nuget download content request' do |user_type, st
it_behaves_like 'returning response status', status
- it_behaves_like 'a gitlab tracking event', described_class.name, 'pull_package'
+ it_behaves_like 'a package tracking event', described_class.name, 'pull_package'
it 'returns a valid package archive' do
subject
@@ -336,7 +336,7 @@ RSpec.shared_examples 'process nuget search request' do |user_type, status, add_
it_behaves_like 'returns a valid json search response', status, 4, [1, 5, 5, 1]
- it_behaves_like 'a gitlab tracking event', described_class.name, 'search_package'
+ it_behaves_like 'a package tracking event', described_class.name, 'search_package'
context 'with skip set to 2' do
let(:skip) { 2 }
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 c9a33701161..d730ed53109 100644
--- a/spec/support/shared_examples/requests/api/packages_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/packages_shared_examples.rb
@@ -126,3 +126,11 @@ RSpec.shared_examples 'job token for package uploads' do
end
end
end
+
+RSpec.shared_examples 'a package tracking event' do |category, action|
+ it "creates a gitlab tracking event #{action}" do
+ expect(Gitlab::Tracking).to receive(:event).with(category, action, {})
+
+ expect { subject }.to change { Packages::Event.count }.by(1)
+ 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 715c494840e..bbcf856350d 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
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-RSpec.shared_examples 'PyPi package creation' do |user_type, status, add_member = true|
+RSpec.shared_examples 'PyPI package creation' do |user_type, status, add_member = true|
RSpec.shared_examples 'creating pypi package files' do
it 'creates package files' do
expect { subject }
@@ -52,7 +52,7 @@ RSpec.shared_examples 'PyPi package creation' do |user_type, status, add_member
context 'with correct params' do
it_behaves_like 'package workhorse uploads'
it_behaves_like 'creating pypi package files'
- it_behaves_like 'a gitlab tracking event', described_class.name, 'push_package'
+ it_behaves_like 'a package tracking event', described_class.name, 'push_package'
end
end
@@ -106,7 +106,7 @@ RSpec.shared_examples 'PyPi package creation' do |user_type, status, add_member
end
end
-RSpec.shared_examples 'PyPi package versions' do |user_type, status, add_member = true|
+RSpec.shared_examples 'PyPI package versions' do |user_type, status, add_member = true|
context "for user type #{user_type}" do
before do
project.send("add_#{user_type}", user) if add_member && user_type != :anonymous
@@ -119,11 +119,11 @@ RSpec.shared_examples 'PyPi package versions' do |user_type, status, add_member
end
it_behaves_like 'returning response status', status
- it_behaves_like 'a gitlab tracking event', described_class.name, 'list_package'
+ it_behaves_like 'a package tracking event', described_class.name, 'list_package'
end
end
-RSpec.shared_examples 'PyPi package download' do |user_type, status, add_member = true|
+RSpec.shared_examples 'PyPI package download' do |user_type, status, add_member = true|
context "for user type #{user_type}" do
before do
project.send("add_#{user_type}", user) if add_member && user_type != :anonymous
@@ -136,11 +136,11 @@ RSpec.shared_examples 'PyPi package download' do |user_type, status, add_member
end
it_behaves_like 'returning response status', status
- it_behaves_like 'a gitlab tracking event', described_class.name, 'pull_package'
+ it_behaves_like 'a package tracking event', described_class.name, 'pull_package'
end
end
-RSpec.shared_examples 'process PyPi api request' do |user_type, status, add_member = true|
+RSpec.shared_examples 'process PyPI api request' do |user_type, status, add_member = true|
context "for user type #{user_type}" do
before do
project.send("add_#{user_type}", user) if add_member && user_type != :anonymous
@@ -155,13 +155,13 @@ RSpec.shared_examples 'rejects PyPI access with unknown project id' do
let(:project) { OpenStruct.new(id: 1234567890) }
context 'as anonymous' do
- it_behaves_like 'process PyPi api request', :anonymous, :not_found
+ it_behaves_like 'process PyPI api request', :anonymous, :not_found
end
context 'as authenticated user' do
subject { get api(url), headers: basic_auth_header(user.username, personal_access_token.token) }
- it_behaves_like 'process PyPi api request', :anonymous, :not_found
+ it_behaves_like 'process PyPI api request', :anonymous, :not_found
end
end
end
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 051367fbe96..2b72c69cb37 100644
--- a/spec/support/shared_examples/requests/api/snippets_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/snippets_shared_examples.rb
@@ -1,46 +1,30 @@
# frozen_string_literal: true
RSpec.shared_examples 'raw snippet files' do
- let_it_be(:unauthorized_user) { create(:user) }
+ let_it_be(:user_token) { create(:personal_access_token, user: snippet.author) }
let(:snippet_id) { snippet.id }
let(:user) { snippet.author }
let(:file_path) { '%2Egitattributes' }
let(:ref) { 'master' }
- context 'with no user' do
- it 'requires authentication' do
- get api(api_path)
+ subject { get api(api_path, personal_access_token: user_token) }
- expect(response).to have_gitlab_http_status(:unauthorized)
- end
- end
+ context 'with an invalid snippet ID' do
+ let(:snippet_id) { non_existing_record_id }
- shared_examples 'not found' do
it 'returns 404' do
- get api(api_path, user)
+ subject
expect(response).to have_gitlab_http_status(:not_found)
expect(json_response['message']).to eq('404 Snippet Not Found')
end
end
- context 'when not authorized' do
- let(:user) { unauthorized_user }
-
- it_behaves_like 'not found'
- end
-
- context 'with an invalid snippet ID' do
- let(:snippet_id) { 'invalid' }
-
- it_behaves_like 'not found'
- end
-
context 'with valid params' do
it 'returns the raw file info' do
expect(Gitlab::Workhorse).to receive(:send_git_blob).and_call_original
- get api(api_path, user)
+ subject
aggregate_failures do
expect(response).to have_gitlab_http_status(:ok)
@@ -52,6 +36,17 @@ RSpec.shared_examples 'raw snippet files' do
end
end
+ context 'with unauthorized user' do
+ let(:user_token) { create(:personal_access_token) }
+
+ it 'returns 404' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect(json_response['message']).to eq('404 Snippet Not Found')
+ end
+ end
+
context 'with invalid params' do
using RSpec::Parameterized::TableSyntax
@@ -68,12 +63,12 @@ RSpec.shared_examples 'raw snippet files' do
end
with_them do
- before do
- get api(api_path, user)
- end
+ it 'returns the proper response code and message' do
+ subject
- it { expect(response).to have_gitlab_http_status(status) }
- it { expect(json_response[key]).to eq(message) }
+ expect(response).to have_gitlab_http_status(status)
+ expect(json_response[key]).to eq(message)
+ end
end
end
end
@@ -216,3 +211,133 @@ RSpec.shared_examples 'invalid snippet updates' do
expect(json_response['error']).to eq 'title is empty'
end
end
+
+RSpec.shared_examples 'snippet access with different users' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:requester, :visibility, :status) do
+ :admin | :public | :ok
+ :admin | :private | :ok
+ :admin | :internal | :ok
+ :author | :public | :ok
+ :author | :private | :ok
+ :author | :internal | :ok
+ :other | :public | :ok
+ :other | :private | :not_found
+ :other | :internal | :ok
+ nil | :public | :ok
+ nil | :private | :not_found
+ nil | :internal | :not_found
+ end
+
+ with_them do
+ let(:snippet) { snippet_for(visibility) }
+
+ it 'returns the correct response' do
+ request_user = user_for(requester)
+
+ get api(path, request_user)
+
+ expect(response).to have_gitlab_http_status(status)
+ end
+ end
+
+ def user_for(user_type)
+ case user_type
+ when :author
+ user
+ when :other
+ other_user
+ when :admin
+ admin
+ else
+ nil
+ end
+ end
+
+ def snippet_for(snippet_type)
+ case snippet_type
+ when :private
+ private_snippet
+ when :internal
+ internal_snippet
+ when :public
+ public_snippet
+ end
+ end
+end
+
+RSpec.shared_examples 'expected response status' do
+ it 'returns the correct response' do
+ get api(path, personal_access_token: user_token)
+
+ expect(response).to have_gitlab_http_status(status)
+ end
+end
+
+RSpec.shared_examples 'unauthenticated project snippet access' do
+ using RSpec::Parameterized::TableSyntax
+
+ let(:user_token) { nil }
+
+ where(:project_visibility, :snippet_visibility, :status) do
+ :public | :public | :ok
+ :public | :private | :not_found
+ :public | :internal | :not_found
+ :internal | :public | :not_found
+ :private | :public | :not_found
+ end
+
+ with_them do
+ it_behaves_like 'expected response status'
+ end
+end
+
+RSpec.shared_examples 'non-member project snippet access' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:project_visibility, :snippet_visibility, :status) do
+ :public | :public | :ok
+ :public | :internal | :ok
+ :internal | :public | :ok
+ :public | :private | :not_found
+ :private | :public | :not_found
+ end
+
+ with_them do
+ it_behaves_like 'expected response status'
+ end
+end
+
+RSpec.shared_examples 'member project snippet access' do
+ using RSpec::Parameterized::TableSyntax
+
+ before do
+ project.add_guest(user)
+ end
+
+ where(:project_visibility, :snippet_visibility, :status) do
+ :public | :public | :ok
+ :public | :internal | :ok
+ :internal | :public | :ok
+ :public | :private | :ok
+ :private | :public | :ok
+ end
+
+ with_them do
+ it_behaves_like 'expected response status'
+ end
+end
+
+RSpec.shared_examples 'project snippet access levels' do
+ let_it_be(:user_token) { create(:personal_access_token, user: user) }
+
+ let(:project) { create(:project, project_visibility) }
+ let(:snippet) { create(:project_snippet, :repository, snippet_visibility, project: project) }
+
+ it_behaves_like 'unauthenticated project snippet access'
+
+ it_behaves_like 'non-member project snippet access'
+
+ it_behaves_like 'member project snippet access'
+end
diff --git a/spec/support/shared_examples/requests/api/tracking_shared_examples.rb b/spec/support/shared_examples/requests/api/tracking_shared_examples.rb
new file mode 100644
index 00000000000..2e6feae3f98
--- /dev/null
+++ b/spec/support/shared_examples/requests/api/tracking_shared_examples.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'a gitlab tracking event' do |category, action|
+ it "creates a gitlab tracking event #{action}" do
+ expect(Gitlab::Tracking).to receive(:event).with(category, action, {})
+
+ subject
+ end
+end
diff --git a/spec/support/shared_examples/requests/rack_attack_shared_examples.rb b/spec/support/shared_examples/requests/rack_attack_shared_examples.rb
index 08ccbd4a9c1..730df4dc5ab 100644
--- a/spec/support/shared_examples/requests/rack_attack_shared_examples.rb
+++ b/spec/support/shared_examples/requests/rack_attack_shared_examples.rb
@@ -48,7 +48,7 @@ RSpec.shared_examples 'rate-limited token-authenticated requests' do
expect_rejection { make_request(request_args) }
- Timecop.travel(period.from_now) do
+ travel_to(period.from_now) do
requests_per_period.times do
make_request(request_args)
expect(response).not_to have_gitlab_http_status(:too_many_requests)
@@ -175,7 +175,7 @@ RSpec.shared_examples 'rate-limited web authenticated requests' do
expect_rejection { request_authenticated_web_url }
- Timecop.travel(period.from_now) do
+ travel_to(period.from_now) do
requests_per_period.times do
request_authenticated_web_url
expect(response).not_to have_gitlab_http_status(:too_many_requests)
diff --git a/spec/support/shared_examples/requests/snippet_shared_examples.rb b/spec/support/shared_examples/requests/snippet_shared_examples.rb
index 84ef7723b9b..dae3a3e74be 100644
--- a/spec/support/shared_examples/requests/snippet_shared_examples.rb
+++ b/spec/support/shared_examples/requests/snippet_shared_examples.rb
@@ -99,18 +99,6 @@ RSpec.shared_examples 'snippet blob content' do
end
end
-RSpec.shared_examples 'snippet_multiple_files feature disabled' do
- before do
- stub_feature_flags(snippet_multiple_files: false)
-
- subject
- end
-
- it 'does not return files attributes' do
- expect(json_response).not_to have_key('files')
- end
-end
-
RSpec.shared_examples 'snippet creation with files parameter' do
using RSpec::Parameterized::TableSyntax
diff --git a/spec/support/shared_examples/requests/user_activity_shared_examples.rb b/spec/support/shared_examples/requests/user_activity_shared_examples.rb
new file mode 100644
index 00000000000..37da1ce5c63
--- /dev/null
+++ b/spec/support/shared_examples/requests/user_activity_shared_examples.rb
@@ -0,0 +1,97 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'updating of user activity' do |paths_to_visit|
+ let(:user) { create(:user, last_activity_on: nil) }
+
+ before do
+ group = create(:group, name: 'group')
+ project = create(:project, :public, namespace: group, name: 'project')
+
+ create(:issue, project: project, iid: 10)
+ create(:merge_request, source_project: project, iid: 15)
+
+ project.add_maintainer(user)
+ end
+
+ context 'without an authenticated user' do
+ it 'does not set the last activity cookie' do
+ get "/group/project"
+
+ expect(response.cookies['user_last_activity_on']).to be_nil
+ end
+ end
+
+ context 'with an authenticated user' do
+ before do
+ login_as(user)
+ end
+
+ context 'with a POST request' do
+ it 'does not set the last activity cookie' do
+ post "/group/project/archive"
+
+ expect(response.cookies['user_last_activity_on']).to be_nil
+ end
+ end
+
+ paths_to_visit.each do |path|
+ context "on GET to #{path}" do
+ it 'updates the last activity date' do
+ expect(Users::ActivityService).to receive(:new).and_call_original
+
+ get path
+
+ expect(user.last_activity_on).to eq(Date.today)
+ end
+
+ context 'when calling it twice' do
+ it 'updates last_activity_on just once' do
+ expect(Users::ActivityService).to receive(:new).once.and_call_original
+
+ 2.times do
+ get path
+ end
+ end
+ end
+
+ context 'when last_activity_on is nil' do
+ before do
+ user.update_attribute(:last_activity_on, nil)
+ end
+
+ it 'updates the last activity date' do
+ expect(user.last_activity_on).to be_nil
+
+ get path
+
+ expect(user.last_activity_on).to eq(Date.today)
+ end
+ end
+
+ context 'when last_activity_on is stale' do
+ before do
+ user.update_attribute(:last_activity_on, 2.days.ago.to_date)
+ end
+
+ it 'updates the last activity date' do
+ get path
+
+ expect(user.last_activity_on).to eq(Date.today)
+ end
+ end
+
+ context 'when last_activity_on is up to date' do
+ before do
+ user.update_attribute(:last_activity_on, Date.today)
+ end
+
+ it 'does not try to update it' do
+ expect(Users::ActivityService).not_to receive(:new)
+
+ get path
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/serializers/note_entity_shared_examples.rb b/spec/support/shared_examples/serializers/note_entity_shared_examples.rb
index 7b2ec02c7b6..a90a2dc3667 100644
--- a/spec/support/shared_examples/serializers/note_entity_shared_examples.rb
+++ b/spec/support/shared_examples/serializers/note_entity_shared_examples.rb
@@ -24,7 +24,7 @@ RSpec.shared_examples 'note entity' do
context 'when note was edited' do
before do
- note.update(updated_at: 1.minute.from_now, updated_by: user)
+ note.update!(updated_at: 1.minute.from_now, updated_by: user)
end
it 'exposes last_edited_at and last_edited_by elements' do
@@ -34,7 +34,7 @@ RSpec.shared_examples 'note entity' do
context 'when note is a system note' do
before do
- note.update(system: true)
+ note.update!(system: true)
end
it 'exposes system_note_icon_name element' do
diff --git a/spec/support/shared_examples/services/boards/boards_create_service_shared_examples.rb b/spec/support/shared_examples/services/boards/boards_create_service_shared_examples.rb
index fced2e59ace..f28c78aec97 100644
--- a/spec/support/shared_examples/services/boards/boards_create_service_shared_examples.rb
+++ b/spec/support/shared_examples/services/boards/boards_create_service_shared_examples.rb
@@ -7,7 +7,7 @@ RSpec.shared_examples 'boards create service' do
end
it 'creates the default lists' do
- board = service.execute
+ board = service.execute.payload
expect(board.lists.size).to eq 2
expect(board.lists.first).to be_backlog
diff --git a/spec/support/shared_examples/services/boards/issues_move_service_shared_examples.rb b/spec/support/shared_examples/services/boards/issues_move_service_shared_examples.rb
index f352b430cc7..4aa5d7d890b 100644
--- a/spec/support/shared_examples/services/boards/issues_move_service_shared_examples.rb
+++ b/spec/support/shared_examples/services/boards/issues_move_service_shared_examples.rb
@@ -131,7 +131,7 @@ RSpec.shared_examples 'issues move service' do |group|
updated_at1 = issue1.updated_at
updated_at2 = issue2.updated_at
- Timecop.travel(1.minute.from_now) do
+ travel_to(1.minute.from_now) do
described_class.new(parent, user, params).execute(issue)
end
diff --git a/spec/support/shared_examples/services/incident_shared_examples.rb b/spec/support/shared_examples/services/incident_shared_examples.rb
index d6e79931df5..39c22ac8aa3 100644
--- a/spec/support/shared_examples/services/incident_shared_examples.rb
+++ b/spec/support/shared_examples/services/incident_shared_examples.rb
@@ -45,3 +45,74 @@ RSpec.shared_examples 'not an incident issue' do
expect(issue.labels).not_to include(have_attributes(label_properties))
end
end
+
+# This shared example is to test the execution of incident management label services
+# For example:
+# - IncidentManagement::CreateIncidentSlaExceededLabelService
+# - IncidentManagement::CreateIncidentLabelService
+
+# It doesn't require any defined variables
+
+RSpec.shared_examples 'incident management label service' do
+ let_it_be(:project) { create(:project, :private) }
+ let_it_be(:user) { User.alert_bot }
+ let(:service) { described_class.new(project, user) }
+
+ subject(:execute) { service.execute }
+
+ describe 'execute' do
+ let(:incident_label_attributes) { described_class::LABEL_PROPERTIES }
+ let(:title) { incident_label_attributes[:title] }
+ let(:color) { incident_label_attributes[:color] }
+ let(:description) { incident_label_attributes[:description] }
+
+ shared_examples 'existing label' do
+ it 'returns the existing label' do
+ expect { execute }.not_to change(Label, :count)
+
+ expect(execute).to be_success
+ expect(execute.payload).to eq(label: label)
+ end
+ end
+
+ shared_examples 'new label' do
+ it 'creates a new label' do
+ expect { execute }.to change(Label, :count).by(1)
+
+ label = project.reload.labels.last
+ expect(execute).to be_success
+ expect(execute.payload).to eq(label: label)
+ expect(label.title).to eq(title)
+ expect(label.color).to eq(color)
+ expect(label.description).to eq(description)
+ end
+ end
+
+ context 'with predefined project label' do
+ it_behaves_like 'existing label' do
+ let!(:label) { create(:label, project: project, title: title) }
+ end
+ end
+
+ context 'with predefined group label' do
+ let(:project) { create(:project, group: group) }
+ let(:group) { create(:group) }
+
+ it_behaves_like 'existing label' do
+ let!(:label) { create(:group_label, group: group, title: title) }
+ end
+ end
+
+ context 'without label' do
+ context 'when user has permissions to create labels' do
+ it_behaves_like 'new label'
+ end
+
+ context 'when user has no permissions to create labels' do
+ let_it_be(:user) { create(:user) }
+
+ it_behaves_like 'new label'
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/services/merge_request_shared_examples.rb b/spec/support/shared_examples/services/merge_request_shared_examples.rb
index a7032640217..2bd06ac3e9c 100644
--- a/spec/support/shared_examples/services/merge_request_shared_examples.rb
+++ b/spec/support/shared_examples/services/merge_request_shared_examples.rb
@@ -13,11 +13,10 @@ RSpec.shared_examples 'reviewer_ids filter' do
end
context 'with reviewer_ids' do
- let(:reviewer_ids_param) { { reviewer_ids: [reviewer1.id, reviewer2.id, reviewer3.id] } }
+ let(:reviewer_ids_param) { { reviewer_ids: [reviewer1.id, reviewer2.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
@@ -25,14 +24,13 @@ RSpec.shared_examples 'reviewer_ids filter' do
stub_feature_flags(merge_request_reviewer: true)
end
- context 'with reviewers who can read the merge_request' do
+ context 'with a reviewer 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)
+ expect(execute.reviewers).to contain_exactly(reviewer1)
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 7fd59c3d963..65f4b3b5513 100644
--- a/spec/support/shared_examples/services/packages_shared_examples.rb
+++ b/spec/support/shared_examples/services/packages_shared_examples.rb
@@ -170,6 +170,8 @@ RSpec.shared_examples 'filters on each package_type' do |is_project: false|
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) }
+ let_it_be(:package8) { create(:golang_package, project: project) }
+ let_it_be(:package9) { create(:debian_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/projects/urls_with_escaped_elements_shared_example.rb b/spec/support/shared_examples/services/projects/urls_with_escaped_elements_shared_example.rb
new file mode 100644
index 00000000000..df8b1f91629
--- /dev/null
+++ b/spec/support/shared_examples/services/projects/urls_with_escaped_elements_shared_example.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+# Shared examples that test requests against URLs with escaped elements
+#
+RSpec.shared_examples "URLs containing escaped elements return expected status" do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:url, :result_status) do
+ "https://user:0a%23@test.example.com/project.git" | :success
+ "https://git.example.com:1%2F%2F@source.developers.google.com/project.git" | :success
+ CGI.escape("git://localhost:1234/some-path?some-query=some-val\#@example.com/") | :error
+ CGI.escape(CGI.escape("https://user:0a%23@test.example.com/project.git")) | :error
+ end
+
+ with_them do
+ it "returns expected status" do
+ expect(result[:status]).to eq(result_status)
+ end
+ end
+end
diff --git a/spec/support/shared_examples/validators/ip_address_validator_shared_examples.rb b/spec/support/shared_examples/validators/ip_address_validator_shared_examples.rb
new file mode 100644
index 00000000000..5680d4f772c
--- /dev/null
+++ b/spec/support/shared_examples/validators/ip_address_validator_shared_examples.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'validates IP address' do
+ subject { object }
+
+ it { is_expected.to allow_value('192.168.17.43').for(attribute.to_sym) }
+ it { is_expected.to allow_value('2001:0db8:85a3:0000:0000:8a2e:0370:7334').for(attribute.to_sym) }
+
+ it { is_expected.not_to allow_value('invalid IP').for(attribute.to_sym) }
+end
diff --git a/spec/support/test_reports/test_reports_helper.rb b/spec/support/test_reports/test_reports_helper.rb
index ad9ecb6f460..18b40a20cf1 100644
--- a/spec/support/test_reports/test_reports_helper.rb
+++ b/spec/support/test_reports/test_reports_helper.rb
@@ -3,6 +3,7 @@
module TestReportsHelper
def create_test_case_rspec_success(name = 'test_spec')
Gitlab::Ci::Reports::TestCase.new(
+ suite_name: 'rspec',
name: 'Test#sum when a is 1 and b is 3 returns summary',
classname: "spec.#{name}",
file: './spec/test_spec.rb',
@@ -12,6 +13,7 @@ module TestReportsHelper
def create_test_case_rspec_failed(name = 'test_spec', execution_time = 2.22)
Gitlab::Ci::Reports::TestCase.new(
+ suite_name: 'rspec',
name: 'Test#sum when a is 1 and b is 3 returns summary',
classname: "spec.#{name}",
file: './spec/test_spec.rb',
@@ -22,6 +24,7 @@ module TestReportsHelper
def create_test_case_rspec_skipped(name = 'test_spec')
Gitlab::Ci::Reports::TestCase.new(
+ suite_name: 'rspec',
name: 'Test#sum when a is 3 and b is 3 returns summary',
classname: "spec.#{name}",
file: './spec/test_spec.rb',
@@ -31,6 +34,7 @@ module TestReportsHelper
def create_test_case_rspec_error(name = 'test_spec')
Gitlab::Ci::Reports::TestCase.new(
+ suite_name: 'rspec',
name: 'Test#sum when a is 4 and b is 4 returns summary',
classname: "spec.#{name}",
file: './spec/test_spec.rb',
@@ -52,6 +56,7 @@ module TestReportsHelper
def create_test_case_java_success(name = 'addTest')
Gitlab::Ci::Reports::TestCase.new(
+ suite_name: 'java',
name: name,
classname: 'CalculatorTest',
execution_time: 5.55,
@@ -60,6 +65,7 @@ module TestReportsHelper
def create_test_case_java_failed(name = 'addTest')
Gitlab::Ci::Reports::TestCase.new(
+ suite_name: 'java',
name: name,
classname: 'CalculatorTest',
execution_time: 6.66,
@@ -69,6 +75,7 @@ module TestReportsHelper
def create_test_case_java_skipped(name = 'addTest')
Gitlab::Ci::Reports::TestCase.new(
+ suite_name: 'java',
name: name,
classname: 'CalculatorTest',
execution_time: 7.77,
@@ -77,6 +84,7 @@ module TestReportsHelper
def create_test_case_java_error(name = 'addTest')
Gitlab::Ci::Reports::TestCase.new(
+ suite_name: 'java',
name: name,
classname: 'CalculatorTest',
execution_time: 8.88,