diff options
Diffstat (limited to 'spec/support')
66 files changed, 2597 insertions, 205 deletions
diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb index 66fce4fddf1..27c360ee39a 100644 --- a/spec/support/capybara.rb +++ b/spec/support/capybara.rb @@ -123,6 +123,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..286f3c03357 100644 --- a/spec/support/google_api/cloud_platform_helpers.rb +++ b/spec/support/google_api/cloud_platform_helpers.rb @@ -24,7 +24,7 @@ module GoogleApi 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))) + .to_return(cloud_platform_response(cloud_platform_cluster_body(**options))) end def stub_cloud_platform_get_zone_cluster_error(project_id, zone, cluster_id) 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..058d07c7a1d --- /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.project) + 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/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/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..e754a24417c 100644 --- a/spec/support/helpers/graphql_helpers.rb +++ b/spec/support/helpers/graphql_helpers.rb @@ -234,7 +234,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/kubernetes_helpers.rb b/spec/support/helpers/kubernetes_helpers.rb index 8882f31e2f4..16d2e84cc0f 100644 --- a/spec/support/helpers/kubernetes_helpers.rb +++ b/spec/support/helpers/kubernetes_helpers.rb @@ -153,7 +153,7 @@ module KubernetesHelpers 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) @@ -167,7 +167,7 @@ module KubernetesHelpers options[:namespace] ||= "default" WebMock.stub_request(:get, api_url + "/api/v1/namespaces/#{options[:namespace]}/secrets/#{options[:metadata_name]}") - .to_return(kube_response(kube_v1_secret_body(options))) + .to_return(kube_response(kube_v1_secret_body(**options))) end def stub_kubeclient_get_secret_error(api_url, name, namespace: 'default', status: 404) @@ -517,7 +517,7 @@ module KubernetesHelpers def kube_knative_services_body(**options) { "kind" => "List", - "items" => [knative_09_service(options)] + "items" => [knative_09_service(**options)] } end @@ -604,7 +604,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 +617,7 @@ module KubernetesHelpers "track" => track }.compact }, - "spec" => { "replicas" => 3 }, + "spec" => { "replicas" => replicas }, "status" => { "observedGeneration" => 4 } 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/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/usage_data_helpers.rb b/spec/support/helpers/usage_data_helpers.rb index d92fcdc2d4a..17e806d21d9 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( @@ -228,7 +230,7 @@ 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 yield 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/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_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/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/wiki_actions_shared_examples.rb b/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb index 4ca400dd87b..1cf31c47df8 100644 --- a/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb +++ b/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb @@ -226,7 +226,7 @@ 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 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..218f1afce98 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 @@ -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/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/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..cf6ae6cfa5b --- /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('.js-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..fee455207f1 --- /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('.nav-text')).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..40998ebde4c --- /dev/null +++ b/spec/support/shared_examples/features/wiki/user_views_wiki_page_shared_examples.rb @@ -0,0 +1,278 @@ +# 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('.nav-text')).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('a.btn', text: 'Edit') + + 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('a.btn', text: 'Edit') + end + + context 'show the diff' do + before do + skip('Diffing for group wikis will be implemented in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/42610') if wiki.container.is_a?(Group) + end + + 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('.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_css('.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('a.btn', text: 'Edit') + 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/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/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/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..6c8ab38413d --- /dev/null +++ b/spec/support/shared_examples/lib/gitlab/search_confidential_filter_shared_examples.rb @@ -0,0 +1,91 @@ +# 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: 'yes' } } + + 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: 'no' } } + + 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 + + context 'unsupported filter' do + let(:filters) { { confidential: 'goodbye' } } + + 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 +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/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/relative_positioning_shared_examples.rb b/spec/support/shared_examples/models/relative_positioning_shared_examples.rb index d1437244082..7cf014e8023 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 @@ -120,16 +155,16 @@ RSpec.shared_examples 'a class that supports relative positioning' do 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 +184,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 +687,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/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..336a734a25f 100644 --- a/spec/support/shared_examples/models/wiki_shared_examples.rb +++ b/spec/support/shared_examples/models/wiki_shared_examples.rb @@ -164,7 +164,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 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/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..40bedc84366 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 @@ -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 @@ -119,7 +119,7 @@ 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 @@ -136,7 +136,7 @@ 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 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/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/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..c00a087311c 100644 --- a/spec/support/shared_examples/services/packages_shared_examples.rb +++ b/spec/support/shared_examples/services/packages_shared_examples.rb @@ -170,6 +170,7 @@ 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) } 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 |