From 6e4e1050d9dba2b7b2523fdd1768823ab85feef4 Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Thu, 20 Aug 2020 18:42:06 +0000 Subject: Add latest changes from gitlab-org/gitlab@13-3-stable-ee --- spec/support/counter_attribute.rb | 14 + spec/support/csv_response.rb | 5 + spec/support/gitlab_stubs/gitlab_ci_for_sast.yml | 13 + spec/support/helpers/bare_repo_operations.rb | 2 +- spec/support/helpers/cycle_analytics_helpers.rb | 2 +- .../helpers/design_management_test_helpers.rb | 4 +- spec/support/helpers/filtered_search_helpers.rb | 2 +- spec/support/helpers/http_basic_auth_helpers.rb | 17 +- spec/support/helpers/jira_service_helper.rb | 2 +- spec/support/helpers/login_helpers.rb | 2 +- spec/support/helpers/memory_usage_helper.rb | 2 +- spec/support/helpers/metrics_dashboard_helpers.rb | 18 +- .../helpers/metrics_dashboard_url_helpers.rb | 10 + spec/support/helpers/navbar_structure_helper.rb | 18 + spec/support/helpers/notification_helpers.rb | 4 +- .../helpers/packages_manager_api_spec_helper.rb | 12 - spec/support/helpers/require_migration.rb | 31 ++ spec/support/helpers/stub_configuration.rb | 4 +- spec/support/helpers/stub_feature_flags.rb | 31 +- spec/support/helpers/stub_object_storage.rb | 13 +- spec/support/helpers/stubbed_feature.rb | 49 ++ spec/support/helpers/test_env.rb | 6 +- spec/support/helpers/trigger_helpers.rb | 2 - spec/support/helpers/usage_data_helpers.rb | 1 + spec/support/helpers/wait_for_requests.rb | 6 +- spec/support/matchers/exceed_query_limit.rb | 4 +- .../track_untracked_uploads_helpers.rb | 130 ----- spec/support/protected_branch_helpers.rb | 5 + .../change_access_checks_shared_context.rb | 2 +- .../shared_contexts/csv_response_shared_context.rb | 5 + .../merge_requests_finder_shared_contexts.rb | 6 + .../lib/gitlab/git_access_shared_examples.rb | 17 + .../shared_contexts/navbar_structure_context.rb | 3 + .../policies/group_policy_shared_context.rb | 3 + .../prometheus/alert_shared_context.rb | 2 +- .../read_ci_configuration_shared_context.rb | 9 + .../graphql/jira_import/jira_projects_context.rb | 2 +- .../delete_tags_service_shared_context.rb | 78 +++ .../alert_notification_service_shared_examples.rb | 29 + .../controllers/binary_blob_shared_examples.rb | 34 +- .../graceful_timeout_handling_shared_examples.rb | 7 + .../githubish_import_controller_shared_examples.rb | 83 ++- .../prometheus_api_proxy_shared_examples.rb | 1 + .../controllers/variables_shared_examples.rb | 1 + .../controllers/wiki_actions_shared_examples.rb | 54 ++ .../create_alert_issue_shared_examples.rb | 27 - ...aster_manages_access_requests_shared_example.rb | 21 +- .../features/packages_shared_examples.rb | 113 ++++ ...d_branches_access_control_ce_shared_examples.rb | 8 +- .../features/rss_shared_examples.rb | 6 +- .../features/snippets_shared_examples.rb | 222 ++++++++ .../finders/snippet_visibility_shared_examples.rb | 386 ++++++------- .../graphql/design_fields_shared_examples.rb | 1 + .../resolves_subscription_shared_examples.rb | 45 ++ .../mutations/set_assignees_shared_examples.rb | 126 +++++ .../graphql/notes_on_noteables_shared_examples.rb | 1 + .../merge_request_n_plus_one_query_examples.rb | 11 + .../lib/api/ci/runner_shared_examples.rb | 22 + .../mentions_migration_shared_examples.rb | 22 + .../network_policy_common_shared_examples.rb | 160 ++++++ .../gitlab/template/template_shared_examples.rb | 49 ++ .../models/chat_service_shared_examples.rb | 2 + ...luster_application_helm_cert_shared_examples.rb | 38 +- ...r_application_initial_status_shared_examples.rb | 42 +- .../cluster_application_status_shared_examples.rb | 152 ++---- .../concerns/counter_attribute_shared_examples.rb | 176 ++++++ .../concerns/file_store_mounter_shared_examples.rb | 17 + .../models/concerns/timebox_shared_examples.rb | 41 +- .../models/issuable_hook_data_shared_examples.rb | 1 + .../models/relative_positioning_shared_examples.rb | 602 ++++++++++++++++++--- .../models/resource_event_shared_examples.rb | 155 ++++++ .../resource_timebox_event_shared_examples.rb | 75 +++ .../update_project_statistics_shared_examples.rb | 24 +- .../path_extraction_shared_examples.rb | 68 ++- .../policies/project_policy_shared_examples.rb | 15 +- .../api/composer_packages_shared_examples.rb | 14 +- .../mutations/subscription_shared_examples.rb | 69 +++ .../requests/api/milestones_shared_examples.rb | 1 + .../requests/api/notes_shared_examples.rb | 61 ++- .../requests/api/nuget_packages_shared_examples.rb | 5 +- .../requests/api/packages_shared_examples.rb | 8 +- .../requests/api/pypi_packages_shared_examples.rb | 17 +- .../requests/snippet_shared_examples.rb | 77 +++ spec/support/shared_examples/resource_events.rb | 155 ------ .../boards/issues_list_service_shared_examples.rb | 22 + .../boards/lists_list_service_shared_examples.rb | 18 + .../user_mapper_services_shared_examples.rb | 39 ++ .../services/metrics/dashboard_shared_examples.rb | 8 +- ...e_repository_storage_service_shared_examples.rb | 41 +- .../change_milestone_service_shared_examples.rb | 52 +- .../wiki_pages/create_service_shared_examples.rb | 11 +- .../create_attachment_service_shared_examples.rb | 1 + .../snippet_blob_shared_examples.rb | 21 + 93 files changed, 3010 insertions(+), 983 deletions(-) create mode 100644 spec/support/counter_attribute.rb create mode 100644 spec/support/csv_response.rb create mode 100644 spec/support/gitlab_stubs/gitlab_ci_for_sast.yml create mode 100644 spec/support/helpers/require_migration.rb create mode 100644 spec/support/helpers/stubbed_feature.rb delete mode 100644 spec/support/migrations_helpers/track_untracked_uploads_helpers.rb create mode 100644 spec/support/shared_contexts/csv_response_shared_context.rb create mode 100644 spec/support/shared_contexts/lib/gitlab/git_access_shared_examples.rb create mode 100644 spec/support/shared_contexts/read_ci_configuration_shared_context.rb create mode 100644 spec/support/shared_contexts/services/projects/container_repository/delete_tags_service_shared_context.rb create mode 100644 spec/support/shared_examples/alert_notification_service_shared_examples.rb create mode 100644 spec/support/shared_examples/controllers/concerns/graceful_timeout_handling_shared_examples.rb delete mode 100644 spec/support/shared_examples/create_alert_issue_shared_examples.rb create mode 100644 spec/support/shared_examples/features/packages_shared_examples.rb create mode 100644 spec/support/shared_examples/graphql/mutations/resolves_subscription_shared_examples.rb create mode 100644 spec/support/shared_examples/graphql/mutations/set_assignees_shared_examples.rb create mode 100644 spec/support/shared_examples/graphql/projects/merge_request_n_plus_one_query_examples.rb create mode 100644 spec/support/shared_examples/lib/api/ci/runner_shared_examples.rb create mode 100644 spec/support/shared_examples/lib/gitlab/kubernetes/network_policy_common_shared_examples.rb create mode 100644 spec/support/shared_examples/lib/gitlab/template/template_shared_examples.rb create mode 100644 spec/support/shared_examples/models/concerns/counter_attribute_shared_examples.rb create mode 100644 spec/support/shared_examples/models/concerns/file_store_mounter_shared_examples.rb create mode 100644 spec/support/shared_examples/models/resource_event_shared_examples.rb create mode 100644 spec/support/shared_examples/models/resource_timebox_event_shared_examples.rb create mode 100644 spec/support/shared_examples/requests/api/graphql/mutations/subscription_shared_examples.rb delete mode 100644 spec/support/shared_examples/resource_events.rb create mode 100644 spec/support/shared_examples/services/jira_import/user_mapper_services_shared_examples.rb (limited to 'spec/support') diff --git a/spec/support/counter_attribute.rb b/spec/support/counter_attribute.rb new file mode 100644 index 00000000000..ea71b25b4c0 --- /dev/null +++ b/spec/support/counter_attribute.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +RSpec.configure do |config| + config.before(:each, :counter_attribute) do + stub_const('CounterAttributeModel', Class.new(ProjectStatistics)) + + CounterAttributeModel.class_eval do + include CounterAttribute + + counter_attribute :build_artifacts_size + counter_attribute :commit_count + end + end +end diff --git a/spec/support/csv_response.rb b/spec/support/csv_response.rb new file mode 100644 index 00000000000..9ed76dcdd4a --- /dev/null +++ b/spec/support/csv_response.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +RSpec.configure do |config| + config.include_context 'CSV response', type: :controller +end diff --git a/spec/support/gitlab_stubs/gitlab_ci_for_sast.yml b/spec/support/gitlab_stubs/gitlab_ci_for_sast.yml new file mode 100644 index 00000000000..4134660e4b9 --- /dev/null +++ b/spec/support/gitlab_stubs/gitlab_ci_for_sast.yml @@ -0,0 +1,13 @@ +include: + - template: SAST.gitlab-ci.yml + +variables: + SECURE_ANALYZERS_PREFIX: "registry.gitlab.com/gitlab-org/security-products/analyzers2" + SAST_EXCLUDED_PATHS: "spec, executables" + +stages: + - our_custom_security_stage +sast: + stage: our_custom_security_stage + variables: + SEARCH_MAX_DEPTH: 8 diff --git a/spec/support/helpers/bare_repo_operations.rb b/spec/support/helpers/bare_repo_operations.rb index 099610f087d..98fa13db6c2 100644 --- a/spec/support/helpers/bare_repo_operations.rb +++ b/spec/support/helpers/bare_repo_operations.rb @@ -44,7 +44,7 @@ class BareRepoOperations yield stdin if block_given? end - unless status.zero? + unless status == 0 if allow_failure return [] else diff --git a/spec/support/helpers/cycle_analytics_helpers.rb b/spec/support/helpers/cycle_analytics_helpers.rb index d101b092e7d..f4343b8b783 100644 --- a/spec/support/helpers/cycle_analytics_helpers.rb +++ b/spec/support/helpers/cycle_analytics_helpers.rb @@ -37,7 +37,7 @@ module CycleAnalyticsHelpers end def create_cycle(user, project, issue, mr, milestone, pipeline) - issue.update(milestone: milestone) + issue.update!(milestone: milestone) pipeline.run ci_build = create(:ci_build, pipeline: pipeline, status: :success, author: user) diff --git a/spec/support/helpers/design_management_test_helpers.rb b/spec/support/helpers/design_management_test_helpers.rb index 1daa92e8ad4..db217250b17 100644 --- a/spec/support/helpers/design_management_test_helpers.rb +++ b/spec/support/helpers/design_management_test_helpers.rb @@ -35,9 +35,9 @@ module DesignManagementTestHelpers def act_on_designs(designs, &block) issue = designs.first.issue - version = build(:design_version, :empty, issue: issue).tap { |v| v.save(validate: false) } + version = build(:design_version, :empty, issue: issue).tap { |v| v.save!(validate: false) } designs.each do |d| - yield.create(design: d, version: version) + yield.create!(design: d, version: version) end version end diff --git a/spec/support/helpers/filtered_search_helpers.rb b/spec/support/helpers/filtered_search_helpers.rb index 1847a8f8a06..d203ff60cc9 100644 --- a/spec/support/helpers/filtered_search_helpers.rb +++ b/spec/support/helpers/filtered_search_helpers.rb @@ -13,7 +13,7 @@ module FilteredSearchHelpers search = "#{search_term} " end - filtered_search.set(search) + filtered_search.set(search, rapid: false) if submit # Wait for the lazy author/assignee tokens that diff --git a/spec/support/helpers/http_basic_auth_helpers.rb b/spec/support/helpers/http_basic_auth_helpers.rb index c0b24b3dfa4..bc34e073f9f 100644 --- a/spec/support/helpers/http_basic_auth_helpers.rb +++ b/spec/support/helpers/http_basic_auth_helpers.rb @@ -8,19 +8,22 @@ module HttpBasicAuthHelpers end def job_basic_auth_header(job) - basic_auth_header(Ci::Build::CI_REGISTRY_USER, job.token) + basic_auth_header(::Gitlab::Auth::CI_JOB_USER, job.token) end def client_basic_auth_header(client) basic_auth_header(client.uid, client.secret) end + def build_auth_headers(value) + { 'HTTP_AUTHORIZATION' => value } + end + + def build_token_auth_header(token) + build_auth_headers("Bearer #{token}") + end + def basic_auth_header(username, password) - { - 'HTTP_AUTHORIZATION' => ActionController::HttpAuthentication::Basic.encode_credentials( - username, - password - ) - } + build_auth_headers(ActionController::HttpAuthentication::Basic.encode_credentials(username, password)) end end diff --git a/spec/support/helpers/jira_service_helper.rb b/spec/support/helpers/jira_service_helper.rb index 9072c41fe66..4895bc3ba15 100644 --- a/spec/support/helpers/jira_service_helper.rb +++ b/spec/support/helpers/jira_service_helper.rb @@ -10,7 +10,7 @@ module JiraServiceHelper password = 'my-secret-password' jira_issue_transition_id = '1' - jira_tracker.update( + jira_tracker.update!( url: url, username: username, password: password, jira_issue_transition_id: jira_issue_transition_id, active: true ) diff --git a/spec/support/helpers/login_helpers.rb b/spec/support/helpers/login_helpers.rb index 92f6d673255..1118cfcf7ac 100644 --- a/spec/support/helpers/login_helpers.rb +++ b/spec/support/helpers/login_helpers.rb @@ -40,7 +40,7 @@ module LoginHelpers if user_or_role.is_a?(User) user_or_role else - create(user_or_role) + create(user_or_role) # rubocop:disable Rails/SaveBang end gitlab_sign_in_with(user, **kwargs) diff --git a/spec/support/helpers/memory_usage_helper.rb b/spec/support/helpers/memory_usage_helper.rb index 984ea8cc571..aa7b3bae83a 100644 --- a/spec/support/helpers/memory_usage_helper.rb +++ b/spec/support/helpers/memory_usage_helper.rb @@ -21,7 +21,7 @@ module MemoryUsageHelper def get_memory_usage output, status = Gitlab::Popen.popen(%w(free -m)) - abort "`free -m` return code is #{status}: #{output}" unless status.zero? + abort "`free -m` return code is #{status}: #{output}" unless status == 0 result = output.split("\n")[1].split(" ")[1..-1] attrs = %i(m_total m_used m_free m_shared m_buffers_cache m_available).freeze diff --git a/spec/support/helpers/metrics_dashboard_helpers.rb b/spec/support/helpers/metrics_dashboard_helpers.rb index b2dd8ead7dd..7168079fead 100644 --- a/spec/support/helpers/metrics_dashboard_helpers.rb +++ b/spec/support/helpers/metrics_dashboard_helpers.rb @@ -1,16 +1,22 @@ # frozen_string_literal: true module MetricsDashboardHelpers - def project_with_dashboard(dashboard_path, dashboard_yml = nil) - dashboard_yml ||= fixture_file('lib/gitlab/metrics/dashboard/sample_dashboard.yml') - - create(:project, :custom_repo, files: { dashboard_path => dashboard_yml }) + # @param dashboards [Hash] - Should contain a hash where + # each key is the path to a dashboard in the repository and each value is + # the dashboard content. + # Ex: { '.gitlab/dashboards/dashboard1.yml' => fixture_file('lib/gitlab/metrics/dashboard/sample_dashboard.yml') } + def project_with_dashboards(dashboards, project_params = {}) + create(:project, :custom_repo, **project_params, files: dashboards) end - def project_with_dashboard_namespace(dashboard_path, dashboard_yml = nil) + def project_with_dashboard(dashboard_path, dashboard_yml = nil, project_params = {}) dashboard_yml ||= fixture_file('lib/gitlab/metrics/dashboard/sample_dashboard.yml') - create(:project, :custom_repo, namespace: namespace, path: 'monitor-project', files: { dashboard_path => dashboard_yml }) + project_with_dashboards({ dashboard_path => dashboard_yml }, project_params) + end + + def project_with_dashboard_namespace(dashboard_path, dashboard_yml = nil, project_params = {}) + project_with_dashboard(dashboard_path, dashboard_yml, project_params.reverse_merge(path: 'monitor-project')) end def delete_project_dashboard(project, user, dashboard_path) diff --git a/spec/support/helpers/metrics_dashboard_url_helpers.rb b/spec/support/helpers/metrics_dashboard_url_helpers.rb index cb9f58753a3..58b3d1e4d1d 100644 --- a/spec/support/helpers/metrics_dashboard_url_helpers.rb +++ b/spec/support/helpers/metrics_dashboard_url_helpers.rb @@ -13,4 +13,14 @@ module MetricsDashboardUrlHelpers Gitlab::Metrics::Dashboard::Url.clear_memoization(method_name) end end + + def stub_gitlab_domain + allow_any_instance_of(Banzai::Filter::InlineEmbedsFilter) + .to receive(:gitlab_domain) + .and_return(urls.root_url.chomp('/')) + + allow(Gitlab::Metrics::Dashboard::Url) + .to receive(:gitlab_domain) + .and_return(urls.root_url.chomp('/')) + end end diff --git a/spec/support/helpers/navbar_structure_helper.rb b/spec/support/helpers/navbar_structure_helper.rb index cfb1b185560..c7aa2ffe536 100644 --- a/spec/support/helpers/navbar_structure_helper.rb +++ b/spec/support/helpers/navbar_structure_helper.rb @@ -18,4 +18,22 @@ module NavbarStructureHelper index = hash[:nav_sub_items].find_index(before_sub_nav_item_name) hash[:nav_sub_items].insert(index + 1, new_sub_nav_item_name) end + + def insert_package_nav(within) + insert_after_nav_item( + within, + new_nav_item: { + nav_item: _('Packages & Registries'), + nav_sub_items: [_('Package Registry')] + } + ) + end + + def insert_container_nav(within) + insert_after_sub_nav_item( + _('Package Registry'), + within: _('Packages & Registries'), + new_sub_nav_item_name: _('Container Registry') + ) + end end diff --git a/spec/support/helpers/notification_helpers.rb b/spec/support/helpers/notification_helpers.rb index 887d68de4e1..aee57b452fe 100644 --- a/spec/support/helpers/notification_helpers.rb +++ b/spec/support/helpers/notification_helpers.rb @@ -12,7 +12,7 @@ module NotificationHelpers def create_global_setting_for(user, level) setting = user.global_notification_setting setting.level = level - setting.save + setting.save! user end @@ -27,7 +27,7 @@ module NotificationHelpers def create_notification_setting(user, resource, level) setting = user.notification_settings_for(resource) setting.level = level - setting.save + setting.save! end # Create custom notifications diff --git a/spec/support/helpers/packages_manager_api_spec_helper.rb b/spec/support/helpers/packages_manager_api_spec_helper.rb index e5a690e1680..34e92c0595c 100644 --- a/spec/support/helpers/packages_manager_api_spec_helper.rb +++ b/spec/support/helpers/packages_manager_api_spec_helper.rb @@ -1,18 +1,6 @@ # frozen_string_literal: true module PackagesManagerApiSpecHelpers - def build_auth_headers(value) - { 'HTTP_AUTHORIZATION' => value } - end - - def build_basic_auth_header(username, password) - build_auth_headers(ActionController::HttpAuthentication::Basic.encode_credentials(username, password)) - end - - def build_token_auth_header(token) - build_auth_headers("Bearer #{token}") - end - def build_jwt(personal_access_token, secret: jwt_secret, user_id: nil) JSONWebToken::HMACToken.new(secret).tap do |jwt| jwt['access_token'] = personal_access_token.id diff --git a/spec/support/helpers/require_migration.rb b/spec/support/helpers/require_migration.rb new file mode 100644 index 00000000000..d3f192a4142 --- /dev/null +++ b/spec/support/helpers/require_migration.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require 'find' + +class RequireMigration + MIGRATION_FOLDERS = %w(db/migrate db/post_migrate ee/db/geo/migrate ee/db/geo/post_migrate).freeze + SPEC_FILE_PATTERN = /.+\/(?.+)_spec\.rb/.freeze + + class << self + def require_migration!(file_name) + file_paths = search_migration_file(file_name) + + require file_paths.first + end + + def search_migration_file(file_name) + MIGRATION_FOLDERS.flat_map do |path| + migration_path = Rails.root.join(path).to_s + + Find.find(migration_path).grep(/\d+_#{file_name}\.rb/) + end + end + end +end + +def require_migration!(file_name = nil) + location_info = caller_locations.first.path.match(RequireMigration::SPEC_FILE_PATTERN) + file_name ||= location_info[:file_name] + + RequireMigration.require_migration!(file_name) +end diff --git a/spec/support/helpers/stub_configuration.rb b/spec/support/helpers/stub_configuration.rb index e19f230d8df..3b733a2e57a 100644 --- a/spec/support/helpers/stub_configuration.rb +++ b/spec/support/helpers/stub_configuration.rb @@ -33,8 +33,8 @@ module StubConfiguration allow(Gitlab.config).to receive_messages(to_settings(messages)) end - def stub_default_url_options(host: "localhost", protocol: "http") - url_options = { host: host, protocol: protocol } + def stub_default_url_options(host: "localhost", protocol: "http", script_name: nil) + url_options = { host: host, protocol: protocol, script_name: script_name } allow(Rails.application.routes).to receive(:default_url_options).and_return(url_options) end diff --git a/spec/support/helpers/stub_feature_flags.rb b/spec/support/helpers/stub_feature_flags.rb index 696148cacaf..792a1c21c31 100644 --- a/spec/support/helpers/stub_feature_flags.rb +++ b/spec/support/helpers/stub_feature_flags.rb @@ -1,6 +1,11 @@ # frozen_string_literal: true module StubFeatureFlags + def self.included(base) + # Extend Feature class with methods that can stub feature flags. + Feature.prepend(StubbedFeature) + end + class StubFeatureGate attr_reader :flipper_id @@ -9,28 +14,14 @@ module StubFeatureFlags end end + # Ensure feature flags are stubbed and reset. def stub_all_feature_flags - adapter = Flipper::Adapters::Memory.new - flipper = Flipper.new(adapter) - - allow(Feature).to receive(:flipper).and_return(flipper) - - # All new requested flags are enabled by default - allow(Feature).to receive(:enabled?).and_wrap_original do |m, *args| - feature_flag = m.call(*args) - - # If feature flag is not persisted we mark the feature flag as enabled - # We do `m.call` as we want to validate the execution of method arguments - # and a feature flag state if it is not persisted - unless Feature.persisted_name?(args.first) - # TODO: this is hack to support `promo_feature_available?` - # We enable all feature flags by default unless they are `promo_` - # Issue: https://gitlab.com/gitlab-org/gitlab/-/issues/218667 - feature_flag = true unless args.first.to_s.start_with?('promo_') - end + Feature.stub = true + Feature.reset_flipper + end - feature_flag - end + def unstub_all_feature_flags + Feature.stub = false end # Stub Feature flags with `flag_name: true/false` diff --git a/spec/support/helpers/stub_object_storage.rb b/spec/support/helpers/stub_object_storage.rb index 6056359d026..8a52a614821 100644 --- a/spec/support/helpers/stub_object_storage.rb +++ b/spec/support/helpers/stub_object_storage.rb @@ -1,13 +1,6 @@ # frozen_string_literal: true module StubObjectStorage - def stub_packages_object_storage(**params) - stub_object_storage_uploader(config: ::Gitlab.config.packages.object_store, - uploader: ::Packages::PackageFileUploader, - remote_directory: 'packages', - **params) - end - def stub_dependency_proxy_object_storage(**params) stub_object_storage_uploader(config: ::Gitlab.config.dependency_proxy.object_store, uploader: ::DependencyProxy::FileUploader, @@ -44,7 +37,7 @@ module StubObjectStorage Fog.mock! ::Fog::Storage.new(connection_params).tap do |connection| - connection.directories.create(key: remote_directory) + connection.directories.create(key: remote_directory) # rubocop:disable Rails/SaveBang # Cleanup remaining files connection.directories.each do |directory| @@ -54,9 +47,9 @@ module StubObjectStorage end end - def stub_artifacts_object_storage(**params) + def stub_artifacts_object_storage(uploader = JobArtifactUploader, **params) stub_object_storage_uploader(config: Gitlab.config.artifacts.object_store, - uploader: JobArtifactUploader, + uploader: uploader, remote_directory: 'artifacts', **params) end diff --git a/spec/support/helpers/stubbed_feature.rb b/spec/support/helpers/stubbed_feature.rb new file mode 100644 index 00000000000..e78efcf6b75 --- /dev/null +++ b/spec/support/helpers/stubbed_feature.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +# Extend the Feature class with the ability to stub feature flags. +module StubbedFeature + extend ActiveSupport::Concern + + class_methods do + # Turn stubbed feature flags on or off. + def stub=(stub) + @stub = stub + end + + def stub? + @stub.nil? ? true : @stub + end + + # Wipe any previously set feature flags. + def reset_flipper + @flipper = nil + end + + # Replace #flipper method with the optional stubbed/unstubbed version. + def flipper + if stub? + @flipper ||= Flipper.new(Flipper::Adapters::Memory.new) + else + super + end + end + + # Replace #enabled? method with the optional stubbed/unstubbed version. + def enabled?(*args) + feature_flag = super(*args) + return feature_flag unless stub? + + # If feature flag is not persisted we mark the feature flag as enabled + # We do `m.call` as we want to validate the execution of method arguments + # and a feature flag state if it is not persisted + unless Feature.persisted_name?(args.first) + # TODO: this is hack to support `promo_feature_available?` + # We enable all feature flags by default unless they are `promo_` + # Issue: https://gitlab.com/gitlab-org/gitlab/-/issues/218667 + feature_flag = true unless args.first.to_s.start_with?('promo_') + end + + feature_flag + end + end +end diff --git a/spec/support/helpers/test_env.rb b/spec/support/helpers/test_env.rb index f787aedf7aa..7dae960410d 100644 --- a/spec/support/helpers/test_env.rb +++ b/spec/support/helpers/test_env.rb @@ -6,8 +6,6 @@ module TestEnv ComponentFailedToInstallError = Class.new(StandardError) - SHA_REGEX = /\A[0-9a-f]{5,40}\z/i.freeze - # When developing the seed repository, comment out the branch you will modify. BRANCH_SHA = { 'signed-commits' => '6101e87', @@ -78,7 +76,7 @@ module TestEnv 'png-lfs' => 'fe42f41', 'sha-starting-with-large-number' => '8426165', 'invalid-utf8-diff-paths' => '99e4853', - 'compare-with-merge-head-source' => 'b5f4399', + 'compare-with-merge-head-source' => 'f20a03d', 'compare-with-merge-head-target' => '2f1e176' }.freeze @@ -524,7 +522,7 @@ module TestEnv def component_matches_git_sha?(component_folder, expected_version) # Not a git SHA, so return early - return false unless expected_version =~ SHA_REGEX + return false unless expected_version =~ ::Gitlab::Git::COMMIT_ID sha, exit_status = Gitlab::Popen.popen(%W(#{Gitlab.config.git.bin_path} rev-parse HEAD), component_folder) return false if exit_status != 0 diff --git a/spec/support/helpers/trigger_helpers.rb b/spec/support/helpers/trigger_helpers.rb index 67c62cf4869..dd6d8ff5bb5 100644 --- a/spec/support/helpers/trigger_helpers.rb +++ b/spec/support/helpers/trigger_helpers.rb @@ -28,8 +28,6 @@ module TriggerHelpers expect(timing).to eq(expected_timing.to_s) expect(events).to match_array(Array.wrap(expected_events)) - # TODO: Update CREATE TRIGGER syntax to use EXECUTE FUNCTION - # https://gitlab.com/gitlab-org/gitlab/-/issues/227089 expect(definition).to match(%r{execute (?:procedure|function) #{fn_name}()}) end diff --git a/spec/support/helpers/usage_data_helpers.rb b/spec/support/helpers/usage_data_helpers.rb index a4f40a4af0a..fab775dd404 100644 --- a/spec/support/helpers/usage_data_helpers.rb +++ b/spec/support/helpers/usage_data_helpers.rb @@ -119,6 +119,7 @@ module UsageDataHelpers projects_with_terraform_states pages_domains protected_branches + protected_branches_except_default releases remote_mirrors snippets diff --git a/spec/support/helpers/wait_for_requests.rb b/spec/support/helpers/wait_for_requests.rb index 52d1c59ab03..2cfd47634ca 100644 --- a/spec/support/helpers/wait_for_requests.rb +++ b/spec/support/helpers/wait_for_requests.rb @@ -42,7 +42,7 @@ module WaitForRequests private def finished_all_rack_requests? - Gitlab::Testing::RequestBlockerMiddleware.num_active_requests.zero? + Gitlab::Testing::RequestBlockerMiddleware.num_active_requests == 0 end def finished_all_js_requests? @@ -53,12 +53,12 @@ module WaitForRequests end def finished_all_axios_requests? - Capybara.page.evaluate_script('window.pendingRequests || 0').zero? + Capybara.page.evaluate_script('window.pendingRequests || 0').zero? # rubocop:disable Style/NumericPredicate end def finished_all_ajax_requests? return true if Capybara.page.evaluate_script('typeof jQuery === "undefined"') - Capybara.page.evaluate_script('jQuery.active').zero? + Capybara.page.evaluate_script('jQuery.active').zero? # rubocop:disable Style/NumericPredicate end end diff --git a/spec/support/matchers/exceed_query_limit.rb b/spec/support/matchers/exceed_query_limit.rb index cc0abfa0dd6..04482d3bfb8 100644 --- a/spec/support/matchers/exceed_query_limit.rb +++ b/spec/support/matchers/exceed_query_limit.rb @@ -44,7 +44,7 @@ module ExceedQueryLimitHelpers def log_message if expected.is_a?(ActiveRecord::QueryRecorder) counts = count_queries(strip_marginalia_annotations(expected.log)) - extra_queries = strip_marginalia_annotations(@recorder.log).reject { |query| counts[query] -= 1 unless counts[query].zero? } + extra_queries = strip_marginalia_annotations(@recorder.log).reject { |query| counts[query] -= 1 unless counts[query] == 0 } extra_queries_display = count_queries(extra_queries).map { |query, count| "[#{count}] #{query}" } (['Extra queries:'] + extra_queries_display).join("\n\n") @@ -188,7 +188,7 @@ RSpec::Matchers.define :issue_same_number_of_queries_as do def expected_count_message or_fewer_msg = "or fewer" if @or_fewer - threshold_msg = "(+/- #{threshold})" unless threshold.zero? + threshold_msg = "(+/- #{threshold})" unless threshold == 0 ["#{expected_count}", or_fewer_msg, threshold_msg].compact.join(' ') end diff --git a/spec/support/migrations_helpers/track_untracked_uploads_helpers.rb b/spec/support/migrations_helpers/track_untracked_uploads_helpers.rb deleted file mode 100644 index 656be3b6d4d..00000000000 --- a/spec/support/migrations_helpers/track_untracked_uploads_helpers.rb +++ /dev/null @@ -1,130 +0,0 @@ -# frozen_string_literal: true - -module MigrationsHelpers - module TrackUntrackedUploadsHelpers - PUBLIC_DIR = File.join(Rails.root, 'tmp', 'tests', 'public') - UPLOADS_DIR = File.join(PUBLIC_DIR, 'uploads') - SYSTEM_DIR = File.join(UPLOADS_DIR, '-', 'system') - UPLOAD_FILENAME = 'image.png'.freeze - FIXTURE_FILE_PATH = File.join(Rails.root, 'spec', 'fixtures', 'dk.png') - FIXTURE_CHECKSUM = 'b804383982bb89b00e828e3f44c038cc991d3d1768009fc39ba8e2c081b9fb75'.freeze - - def create_or_update_appearance(logo: false, header_logo: false) - appearance = appearances.first_or_create(title: 'foo', description: 'bar', logo: (UPLOAD_FILENAME if logo), header_logo: (UPLOAD_FILENAME if header_logo)) - - add_upload(appearance, 'Appearance', 'logo', 'AttachmentUploader') if logo - add_upload(appearance, 'Appearance', 'header_logo', 'AttachmentUploader') if header_logo - - appearance - end - - def create_group(avatar: false) - index = unique_index(:group) - group = namespaces.create(name: "group#{index}", path: "group#{index}", avatar: (UPLOAD_FILENAME if avatar)) - - add_upload(group, 'Group', 'avatar', 'AvatarUploader') if avatar - - group - end - - def create_note(attachment: false) - note = notes.create(attachment: (UPLOAD_FILENAME if attachment)) - - add_upload(note, 'Note', 'attachment', 'AttachmentUploader') if attachment - - note - end - - def create_project(avatar: false) - group = create_group - project = projects.create(namespace_id: group.id, path: "project#{unique_index(:project)}", avatar: (UPLOAD_FILENAME if avatar)) - routes.create(path: "#{group.path}/#{project.path}", source_id: project.id, source_type: 'Project') # so Project.find_by_full_path works - - add_upload(project, 'Project', 'avatar', 'AvatarUploader') if avatar - - project - end - - def create_user(avatar: false) - user = users.create(email: "foo#{unique_index(:user)}@bar.com", avatar: (UPLOAD_FILENAME if avatar), projects_limit: 100) - - add_upload(user, 'User', 'avatar', 'AvatarUploader') if avatar - - user - end - - def unique_index(name = :unnamed) - @unique_index ||= {} - @unique_index[name] ||= 0 - @unique_index[name] += 1 - end - - def add_upload(model, model_type, attachment_type, uploader) - file_path = upload_file_path(model, model_type, attachment_type) - path_relative_to_public = file_path.sub("#{PUBLIC_DIR}/", '') - create_file(file_path) - - uploads.create!( - size: 1062, - path: path_relative_to_public, - model_id: model.id, - model_type: model_type == 'Group' ? 'Namespace' : model_type, - uploader: uploader, - checksum: FIXTURE_CHECKSUM - ) - end - - def add_markdown_attachment(project, hashed_storage: false) - project_dir = hashed_storage ? hashed_project_uploads_dir(project) : legacy_project_uploads_dir(project) - attachment_dir = File.join(project_dir, SecureRandom.hex) - attachment_file_path = File.join(attachment_dir, UPLOAD_FILENAME) - project_attachment_path_relative_to_project = attachment_file_path.sub("#{project_dir}/", '') - create_file(attachment_file_path) - - uploads.create!( - size: 1062, - path: project_attachment_path_relative_to_project, - model_id: project.id, - model_type: 'Project', - uploader: 'FileUploader', - checksum: FIXTURE_CHECKSUM - ) - end - - def legacy_project_uploads_dir(project) - namespace = namespaces.find_by(id: project.namespace_id) - File.join(UPLOADS_DIR, namespace.path, project.path) - end - - def hashed_project_uploads_dir(project) - File.join(UPLOADS_DIR, '@hashed', 'aa', 'aaaaaaaaaaaa') - end - - def upload_file_path(model, model_type, attachment_type) - dir = File.join(upload_dir(model_type.downcase, attachment_type.to_s), model.id.to_s) - File.join(dir, UPLOAD_FILENAME) - end - - def upload_dir(model_type, attachment_type) - File.join(SYSTEM_DIR, model_type, attachment_type) - end - - def create_file(path) - File.delete(path) if File.exist?(path) - FileUtils.mkdir_p(File.dirname(path)) - FileUtils.cp(FIXTURE_FILE_PATH, path) - end - - def get_uploads(model, model_type) - uploads.where(model_type: model_type, model_id: model.id) - end - - def get_full_path(project) - routes.find_by(source_id: project.id, source_type: 'Project').path - end - - def ensure_temporary_tracking_table_exists - Gitlab::BackgroundMigration::PrepareUntrackedUploads.new.send(:ensure_temporary_tracking_table_exists) - end - end -end diff --git a/spec/support/protected_branch_helpers.rb b/spec/support/protected_branch_helpers.rb index ede16d1c1e2..b34b9ec4641 100644 --- a/spec/support/protected_branch_helpers.rb +++ b/spec/support/protected_branch_helpers.rb @@ -27,4 +27,9 @@ module ProtectedBranchHelpers set_allowed_to('merge') set_allowed_to('push') end + + def click_on_protect + click_on "Protect" + wait_for_requests + end end diff --git a/spec/support/shared_contexts/change_access_checks_shared_context.rb b/spec/support/shared_contexts/change_access_checks_shared_context.rb index e1ab81b4e3d..4c55990c901 100644 --- a/spec/support/shared_contexts/change_access_checks_shared_context.rb +++ b/spec/support/shared_contexts/change_access_checks_shared_context.rb @@ -3,7 +3,7 @@ RSpec.shared_context 'change access checks context' do let(:user) { create(:user) } let(:project) { create(:project, :repository) } - let(:user_access) { Gitlab::UserAccess.new(user, project: project) } + let(:user_access) { Gitlab::UserAccess.new(user, container: project) } let(:oldrev) { 'be93687618e4b132087f430a4d8fc3a609c9b77c' } let(:newrev) { '54fcc214b94e78d7a41a9a8fe6d87a5e59500e51' } let(:ref) { 'refs/heads/master' } diff --git a/spec/support/shared_contexts/csv_response_shared_context.rb b/spec/support/shared_contexts/csv_response_shared_context.rb new file mode 100644 index 00000000000..af79e393a91 --- /dev/null +++ b/spec/support/shared_contexts/csv_response_shared_context.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +RSpec.shared_context 'CSV response' do + let(:csv_response) { CSV.parse(response.body) } +end diff --git a/spec/support/shared_contexts/finders/merge_requests_finder_shared_contexts.rb b/spec/support/shared_contexts/finders/merge_requests_finder_shared_contexts.rb index 07b6b98222f..010c445d8df 100644 --- a/spec/support/shared_contexts/finders/merge_requests_finder_shared_contexts.rb +++ b/spec/support/shared_contexts/finders/merge_requests_finder_shared_contexts.rb @@ -28,6 +28,7 @@ RSpec.shared_context 'MergeRequestsFinder multiple projects with merge requests fork_project(project1, user) end end + let!(:project3) do allow_gitaly_n_plus_1 do fork_project(project1, user).tap do |project| @@ -35,6 +36,7 @@ RSpec.shared_context 'MergeRequestsFinder multiple projects with merge requests end end end + let_it_be(:project4, reload: true) do allow_gitaly_n_plus_1 { create(:project, :repository, group: subgroup) } end @@ -53,22 +55,26 @@ RSpec.shared_context 'MergeRequestsFinder multiple projects with merge requests source_project: project2, target_project: project1, target_branch: 'merged-target') end + let!(:merge_request2) do create(:merge_request, :conflict, assignees: [user], author: user, source_project: project2, target_project: project1, state: 'closed') end + let!(:merge_request3) do create(:merge_request, :simple, author: user, assignees: [user2], source_project: project2, target_project: project2, state: 'locked', title: 'thing WIP thing') end + let!(:merge_request4) do create(:merge_request, :simple, author: user, source_project: project3, target_project: project3, title: 'WIP thing') end + let_it_be(:merge_request5) do create(:merge_request, :simple, author: user, source_project: project4, target_project: project4, diff --git a/spec/support/shared_contexts/lib/gitlab/git_access_shared_examples.rb b/spec/support/shared_contexts/lib/gitlab/git_access_shared_examples.rb new file mode 100644 index 00000000000..837c1c37aa3 --- /dev/null +++ b/spec/support/shared_contexts/lib/gitlab/git_access_shared_examples.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'forbidden git access' do + let(:message) { /You can't/ } + + it 'prevents access' do + expect { subject }.to raise_error(Gitlab::GitAccess::ForbiddenError, message) + end +end + +RSpec.shared_examples 'not-found git access' do + let(:message) { /not found/ } + + it 'prevents access' do + expect { subject }.to raise_error(Gitlab::GitAccess::NotFoundError, message) + end +end diff --git a/spec/support/shared_contexts/navbar_structure_context.rb b/spec/support/shared_contexts/navbar_structure_context.rb index d9a72f2b54a..e276a54224b 100644 --- a/spec/support/shared_contexts/navbar_structure_context.rb +++ b/spec/support/shared_contexts/navbar_structure_context.rb @@ -7,6 +7,7 @@ RSpec.shared_context 'project navbar structure' do nav_sub_items: [ _('CI / CD'), (_('Code Review') if Gitlab.ee?), + (_('Merge Request') if Gitlab.ee?), _('Repository'), _('Value Stream') ] @@ -64,8 +65,10 @@ RSpec.shared_context 'project navbar structure' do nav_sub_items: [ _('Metrics'), _('Alerts'), + _('Incidents'), _('Environments'), _('Error Tracking'), + _('Product Analytics'), _('Serverless'), _('Logs'), _('Kubernetes') diff --git a/spec/support/shared_contexts/policies/group_policy_shared_context.rb b/spec/support/shared_contexts/policies/group_policy_shared_context.rb index 4b0c7afab6d..af46e5474b0 100644 --- a/spec/support/shared_contexts/policies/group_policy_shared_context.rb +++ b/spec/support/shared_contexts/policies/group_policy_shared_context.rb @@ -17,6 +17,7 @@ RSpec.shared_context 'GroupPolicy context' do read_group_merge_requests ] end + let(:read_group_permissions) { %i[read_label read_list read_milestone read_board] } let(:reporter_permissions) { %i[admin_label read_container_image read_metrics_dashboard_annotation read_prometheus] } let(:developer_permissions) { %i[admin_milestone create_metrics_dashboard_annotation delete_metrics_dashboard_annotation update_metrics_dashboard_annotation] } @@ -26,6 +27,7 @@ RSpec.shared_context 'GroupPolicy context' do read_cluster create_cluster update_cluster admin_cluster add_cluster ] end + let(:owner_permissions) do [ :admin_group, @@ -38,6 +40,7 @@ RSpec.shared_context 'GroupPolicy context' do :update_default_branch_protection ].compact end + let(:admin_permissions) { %i[read_confidential_issues] } before_all do diff --git a/spec/support/shared_contexts/prometheus/alert_shared_context.rb b/spec/support/shared_contexts/prometheus/alert_shared_context.rb index 330d2c4515f..932ab899270 100644 --- a/spec/support/shared_contexts/prometheus/alert_shared_context.rb +++ b/spec/support/shared_contexts/prometheus/alert_shared_context.rb @@ -15,7 +15,7 @@ RSpec.shared_context 'self-managed prometheus alert attributes' do { panel_groups: [{ panels: [{ - type: 'line-graph', + type: 'area-chart', title: title, y_label: y_label, metrics: [{ query_range: query }] diff --git a/spec/support/shared_contexts/read_ci_configuration_shared_context.rb b/spec/support/shared_contexts/read_ci_configuration_shared_context.rb new file mode 100644 index 00000000000..f8f33e2a745 --- /dev/null +++ b/spec/support/shared_contexts/read_ci_configuration_shared_context.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +RSpec.shared_context 'read ci configuration for sast enabled project' do + let_it_be(:gitlab_ci_yml_content) do + File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci_for_sast.yml')) + end + + let_it_be(:project) { create(:project, :repository) } +end diff --git a/spec/support/shared_contexts/requests/api/graphql/jira_import/jira_projects_context.rb b/spec/support/shared_contexts/requests/api/graphql/jira_import/jira_projects_context.rb index 7f150bed43d..edc5b313220 100644 --- a/spec/support/shared_contexts/requests/api/graphql/jira_import/jira_projects_context.rb +++ b/spec/support/shared_contexts/requests/api/graphql/jira_import/jira_projects_context.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -RSpec.shared_context 'jira projects request context' do +RSpec.shared_context 'Jira projects request context' do let(:url) { 'https://jira.example.com' } let(:username) { 'jira-username' } let(:password) { 'jira-password' } diff --git a/spec/support/shared_contexts/services/projects/container_repository/delete_tags_service_shared_context.rb b/spec/support/shared_contexts/services/projects/container_repository/delete_tags_service_shared_context.rb new file mode 100644 index 00000000000..bcc98cf6416 --- /dev/null +++ b/spec/support/shared_contexts/services/projects/container_repository/delete_tags_service_shared_context.rb @@ -0,0 +1,78 @@ +# frozen_string_literal: true + +RSpec.shared_context 'container repository delete tags service shared context' do + let_it_be(:user) { create(:user) } + let_it_be(:project, reload: true) { create(:project, :private) } + let_it_be(:repository) { create(:container_repository, :root, project: project) } + + let(:params) { { tags: tags } } + + before do + stub_container_registry_config(enabled: true, + api_url: 'http://registry.gitlab', + host_port: 'registry.gitlab') + + stub_container_registry_tags( + repository: repository.path, + tags: %w(latest A Ba Bb C D E)) + end + + def stub_delete_reference_request(tag, status = 200) + stub_request(:delete, "http://registry.gitlab/v2/#{repository.path}/tags/reference/#{tag}") + .to_return(status: status, body: '') + end + + def stub_delete_reference_requests(tags) + tags = Hash[Array.wrap(tags).map { |tag| [tag, 200] }] unless tags.is_a?(Hash) + + tags.each do |tag, status| + stub_request(:delete, "http://registry.gitlab/v2/#{repository.path}/tags/reference/#{tag}") + .to_return(status: status, body: '') + end + end + + def stub_put_manifest_request(tag, status = 200, headers = { 'docker-content-digest' => 'sha256:dummy' }) + stub_request(:put, "http://registry.gitlab/v2/#{repository.path}/manifests/#{tag}") + .to_return(status: status, body: '', headers: headers) + end + + def stub_tag_digest(tag, digest) + stub_request(:head, "http://registry.gitlab/v2/#{repository.path}/manifests/#{tag}") + .to_return(status: 200, body: '', headers: { 'docker-content-digest' => digest }) + end + + def stub_digest_config(digest, created_at) + allow_any_instance_of(ContainerRegistry::Client) + .to receive(:blob) + .with(repository.path, digest, nil) do + { 'created' => created_at.to_datetime.rfc3339 }.to_json if created_at + end + end + + def stub_upload(digest, success: true) + content = "{\n \"config\": {\n }\n}" + expect_any_instance_of(ContainerRegistry::Client) + .to receive(:upload_blob) + .with(repository.path, content, digest) { double(success?: success ) } + end + + def expect_delete_tag_by_digest(digest) + expect_any_instance_of(ContainerRegistry::Client) + .to receive(:delete_repository_tag_by_digest) + .with(repository.path, digest) { true } + + expect_any_instance_of(ContainerRegistry::Client) + .not_to receive(:delete_repository_tag_by_name) + end + + def expect_delete_tag_by_names(names) + Array.wrap(names).each do |name| + expect_any_instance_of(ContainerRegistry::Client) + .to receive(:delete_repository_tag_by_name) + .with(repository.path, name) { true } + + expect_any_instance_of(ContainerRegistry::Client) + .not_to receive(:delete_repository_tag_by_digest) + end + end +end diff --git a/spec/support/shared_examples/alert_notification_service_shared_examples.rb b/spec/support/shared_examples/alert_notification_service_shared_examples.rb new file mode 100644 index 00000000000..1568e4357a1 --- /dev/null +++ b/spec/support/shared_examples/alert_notification_service_shared_examples.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'Alert Notification Service sends notification email' do + let(:notification_service) { spy } + + it 'sends a notification for firing alerts only' do + expect(NotificationService) + .to receive(:new) + .and_return(notification_service) + + expect(notification_service) + .to receive_message_chain(:async, :prometheus_alerts_fired) + + expect(subject).to be_success + end +end + +RSpec.shared_examples 'Alert Notification Service sends no notifications' do |http_status:| + let(:notification_service) { spy } + let(:create_events_service) { spy } + + it 'does not notify' do + expect(notification_service).not_to receive(:async) + expect(create_events_service).not_to receive(:execute) + + expect(subject).to be_error + expect(subject.http_status).to eq(http_status) + end +end diff --git a/spec/support/shared_examples/controllers/binary_blob_shared_examples.rb b/spec/support/shared_examples/controllers/binary_blob_shared_examples.rb index c1ec515f1fe..acce7642cfe 100644 --- a/spec/support/shared_examples/controllers/binary_blob_shared_examples.rb +++ b/spec/support/shared_examples/controllers/binary_blob_shared_examples.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true RSpec.shared_examples 'editing snippet checks blob is binary' do + let(:snippets_binary_blob_value) { true } + before do sign_in(user) @@ -8,6 +10,8 @@ RSpec.shared_examples 'editing snippet checks blob is binary' do allow(blob).to receive(:binary?).and_return(binary) end + stub_feature_flags(snippets_binary_blob: snippets_binary_blob_value) + subject end @@ -23,13 +27,24 @@ RSpec.shared_examples 'editing snippet checks blob is binary' do context 'when blob is binary' do let(:binary) { true } - it 'redirects away' do - expect(response).to redirect_to(gitlab_snippet_path(snippet)) + it 'responds with status 200' do + expect(response).to have_gitlab_http_status(:ok) + expect(response).to render_template(:edit) + end + + context 'when feature flag :snippets_binary_blob is disabled' do + let(:snippets_binary_blob_value) { false } + + it 'redirects away' do + expect(response).to redirect_to(gitlab_snippet_path(snippet)) + end end end end RSpec.shared_examples 'updating snippet checks blob is binary' do + let(:snippets_binary_blob_value) { true } + before do sign_in(user) @@ -37,6 +52,8 @@ RSpec.shared_examples 'updating snippet checks blob is binary' do allow(blob).to receive(:binary?).and_return(binary) end + stub_feature_flags(snippets_binary_blob: snippets_binary_blob_value) + subject end @@ -52,9 +69,18 @@ RSpec.shared_examples 'updating snippet checks blob is binary' do context 'when blob is binary' do let(:binary) { true } - it 'redirects away without updating' do + it 'updates successfully' do + expect(snippet.reload.title).to eq title expect(response).to redirect_to(gitlab_snippet_path(snippet)) - expect(snippet.reload.title).not_to eq title + end + + context 'when feature flag :snippets_binary_blob is disabled' do + let(:snippets_binary_blob_value) { false } + + it 'redirects away without updating' do + expect(response).to redirect_to(gitlab_snippet_path(snippet)) + expect(snippet.reload.title).not_to eq title + end end end end diff --git a/spec/support/shared_examples/controllers/concerns/graceful_timeout_handling_shared_examples.rb b/spec/support/shared_examples/controllers/concerns/graceful_timeout_handling_shared_examples.rb new file mode 100644 index 00000000000..ea002776eeb --- /dev/null +++ b/spec/support/shared_examples/controllers/concerns/graceful_timeout_handling_shared_examples.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +RSpec.shared_examples GracefulTimeoutHandling do + it 'includes GracefulTimeoutHandling' do + expect(controller).to be_a(GracefulTimeoutHandling) + end +end diff --git a/spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb b/spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb index a01fa49d701..8bc91f72b8c 100644 --- a/spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb +++ b/spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb @@ -72,7 +72,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: GET status' do project = create(:project, import_type: provider, namespace: user.namespace, import_status: :finished, import_source: 'example/repo') group = create(:group) group.add_owner(user) - stub_client(repos: [repo, org_repo], orgs: [org], org_repos: [org_repo]) + stub_client(repos: [repo, org_repo], orgs: [org], org_repos: [org_repo], each_page: [OpenStruct.new(objects: [repo, org_repo])].to_enum) get :status, format: :json @@ -85,7 +85,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: GET status' do it "does not show already added project" do project = create(:project, import_type: provider, namespace: user.namespace, import_status: :finished, import_source: 'asd/vim') - stub_client(repos: [repo], orgs: []) + stub_client(repos: [repo], orgs: [], each_page: [OpenStruct.new(objects: [repo])].to_enum) get :status, format: :json @@ -94,7 +94,8 @@ RSpec.shared_examples 'a GitHub-ish import controller: GET status' do end it "touches the etag cache store" do - expect(stub_client(repos: [], orgs: [])).to receive(:repos) + stub_client(repos: [], orgs: [], each_page: []) + expect_next_instance_of(Gitlab::EtagCaching::Store) do |store| expect(store).to receive(:touch) { "realtime_changes_import_#{provider}_path" } end @@ -102,17 +103,11 @@ RSpec.shared_examples 'a GitHub-ish import controller: GET status' do get :status, format: :json end - it "requests provider repos list" do - expect(stub_client(repos: [], orgs: [])).to receive(:repos) - - get :status - - expect(response).to have_gitlab_http_status(:ok) - end - it "handles an invalid access token" do - allow_any_instance_of(Gitlab::LegacyGithubImport::Client) - .to receive(:repos).and_raise(Octokit::Unauthorized) + client = stub_client(repos: [], orgs: [], each_page: []) + + allow(client).to receive(:repos).and_raise(Octokit::Unauthorized) + allow(client).to receive(:each_page).and_raise(Octokit::Unauthorized) get :status @@ -122,7 +117,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: GET status' do end it "does not produce N+1 database queries" do - stub_client(repos: [repo], orgs: []) + stub_client(repos: [repo], orgs: [], each_page: [].to_enum) group_a = create(:group) group_a.add_owner(user) create(:project, :import_started, import_type: provider, namespace: user.namespace) @@ -144,10 +139,12 @@ RSpec.shared_examples 'a GitHub-ish import controller: GET status' do let(:repo_2) { OpenStruct.new(login: 'emacs', full_name: 'asd/emacs', name: 'emacs', owner: { login: 'owner' }) } let(:project) { create(:project, import_type: provider, namespace: user.namespace, import_status: :finished, import_source: 'example/repo') } let(:group) { create(:group) } + let(:repos) { [repo, repo_2, org_repo] } before do group.add_owner(user) - stub_client(repos: [repo, repo_2, org_repo], orgs: [org], org_repos: [org_repo]) + client = stub_client(repos: repos, orgs: [org], org_repos: [org_repo]) + allow(client).to receive(:each_page).and_return([OpenStruct.new(objects: repos)].to_enum) end it 'filters list of repositories by name' do @@ -187,14 +184,14 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do end before do - stub_client(user: provider_user, repo: provider_repo) + stub_client(user: provider_user, repo: provider_repo, repository: provider_repo) assign_session_token(provider) end it 'returns 200 response when the project is imported successfully' do allow(Gitlab::LegacyGithubImport::ProjectCreator) .to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider) - .and_return(double(execute: project)) + .and_return(double(execute: project)) post :create, format: :json @@ -208,7 +205,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do allow(Gitlab::LegacyGithubImport::ProjectCreator) .to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider) - .and_return(double(execute: project)) + .and_return(double(execute: project)) post :create, format: :json @@ -219,7 +216,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do it "touches the etag cache store" do allow(Gitlab::LegacyGithubImport::ProjectCreator) .to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider) - .and_return(double(execute: project)) + .and_return(double(execute: project)) expect_next_instance_of(Gitlab::EtagCaching::Store) do |store| expect(store).to receive(:touch) { "realtime_changes_import_#{provider}_path" } end @@ -232,7 +229,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do it "takes the current user's namespace" do expect(Gitlab::LegacyGithubImport::ProjectCreator) .to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider) - .and_return(double(execute: project)) + .and_return(double(execute: project)) post :create, format: :json end @@ -244,7 +241,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do it "takes the current user's namespace" do expect(Gitlab::LegacyGithubImport::ProjectCreator) .to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider) - .and_return(double(execute: project)) + .and_return(double(execute: project)) post :create, format: :json end @@ -271,7 +268,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do it "takes the existing namespace" do expect(Gitlab::LegacyGithubImport::ProjectCreator) .to receive(:new).with(provider_repo, provider_repo.name, existing_namespace, user, access_params, type: provider) - .and_return(double(execute: project)) + .and_return(double(execute: project)) post :create, format: :json end @@ -283,7 +280,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do expect(Gitlab::LegacyGithubImport::ProjectCreator) .to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider) - .and_return(double(execute: project)) + .and_return(double(execute: project)) post :create, format: :json end @@ -302,7 +299,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do it "takes the new namespace" do expect(Gitlab::LegacyGithubImport::ProjectCreator) .to receive(:new).with(provider_repo, provider_repo.name, an_instance_of(Group), user, access_params, type: provider) - .and_return(double(execute: project)) + .and_return(double(execute: project)) post :create, params: { target_namespace: provider_repo.name }, format: :json end @@ -323,7 +320,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do it "takes the current user's namespace" do expect(Gitlab::LegacyGithubImport::ProjectCreator) .to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider) - .and_return(double(execute: project)) + .and_return(double(execute: project)) post :create, format: :json end @@ -341,7 +338,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do it 'takes the selected namespace and name' do expect(Gitlab::LegacyGithubImport::ProjectCreator) .to receive(:new).with(provider_repo, test_name, test_namespace, user, access_params, type: provider) - .and_return(double(execute: project)) + .and_return(double(execute: project)) post :create, params: { target_namespace: test_namespace.name, new_name: test_name }, format: :json end @@ -349,7 +346,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do it 'takes the selected name and default namespace' do expect(Gitlab::LegacyGithubImport::ProjectCreator) .to receive(:new).with(provider_repo, test_name, user.namespace, user, access_params, type: provider) - .and_return(double(execute: project)) + .and_return(double(execute: project)) post :create, params: { new_name: test_name }, format: :json end @@ -368,7 +365,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do it 'takes the selected namespace and name' do expect(Gitlab::LegacyGithubImport::ProjectCreator) .to receive(:new).with(provider_repo, test_name, nested_namespace, user, access_params, type: provider) - .and_return(double(execute: project)) + .and_return(double(execute: project)) post :create, params: { target_namespace: nested_namespace.full_path, new_name: test_name }, format: :json end @@ -380,7 +377,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do it 'takes the selected namespace and name' do expect(Gitlab::LegacyGithubImport::ProjectCreator) .to receive(:new).with(provider_repo, test_name, kind_of(Namespace), user, access_params, type: provider) - .and_return(double(execute: project)) + .and_return(double(execute: project)) post :create, params: { target_namespace: 'foo/bar', new_name: test_name }, format: :json end @@ -388,7 +385,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do it 'creates the namespaces' do allow(Gitlab::LegacyGithubImport::ProjectCreator) .to receive(:new).with(provider_repo, test_name, kind_of(Namespace), user, access_params, type: provider) - .and_return(double(execute: project)) + .and_return(double(execute: project)) expect { post :create, params: { target_namespace: 'foo/bar', new_name: test_name }, format: :json } .to change { Namespace.count }.by(2) @@ -397,7 +394,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do it 'new namespace has the right parent' do allow(Gitlab::LegacyGithubImport::ProjectCreator) .to receive(:new).with(provider_repo, test_name, kind_of(Namespace), user, access_params, type: provider) - .and_return(double(execute: project)) + .and_return(double(execute: project)) post :create, params: { target_namespace: 'foo/bar', new_name: test_name }, format: :json @@ -416,7 +413,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do it 'takes the selected namespace and name' do expect(Gitlab::LegacyGithubImport::ProjectCreator) .to receive(:new).with(provider_repo, test_name, kind_of(Namespace), user, access_params, type: provider) - .and_return(double(execute: project)) + .and_return(double(execute: project)) post :create, params: { target_namespace: 'foo/foobar/bar', new_name: test_name }, format: :json end @@ -424,7 +421,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do it 'creates the namespaces' do allow(Gitlab::LegacyGithubImport::ProjectCreator) .to receive(:new).with(provider_repo, test_name, kind_of(Namespace), user, access_params, type: provider) - .and_return(double(execute: project)) + .and_return(double(execute: project)) expect { post :create, params: { target_namespace: 'foo/foobar/bar', new_name: test_name }, format: :json } .to change { Namespace.count }.by(2) @@ -432,11 +429,11 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do it 'does not create a new namespace under the user namespace' do expect(Gitlab::LegacyGithubImport::ProjectCreator) - .to receive(:new).with(provider_repo, test_name, user.namespace, user, access_params, type: provider) - .and_return(double(execute: project)) + .to receive(:new).with(provider_repo, test_name, user.namespace, user, access_params, type: provider) + .and_return(double(execute: project)) expect { post :create, params: { target_namespace: "#{user.namespace_path}/test_group", new_name: test_name }, format: :js } - .not_to change { Namespace.count } + .not_to change { Namespace.count } end end @@ -446,19 +443,19 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do it 'does not take the selected namespace and name' do expect(Gitlab::LegacyGithubImport::ProjectCreator) - .to receive(:new).with(provider_repo, test_name, user.namespace, user, access_params, type: provider) - .and_return(double(execute: project)) + .to receive(:new).with(provider_repo, test_name, user.namespace, user, access_params, type: provider) + .and_return(double(execute: project)) post :create, params: { target_namespace: 'foo/foobar/bar', new_name: test_name }, format: :js end it 'does not create the namespaces' do allow(Gitlab::LegacyGithubImport::ProjectCreator) - .to receive(:new).with(provider_repo, test_name, kind_of(Namespace), user, access_params, type: provider) - .and_return(double(execute: project)) + .to receive(:new).with(provider_repo, test_name, kind_of(Namespace), user, access_params, type: provider) + .and_return(double(execute: project)) expect { post :create, params: { target_namespace: 'foo/foobar/bar', new_name: test_name }, format: :js } - .not_to change { Namespace.count } + .not_to change { Namespace.count } end end @@ -471,8 +468,8 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST create' do user.update!(can_create_group: false) expect(Gitlab::LegacyGithubImport::ProjectCreator) - .to receive(:new).with(provider_repo, test_name, group, user, access_params, type: provider) - .and_return(double(execute: project)) + .to receive(:new).with(provider_repo, test_name, group, user, access_params, type: provider) + .and_return(double(execute: project)) post :create, params: { target_namespace: 'foo', new_name: test_name }, format: :js end diff --git a/spec/support/shared_examples/controllers/metrics/dashboard/prometheus_api_proxy_shared_examples.rb b/spec/support/shared_examples/controllers/metrics/dashboard/prometheus_api_proxy_shared_examples.rb index 94cd6971f7c..19b1cee44ee 100644 --- a/spec/support/shared_examples/controllers/metrics/dashboard/prometheus_api_proxy_shared_examples.rb +++ b/spec/support/shared_examples/controllers/metrics/dashboard/prometheus_api_proxy_shared_examples.rb @@ -9,6 +9,7 @@ RSpec.shared_examples_for 'metrics dashboard prometheus api proxy' do id: proxyable.id.to_s } end + let(:expected_params) do ActionController::Parameters.new( prometheus_proxy_params( diff --git a/spec/support/shared_examples/controllers/variables_shared_examples.rb b/spec/support/shared_examples/controllers/variables_shared_examples.rb index 9ff0bc3d217..34632993cf0 100644 --- a/spec/support/shared_examples/controllers/variables_shared_examples.rb +++ b/spec/support/shared_examples/controllers/variables_shared_examples.rb @@ -21,6 +21,7 @@ RSpec.shared_examples 'PATCH #update updates variables' do secret_value: variable.value, protected: variable.protected?.to_s } end + let(:new_variable_attributes) do { key: 'new_key', secret_value: 'dummy_value', 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 4df3139d56e..c89ee0d25ae 100644 --- a/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb +++ b/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb @@ -61,6 +61,14 @@ RSpec.shared_examples 'wiki controller actions' do expect(assigns(:sidebar_wiki_entries)).to be_nil expect(assigns(:sidebar_limited)).to be_nil end + + context 'when the request is of non-html format' do + it 'returns a 404 error' do + get :pages, params: routing_params.merge(format: 'json') + + expect(response).to have_gitlab_http_status(:not_found) + end + end end describe 'GET #history' do @@ -153,6 +161,14 @@ RSpec.shared_examples 'wiki controller actions' do expect(assigns(:sidebar_limited)).to be(false) end + it 'increases the page view counter' do + expect do + subject + + expect(response).to have_gitlab_http_status(:ok) + end.to change { Gitlab::UsageDataCounters::WikiPageCounter.read(:view) }.by(1) + end + context 'when page content encoding is invalid' do it 'sets flash error' do allow(controller).to receive(:valid_encoding?).and_return(false) @@ -339,6 +355,44 @@ RSpec.shared_examples 'wiki controller actions' do end end + describe 'POST #create' do + let(:new_title) { 'New title' } + let(:new_content) { 'New content' } + + subject do + post(:create, + params: routing_params.merge( + wiki: { title: new_title, content: new_content } + )) + end + + context 'when page is valid' do + it 'creates the page' do + expect do + subject + end.to change { wiki.list_pages.size }.by 1 + + wiki_page = wiki.find_page(new_title) + + expect(wiki_page.title).to eq new_title + expect(wiki_page.content).to eq new_content + end + end + + context 'when page is not valid' do + let(:new_title) { '' } + + it 'renders the edit state' do + expect do + subject + end.not_to change { wiki.list_pages.size } + + expect(response).to render_template('shared/wikis/edit') + expect(flash[:alert]).to eq('Could not create wiki page') + end + end + end + def redirect_to_wiki(wiki, page) redirect_to(controller.wiki_page_path(wiki, page)) end diff --git a/spec/support/shared_examples/create_alert_issue_shared_examples.rb b/spec/support/shared_examples/create_alert_issue_shared_examples.rb deleted file mode 100644 index 9f4e1c4335a..00000000000 --- a/spec/support/shared_examples/create_alert_issue_shared_examples.rb +++ /dev/null @@ -1,27 +0,0 @@ -# frozen_string_literal: true - -RSpec.shared_examples 'create alert issue sets issue labels' do - let(:title) { IncidentManagement::CreateIncidentLabelService::LABEL_PROPERTIES[:title] } - let!(:label) { create(:label, project: project, title: title) } - let(:label_service) { instance_double(IncidentManagement::CreateIncidentLabelService, execute: label_service_response) } - - before do - allow(IncidentManagement::CreateIncidentLabelService).to receive(:new).with(project, user).and_return(label_service) - end - - context 'when create incident label responds with success' do - let(:label_service_response) { ServiceResponse.success(payload: { label: label }) } - - it 'adds label to issue' do - expect(issue.labels).to eq([label]) - end - end - - context 'when create incident label responds with error' do - let(:label_service_response) { ServiceResponse.error(payload: { label: label }, message: 'label error') } - - it 'creates an issue without labels' do - expect(issue.labels).to be_empty - end - end -end diff --git a/spec/support/shared_examples/features/master_manages_access_requests_shared_example.rb b/spec/support/shared_examples/features/master_manages_access_requests_shared_example.rb index 00ce690d2e3..ffe4fb83283 100644 --- a/spec/support/shared_examples/features/master_manages_access_requests_shared_example.rb +++ b/spec/support/shared_examples/features/master_manages_access_requests_shared_example.rb @@ -8,17 +8,18 @@ RSpec.shared_examples 'Maintainer manages access requests' do entity.request_access(user) entity.respond_to?(:add_owner) ? entity.add_owner(maintainer) : entity.add_maintainer(maintainer) sign_in(maintainer) - end - - it 'maintainer can see access requests' do visit members_page_path + if has_tabs + click_on 'Access requests' + end + end + + it 'maintainer can see access requests', :js do expect_visible_access_request(entity, user) end it 'maintainer can grant access', :js do - visit members_page_path - expect_visible_access_request(entity, user) click_on 'Grant access' @@ -31,8 +32,6 @@ RSpec.shared_examples 'Maintainer manages access requests' do end it 'maintainer can deny access', :js do - visit members_page_path - expect_visible_access_request(entity, user) # Open modal @@ -47,7 +46,13 @@ RSpec.shared_examples 'Maintainer manages access requests' do end def expect_visible_access_request(entity, user) - expect(page).to have_content "Users requesting access to #{entity.name} 1" + if has_tabs + expect(page).to have_content "Access requests 1" + expect(page).to have_content "Users requesting access to #{entity.name}" + else + expect(page).to have_content "Users requesting access to #{entity.name} 1" + end + expect(page).to have_content user.name end diff --git a/spec/support/shared_examples/features/packages_shared_examples.rb b/spec/support/shared_examples/features/packages_shared_examples.rb new file mode 100644 index 00000000000..6debbf81fc0 --- /dev/null +++ b/spec/support/shared_examples/features/packages_shared_examples.rb @@ -0,0 +1,113 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'packages list' do |check_project_name: false| + it 'shows a list of packages' do + wait_for_requests + + packages.each_with_index do |pkg, index| + package_row = package_table_row(index) + + expect(package_row).to have_content(pkg.name) + expect(package_row).to have_content(pkg.version) + expect(package_row).to have_content(pkg.project.name) if check_project_name + end + end + + def package_table_row(index) + page.all("#{packages_table_selector} > [data-qa-selector=\"packages-row\"]")[index].text + end +end + +RSpec.shared_examples 'package details link' do |property| + let(:package) { packages.first } + + before do + stub_feature_flags(packages_details_one_column: false) + end + + it 'navigates to the correct url' do + page.within(packages_table_selector) do + click_link package.name + end + + expect(page).to have_current_path(project_package_path(package.project, package)) + + page.within('.detail-page-header') do + expect(page).to have_content(package.name) + end + + page.within('[data-qa-selector="package_information_content"]') do + expect(page).to have_content('Installation') + expect(page).to have_content('Registry setup') + end + end +end + +RSpec.shared_examples 'when there are no packages' do + it 'displays the empty message' do + expect(page).to have_content('There are no packages yet') + end +end + +RSpec.shared_examples 'correctly sorted packages list' do |order_by, ascending: false| + context "ordered by #{order_by} and ascending #{ascending}" do + before do + click_sort_option(order_by, ascending) + end + + it_behaves_like 'packages list' + end +end + +RSpec.shared_examples 'shared package sorting' do + it_behaves_like 'correctly sorted packages list', 'Type' do + let(:packages) { [package_two, package_one] } + end + + it_behaves_like 'correctly sorted packages list', 'Type', ascending: true do + let(:packages) { [package_one, package_two] } + end + + it_behaves_like 'correctly sorted packages list', 'Name' do + let(:packages) { [package_two, package_one] } + end + + it_behaves_like 'correctly sorted packages list', 'Name', ascending: true do + let(:packages) { [package_one, package_two] } + end + + it_behaves_like 'correctly sorted packages list', 'Version' do + let(:packages) { [package_one, package_two] } + end + + it_behaves_like 'correctly sorted packages list', 'Version', ascending: true do + let(:packages) { [package_two, package_one] } + end + + it_behaves_like 'correctly sorted packages list', 'Created' do + let(:packages) { [package_two, package_one] } + end + + it_behaves_like 'correctly sorted packages list', 'Created', ascending: true do + let(:packages) { [package_one, package_two] } + end +end + +def packages_table_selector + '[data-qa-selector="packages-table"]' +end + +def click_sort_option(option, ascending) + page.within('.gl-sorting') do + # Reset the sort direction + click_button 'Sort direction' if page.has_selector?('svg[aria-label="Sorting Direction: Ascending"]', wait: 0) + + find('button.dropdown-menu-toggle').click + + page.within('.dropdown-menu') do + click_button option + end + + click_button 'Sort direction' if ascending + end +end diff --git a/spec/support/shared_examples/features/protected_branches_access_control_ce_shared_examples.rb b/spec/support/shared_examples/features/protected_branches_access_control_ce_shared_examples.rb index 65db082505a..a46382bc292 100644 --- a/spec/support/shared_examples/features/protected_branches_access_control_ce_shared_examples.rb +++ b/spec/support/shared_examples/features/protected_branches_access_control_ce_shared_examples.rb @@ -22,7 +22,7 @@ RSpec.shared_examples "protected branches > access control > CE" do end end - click_on "Protect" + click_on_protect expect(ProtectedBranch.count).to eq(1) expect(ProtectedBranch.last.push_access_levels.map(&:access_level)).to eq([access_type_id]) @@ -45,7 +45,7 @@ RSpec.shared_examples "protected branches > access control > CE" do find(:link, 'No one').click end - click_on "Protect" + click_on_protect expect(ProtectedBranch.count).to eq(1) @@ -85,7 +85,7 @@ RSpec.shared_examples "protected branches > access control > CE" do find(:link, 'No one').click end - click_on "Protect" + click_on_protect expect(ProtectedBranch.count).to eq(1) expect(ProtectedBranch.last.merge_access_levels.map(&:access_level)).to eq([access_type_id]) @@ -108,7 +108,7 @@ RSpec.shared_examples "protected branches > access control > CE" do find(:link, 'No one').click end - click_on "Protect" + click_on_protect expect(ProtectedBranch.count).to eq(1) diff --git a/spec/support/shared_examples/features/rss_shared_examples.rb b/spec/support/shared_examples/features/rss_shared_examples.rb index 42df88ec08e..1b0d3f9605a 100644 --- a/spec/support/shared_examples/features/rss_shared_examples.rb +++ b/spec/support/shared_examples/features/rss_shared_examples.rb @@ -9,8 +9,7 @@ end RSpec.shared_examples "it has an RSS button with current_user's feed token" do it "shows the RSS button with current_user's feed token" do expect(page) - .to have_css("a:has(.fa-rss)[href*='feed_token=#{user.feed_token}']") - .or have_css("a.js-rss-button[href*='feed_token=#{user.feed_token}']") + .to have_css("a:has(.qa-rss-icon)[href*='feed_token=#{user.feed_token}']") end end @@ -23,7 +22,6 @@ end RSpec.shared_examples "it has an RSS button without a feed token" do it "shows the RSS button without a feed token" do expect(page) - .to have_css("a:has(.fa-rss):not([href*='feed_token'])") - .or have_css("a.js-rss-button:not([href*='feed_token'])") + .to have_css("a:has(.qa-rss-icon):not([href*='feed_token'])") end end diff --git a/spec/support/shared_examples/features/snippets_shared_examples.rb b/spec/support/shared_examples/features/snippets_shared_examples.rb index 1c8a9714bdf..8d68b1e4c0a 100644 --- a/spec/support/shared_examples/features/snippets_shared_examples.rb +++ b/spec/support/shared_examples/features/snippets_shared_examples.rb @@ -50,3 +50,225 @@ RSpec.shared_examples 'tabs with counts' do expect(tab.find('.badge').text).to eq(counts[:public]) end end + +RSpec.shared_examples 'does not show New Snippet button' do + let(:user) { create(:user, :external) } + + specify do + sign_in(user) + + subject + + wait_for_requests + + expect(page).not_to have_link('New snippet') + end +end + +RSpec.shared_examples 'show and render proper snippet blob' do + before do + allow_any_instance_of(Snippet).to receive(:blobs).and_return([snippet.repository.blob_at('master', file_path)]) + end + + context 'Ruby file' do + let(:file_path) { 'files/ruby/popen.rb' } + + it 'displays the blob' do + subject + + aggregate_failures do + # shows highlighted Ruby code + expect(page).to have_content("require 'fileutils'") + + # does not show a viewer switcher + expect(page).not_to have_selector('.js-blob-viewer-switcher') + + # shows an enabled copy button + expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)') + + # shows a raw button + expect(page).to have_link('Open raw') + + # shows a download button + expect(page).to have_link('Download') + end + end + end + + context 'Markdown file' do + let(:file_path) { 'files/markdown/ruby-style-guide.md' } + + context 'visiting directly' do + before do + subject + end + + it 'displays the blob using the rich viewer' do + aggregate_failures do + # hides the simple viewer + expect(page).to have_selector('.blob-viewer[data-type="simple"]', visible: false) + expect(page).to have_selector('.blob-viewer[data-type="rich"]') + + # shows rendered Markdown + expect(page).to have_link("PEP-8") + + # shows a viewer switcher + expect(page).to have_selector('.js-blob-viewer-switcher') + + # shows a disabled copy button + expect(page).to have_selector('.js-copy-blob-source-btn.disabled') + + # shows a raw button + expect(page).to have_link('Open raw') + + # shows a download button + expect(page).to have_link('Download') + end + end + + context 'switching to the simple viewer' do + before do + find('.js-blob-viewer-switch-btn[data-viewer=simple]').click + + wait_for_requests + end + + it 'displays the blob using the simple viewer' do + aggregate_failures do + # hides the rich viewer + expect(page).to have_selector('.blob-viewer[data-type="simple"]') + expect(page).to have_selector('.blob-viewer[data-type="rich"]', visible: false) + + # shows highlighted Markdown code + expect(page).to have_content("[PEP-8](http://www.python.org/dev/peps/pep-0008/)") + + # shows an enabled copy button + expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)') + end + end + + context 'switching to the rich viewer again' do + before do + find('.js-blob-viewer-switch-btn[data-viewer=rich]').click + + wait_for_requests + end + + it 'displays the blob using the rich viewer' do + aggregate_failures do + # hides the simple viewer + expect(page).to have_selector('.blob-viewer[data-type="simple"]', visible: false) + expect(page).to have_selector('.blob-viewer[data-type="rich"]') + + # shows an enabled copy button + expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)') + end + end + end + end + end + + context 'visiting with a line number anchor' do + let(:anchor) { 'L1' } + + it 'displays the blob using the simple viewer' do + subject + + aggregate_failures do + # hides the rich viewer + expect(page).to have_selector('.blob-viewer[data-type="simple"]') + expect(page).to have_selector('.blob-viewer[data-type="rich"]', visible: false) + + # highlights the line in question + expect(page).to have_selector('#LC1.hll') + + # shows highlighted Markdown code + expect(page).to have_content("[PEP-8](http://www.python.org/dev/peps/pep-0008/)") + + # shows an enabled copy button + expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)') + end + end + end + end +end + +RSpec.shared_examples 'personal snippet with references' do + let_it_be(:project) { create(:project, :repository) } + let_it_be(:merge_request) { create(:merge_request, source_project: project) } + let_it_be(:project_snippet) { create(:project_snippet, :repository, project: project)} + let_it_be(:issue) { create(:issue, project: project) } + let_it_be(:commit) { project.commit } + + let(:mr_reference) { merge_request.to_reference(full: true) } + let(:issue_reference) { issue.to_reference(full: true) } + let(:snippet_reference) { project_snippet.to_reference(full: true) } + let(:commit_reference) { commit.reference_link_text(full: true) } + + RSpec.shared_examples 'handles resource links' do + context 'with access to the resource' do + before do + project.add_developer(user) + end + + it 'converts the reference to a link' do + subject + + page.within(container) do + aggregate_failures do + expect(page).to have_link(mr_reference) + expect(page).to have_link(issue_reference) + expect(page).to have_link(snippet_reference) + expect(page).to have_link(commit_reference) + end + end + end + end + + context 'without access to the resource' do + it 'does not convert the reference to a link' do + subject + + page.within(container) do + expect(page).not_to have_link(mr_reference) + expect(page).not_to have_link(issue_reference) + expect(page).not_to have_link(snippet_reference) + expect(page).not_to have_link(commit_reference) + end + end + end + end + + context 'when using references to resources' do + let(:references) do + <<~REFERENCES + MR: #{mr_reference} + + Commit: #{commit_reference} + + Issue: #{issue_reference} + + ProjectSnippet: #{snippet_reference} + REFERENCES + end + + it_behaves_like 'handles resource links' + end + + context 'when using links to resources' do + let(:args) { { host: Gitlab.config.gitlab.url, port: nil } } + let(:references) do + <<~REFERENCES + MR: #{merge_request_url(merge_request, args)} + + Commit: #{project_commit_url(project, commit, args)} + + Issue: #{issue_url(issue, args)} + + ProjectSnippet: #{project_snippet_url(project, project_snippet, args)} + REFERENCES + end + + it_behaves_like 'handles resource links' + end +end diff --git a/spec/support/shared_examples/finders/snippet_visibility_shared_examples.rb b/spec/support/shared_examples/finders/snippet_visibility_shared_examples.rb index c802038c9da..a2c34cdd4a1 100644 --- a/spec/support/shared_examples/finders/snippet_visibility_shared_examples.rb +++ b/spec/support/shared_examples/finders/snippet_visibility_shared_examples.rb @@ -9,13 +9,28 @@ RSpec.shared_examples 'snippet visibility' do let_it_be(:non_member) { create(:user) } let_it_be(:project, reload: true) do - create(:project).tap do |project| + create(:project, :public).tap do |project| project.add_developer(author) project.add_developer(member) end end + let(:snippets) do + { + private: private_snippet, + public: public_snippet, + internal: internal_snippet + } + end + + let(:user) { users[user_type] } + let(:snippet) { snippets[snippet_visibility] } + context "For project snippets" do + let_it_be(:private_snippet) { create(:project_snippet, :private, project: project, author: author) } + let_it_be(:public_snippet) { create(:project_snippet, :public, project: project, author: author) } + let_it_be(:internal_snippet) { create(:project_snippet, :internal, project: project, author: author) } + let!(:users) do { unauthenticated: nil, @@ -26,214 +41,212 @@ RSpec.shared_examples 'snippet visibility' do } end - where(:project_type, :feature_visibility, :user_type, :snippet_type, :outcome) do + where(:project_visibility, :feature_visibility, :user_type, :snippet_visibility, :outcome) do [ # Public projects - [:public, ProjectFeature::ENABLED, :unauthenticated, Snippet::PUBLIC, true], - [:public, ProjectFeature::ENABLED, :unauthenticated, Snippet::INTERNAL, false], - [:public, ProjectFeature::ENABLED, :unauthenticated, Snippet::PRIVATE, false], + [:public, :enabled, :unauthenticated, :public, true], + [:public, :enabled, :unauthenticated, :internal, false], + [:public, :enabled, :unauthenticated, :private, false], - [:public, ProjectFeature::ENABLED, :external, Snippet::PUBLIC, true], - [:public, ProjectFeature::ENABLED, :external, Snippet::INTERNAL, false], - [:public, ProjectFeature::ENABLED, :external, Snippet::PRIVATE, false], + [:public, :enabled, :external, :public, true], + [:public, :enabled, :external, :internal, false], + [:public, :enabled, :external, :private, false], - [:public, ProjectFeature::ENABLED, :non_member, Snippet::PUBLIC, true], - [:public, ProjectFeature::ENABLED, :non_member, Snippet::INTERNAL, true], - [:public, ProjectFeature::ENABLED, :non_member, Snippet::PRIVATE, false], + [:public, :enabled, :non_member, :public, true], + [:public, :enabled, :non_member, :internal, true], + [:public, :enabled, :non_member, :private, false], - [:public, ProjectFeature::ENABLED, :member, Snippet::PUBLIC, true], - [:public, ProjectFeature::ENABLED, :member, Snippet::INTERNAL, true], - [:public, ProjectFeature::ENABLED, :member, Snippet::PRIVATE, true], + [:public, :enabled, :member, :public, true], + [:public, :enabled, :member, :internal, true], + [:public, :enabled, :member, :private, true], - [:public, ProjectFeature::ENABLED, :author, Snippet::PUBLIC, true], - [:public, ProjectFeature::ENABLED, :author, Snippet::INTERNAL, true], - [:public, ProjectFeature::ENABLED, :author, Snippet::PRIVATE, true], + [:public, :enabled, :author, :public, true], + [:public, :enabled, :author, :internal, true], + [:public, :enabled, :author, :private, true], - [:public, ProjectFeature::PRIVATE, :unauthenticated, Snippet::PUBLIC, false], - [:public, ProjectFeature::PRIVATE, :unauthenticated, Snippet::INTERNAL, false], - [:public, ProjectFeature::PRIVATE, :unauthenticated, Snippet::PRIVATE, false], + [:public, :private, :unauthenticated, :public, false], + [:public, :private, :unauthenticated, :internal, false], + [:public, :private, :unauthenticated, :private, false], - [:public, ProjectFeature::PRIVATE, :external, Snippet::PUBLIC, false], - [:public, ProjectFeature::PRIVATE, :external, Snippet::INTERNAL, false], - [:public, ProjectFeature::PRIVATE, :external, Snippet::PRIVATE, false], + [:public, :private, :external, :public, false], + [:public, :private, :external, :internal, false], + [:public, :private, :external, :private, false], - [:public, ProjectFeature::PRIVATE, :non_member, Snippet::PUBLIC, false], - [:public, ProjectFeature::PRIVATE, :non_member, Snippet::INTERNAL, false], - [:public, ProjectFeature::PRIVATE, :non_member, Snippet::PRIVATE, false], + [:public, :private, :non_member, :public, false], + [:public, :private, :non_member, :internal, false], + [:public, :private, :non_member, :private, false], - [:public, ProjectFeature::PRIVATE, :member, Snippet::PUBLIC, true], - [:public, ProjectFeature::PRIVATE, :member, Snippet::INTERNAL, true], - [:public, ProjectFeature::PRIVATE, :member, Snippet::PRIVATE, true], + [:public, :private, :member, :public, true], + [:public, :private, :member, :internal, true], + [:public, :private, :member, :private, true], - [:public, ProjectFeature::PRIVATE, :author, Snippet::PUBLIC, true], - [:public, ProjectFeature::PRIVATE, :author, Snippet::INTERNAL, true], - [:public, ProjectFeature::PRIVATE, :author, Snippet::PRIVATE, true], + [:public, :private, :author, :public, true], + [:public, :private, :author, :internal, true], + [:public, :private, :author, :private, true], - [:public, ProjectFeature::DISABLED, :unauthenticated, Snippet::PUBLIC, false], - [:public, ProjectFeature::DISABLED, :unauthenticated, Snippet::INTERNAL, false], - [:public, ProjectFeature::DISABLED, :unauthenticated, Snippet::PRIVATE, false], + [:public, :disabled, :unauthenticated, :public, false], + [:public, :disabled, :unauthenticated, :internal, false], + [:public, :disabled, :unauthenticated, :private, false], - [:public, ProjectFeature::DISABLED, :external, Snippet::PUBLIC, false], - [:public, ProjectFeature::DISABLED, :external, Snippet::INTERNAL, false], - [:public, ProjectFeature::DISABLED, :external, Snippet::PRIVATE, false], + [:public, :disabled, :external, :public, false], + [:public, :disabled, :external, :internal, false], + [:public, :disabled, :external, :private, false], - [:public, ProjectFeature::DISABLED, :non_member, Snippet::PUBLIC, false], - [:public, ProjectFeature::DISABLED, :non_member, Snippet::INTERNAL, false], - [:public, ProjectFeature::DISABLED, :non_member, Snippet::PRIVATE, false], + [:public, :disabled, :non_member, :public, false], + [:public, :disabled, :non_member, :internal, false], + [:public, :disabled, :non_member, :private, false], - [:public, ProjectFeature::DISABLED, :member, Snippet::PUBLIC, false], - [:public, ProjectFeature::DISABLED, :member, Snippet::INTERNAL, false], - [:public, ProjectFeature::DISABLED, :member, Snippet::PRIVATE, false], + [:public, :disabled, :member, :public, false], + [:public, :disabled, :member, :internal, false], + [:public, :disabled, :member, :private, false], - [:public, ProjectFeature::DISABLED, :author, Snippet::PUBLIC, false], - [:public, ProjectFeature::DISABLED, :author, Snippet::INTERNAL, false], - [:public, ProjectFeature::DISABLED, :author, Snippet::PRIVATE, false], + [:public, :disabled, :author, :public, false], + [:public, :disabled, :author, :internal, false], + [:public, :disabled, :author, :private, false], # Internal projects - [:internal, ProjectFeature::ENABLED, :unauthenticated, Snippet::PUBLIC, false], - [:internal, ProjectFeature::ENABLED, :unauthenticated, Snippet::INTERNAL, false], - [:internal, ProjectFeature::ENABLED, :unauthenticated, Snippet::PRIVATE, false], + [:internal, :enabled, :unauthenticated, :public, false], + [:internal, :enabled, :unauthenticated, :internal, false], + [:internal, :enabled, :unauthenticated, :private, false], - [:internal, ProjectFeature::ENABLED, :external, Snippet::PUBLIC, false], - [:internal, ProjectFeature::ENABLED, :external, Snippet::INTERNAL, false], - [:internal, ProjectFeature::ENABLED, :external, Snippet::PRIVATE, false], + [:internal, :enabled, :external, :public, false], + [:internal, :enabled, :external, :internal, false], + [:internal, :enabled, :external, :private, false], - [:internal, ProjectFeature::ENABLED, :non_member, Snippet::PUBLIC, true], - [:internal, ProjectFeature::ENABLED, :non_member, Snippet::INTERNAL, true], - [:internal, ProjectFeature::ENABLED, :non_member, Snippet::PRIVATE, false], + [:internal, :enabled, :non_member, :public, true], + [:internal, :enabled, :non_member, :internal, true], + [:internal, :enabled, :non_member, :private, false], - [:internal, ProjectFeature::ENABLED, :member, Snippet::PUBLIC, true], - [:internal, ProjectFeature::ENABLED, :member, Snippet::INTERNAL, true], - [:internal, ProjectFeature::ENABLED, :member, Snippet::PRIVATE, true], + [:internal, :enabled, :member, :public, true], + [:internal, :enabled, :member, :internal, true], + [:internal, :enabled, :member, :private, true], - [:internal, ProjectFeature::ENABLED, :author, Snippet::PUBLIC, true], - [:internal, ProjectFeature::ENABLED, :author, Snippet::INTERNAL, true], - [:internal, ProjectFeature::ENABLED, :author, Snippet::PRIVATE, true], + [:internal, :enabled, :author, :public, true], + [:internal, :enabled, :author, :internal, true], + [:internal, :enabled, :author, :private, true], - [:internal, ProjectFeature::PRIVATE, :unauthenticated, Snippet::PUBLIC, false], - [:internal, ProjectFeature::PRIVATE, :unauthenticated, Snippet::INTERNAL, false], - [:internal, ProjectFeature::PRIVATE, :unauthenticated, Snippet::PRIVATE, false], + [:internal, :private, :unauthenticated, :public, false], + [:internal, :private, :unauthenticated, :internal, false], + [:internal, :private, :unauthenticated, :private, false], - [:internal, ProjectFeature::PRIVATE, :external, Snippet::PUBLIC, false], - [:internal, ProjectFeature::PRIVATE, :external, Snippet::INTERNAL, false], - [:internal, ProjectFeature::PRIVATE, :external, Snippet::PRIVATE, false], + [:internal, :private, :external, :public, false], + [:internal, :private, :external, :internal, false], + [:internal, :private, :external, :private, false], - [:internal, ProjectFeature::PRIVATE, :non_member, Snippet::PUBLIC, false], - [:internal, ProjectFeature::PRIVATE, :non_member, Snippet::INTERNAL, false], - [:internal, ProjectFeature::PRIVATE, :non_member, Snippet::PRIVATE, false], + [:internal, :private, :non_member, :public, false], + [:internal, :private, :non_member, :internal, false], + [:internal, :private, :non_member, :private, false], - [:internal, ProjectFeature::PRIVATE, :member, Snippet::PUBLIC, true], - [:internal, ProjectFeature::PRIVATE, :member, Snippet::INTERNAL, true], - [:internal, ProjectFeature::PRIVATE, :member, Snippet::PRIVATE, true], + [:internal, :private, :member, :public, true], + [:internal, :private, :member, :internal, true], + [:internal, :private, :member, :private, true], - [:internal, ProjectFeature::PRIVATE, :author, Snippet::PUBLIC, true], - [:internal, ProjectFeature::PRIVATE, :author, Snippet::INTERNAL, true], - [:internal, ProjectFeature::PRIVATE, :author, Snippet::PRIVATE, true], + [:internal, :private, :author, :public, true], + [:internal, :private, :author, :internal, true], + [:internal, :private, :author, :private, true], - [:internal, ProjectFeature::DISABLED, :unauthenticated, Snippet::PUBLIC, false], - [:internal, ProjectFeature::DISABLED, :unauthenticated, Snippet::INTERNAL, false], - [:internal, ProjectFeature::DISABLED, :unauthenticated, Snippet::PRIVATE, false], + [:internal, :disabled, :unauthenticated, :public, false], + [:internal, :disabled, :unauthenticated, :internal, false], + [:internal, :disabled, :unauthenticated, :private, false], - [:internal, ProjectFeature::DISABLED, :external, Snippet::PUBLIC, false], - [:internal, ProjectFeature::DISABLED, :external, Snippet::INTERNAL, false], - [:internal, ProjectFeature::DISABLED, :external, Snippet::PRIVATE, false], + [:internal, :disabled, :external, :public, false], + [:internal, :disabled, :external, :internal, false], + [:internal, :disabled, :external, :private, false], - [:internal, ProjectFeature::DISABLED, :non_member, Snippet::PUBLIC, false], - [:internal, ProjectFeature::DISABLED, :non_member, Snippet::INTERNAL, false], - [:internal, ProjectFeature::DISABLED, :non_member, Snippet::PRIVATE, false], + [:internal, :disabled, :non_member, :public, false], + [:internal, :disabled, :non_member, :internal, false], + [:internal, :disabled, :non_member, :private, false], - [:internal, ProjectFeature::DISABLED, :member, Snippet::PUBLIC, false], - [:internal, ProjectFeature::DISABLED, :member, Snippet::INTERNAL, false], - [:internal, ProjectFeature::DISABLED, :member, Snippet::PRIVATE, false], + [:internal, :disabled, :member, :public, false], + [:internal, :disabled, :member, :internal, false], + [:internal, :disabled, :member, :private, false], - [:internal, ProjectFeature::DISABLED, :author, Snippet::PUBLIC, false], - [:internal, ProjectFeature::DISABLED, :author, Snippet::INTERNAL, false], - [:internal, ProjectFeature::DISABLED, :author, Snippet::PRIVATE, false], + [:internal, :disabled, :author, :public, false], + [:internal, :disabled, :author, :internal, false], + [:internal, :disabled, :author, :private, false], # Private projects - [:private, ProjectFeature::ENABLED, :unauthenticated, Snippet::PUBLIC, false], - [:private, ProjectFeature::ENABLED, :unauthenticated, Snippet::INTERNAL, false], - [:private, ProjectFeature::ENABLED, :unauthenticated, Snippet::PRIVATE, false], + [:private, :enabled, :unauthenticated, :public, false], + [:private, :enabled, :unauthenticated, :internal, false], + [:private, :enabled, :unauthenticated, :private, false], - [:private, ProjectFeature::ENABLED, :external, Snippet::PUBLIC, true], - [:private, ProjectFeature::ENABLED, :external, Snippet::INTERNAL, true], - [:private, ProjectFeature::ENABLED, :external, Snippet::PRIVATE, true], + [:private, :enabled, :external, :public, true], + [:private, :enabled, :external, :internal, true], + [:private, :enabled, :external, :private, true], - [:private, ProjectFeature::ENABLED, :non_member, Snippet::PUBLIC, false], - [:private, ProjectFeature::ENABLED, :non_member, Snippet::INTERNAL, false], - [:private, ProjectFeature::ENABLED, :non_member, Snippet::PRIVATE, false], + [:private, :enabled, :non_member, :public, false], + [:private, :enabled, :non_member, :internal, false], + [:private, :enabled, :non_member, :private, false], - [:private, ProjectFeature::ENABLED, :member, Snippet::PUBLIC, true], - [:private, ProjectFeature::ENABLED, :member, Snippet::INTERNAL, true], - [:private, ProjectFeature::ENABLED, :member, Snippet::PRIVATE, true], + [:private, :enabled, :member, :public, true], + [:private, :enabled, :member, :internal, true], + [:private, :enabled, :member, :private, true], - [:private, ProjectFeature::ENABLED, :author, Snippet::PUBLIC, true], - [:private, ProjectFeature::ENABLED, :author, Snippet::INTERNAL, true], - [:private, ProjectFeature::ENABLED, :author, Snippet::PRIVATE, true], + [:private, :enabled, :author, :public, true], + [:private, :enabled, :author, :internal, true], + [:private, :enabled, :author, :private, true], - [:private, ProjectFeature::PRIVATE, :unauthenticated, Snippet::PUBLIC, false], - [:private, ProjectFeature::PRIVATE, :unauthenticated, Snippet::INTERNAL, false], - [:private, ProjectFeature::PRIVATE, :unauthenticated, Snippet::PRIVATE, false], + [:private, :private, :unauthenticated, :public, false], + [:private, :private, :unauthenticated, :internal, false], + [:private, :private, :unauthenticated, :private, false], - [:private, ProjectFeature::PRIVATE, :external, Snippet::PUBLIC, true], - [:private, ProjectFeature::PRIVATE, :external, Snippet::INTERNAL, true], - [:private, ProjectFeature::PRIVATE, :external, Snippet::PRIVATE, true], + [:private, :private, :external, :public, true], + [:private, :private, :external, :internal, true], + [:private, :private, :external, :private, true], - [:private, ProjectFeature::PRIVATE, :non_member, Snippet::PUBLIC, false], - [:private, ProjectFeature::PRIVATE, :non_member, Snippet::INTERNAL, false], - [:private, ProjectFeature::PRIVATE, :non_member, Snippet::PRIVATE, false], + [:private, :private, :non_member, :public, false], + [:private, :private, :non_member, :internal, false], + [:private, :private, :non_member, :private, false], - [:private, ProjectFeature::PRIVATE, :member, Snippet::PUBLIC, true], - [:private, ProjectFeature::PRIVATE, :member, Snippet::INTERNAL, true], - [:private, ProjectFeature::PRIVATE, :member, Snippet::PRIVATE, true], + [:private, :private, :member, :public, true], + [:private, :private, :member, :internal, true], + [:private, :private, :member, :private, true], - [:private, ProjectFeature::PRIVATE, :author, Snippet::PUBLIC, true], - [:private, ProjectFeature::PRIVATE, :author, Snippet::INTERNAL, true], - [:private, ProjectFeature::PRIVATE, :author, Snippet::PRIVATE, true], + [:private, :private, :author, :public, true], + [:private, :private, :author, :internal, true], + [:private, :private, :author, :private, true], - [:private, ProjectFeature::DISABLED, :unauthenticated, Snippet::PUBLIC, false], - [:private, ProjectFeature::DISABLED, :unauthenticated, Snippet::INTERNAL, false], - [:private, ProjectFeature::DISABLED, :unauthenticated, Snippet::PRIVATE, false], + [:private, :disabled, :unauthenticated, :public, false], + [:private, :disabled, :unauthenticated, :internal, false], + [:private, :disabled, :unauthenticated, :private, false], - [:private, ProjectFeature::DISABLED, :external, Snippet::PUBLIC, false], - [:private, ProjectFeature::DISABLED, :external, Snippet::INTERNAL, false], - [:private, ProjectFeature::DISABLED, :external, Snippet::PRIVATE, false], + [:private, :disabled, :external, :public, false], + [:private, :disabled, :external, :internal, false], + [:private, :disabled, :external, :private, false], - [:private, ProjectFeature::DISABLED, :non_member, Snippet::PUBLIC, false], - [:private, ProjectFeature::DISABLED, :non_member, Snippet::INTERNAL, false], - [:private, ProjectFeature::DISABLED, :non_member, Snippet::PRIVATE, false], + [:private, :disabled, :non_member, :public, false], + [:private, :disabled, :non_member, :internal, false], + [:private, :disabled, :non_member, :private, false], - [:private, ProjectFeature::DISABLED, :member, Snippet::PUBLIC, false], - [:private, ProjectFeature::DISABLED, :member, Snippet::INTERNAL, false], - [:private, ProjectFeature::DISABLED, :member, Snippet::PRIVATE, false], + [:private, :disabled, :member, :public, false], + [:private, :disabled, :member, :internal, false], + [:private, :disabled, :member, :private, false], - [:private, ProjectFeature::DISABLED, :author, Snippet::PUBLIC, false], - [:private, ProjectFeature::DISABLED, :author, Snippet::INTERNAL, false], - [:private, ProjectFeature::DISABLED, :author, Snippet::PRIVATE, false] + [:private, :disabled, :author, :public, false], + [:private, :disabled, :author, :internal, false], + [:private, :disabled, :author, :private, false] ] end with_them do - let!(:project_visibility) { project.update_column(:visibility_level, Gitlab::VisibilityLevel.level_value(project_type.to_s)) } - let!(:project_feature) { project.project_feature.update_column(:snippets_access_level, feature_visibility) } - let!(:user) { users[user_type] } - let!(:snippet) { create(:project_snippet, visibility_level: snippet_type, project: project, author: author) } - let!(:external_member) do - member = project.project_member(external) - - if project.private? - project.add_developer(external) unless member - else - member.delete if member + before do + project.update!(visibility_level: Gitlab::VisibilityLevel.level_value(project_visibility.to_s), snippets_access_level: feature_visibility) + + if user_type == :external + member = project.project_member(external) + + if project.private? + project.add_developer(external) unless member + else + member.delete if member + end end end context "For #{params[:project_type]} project and #{params[:user_type]} users" do - it 'agrees with the read_snippet policy' do + it 'returns proper outcome' do expect(can?(user, :read_snippet, snippet)).to eq(outcome) - end - it 'returns proper outcome' do results = described_class.new(user, project: project).execute expect(results.include?(snippet)).to eq(outcome) @@ -243,16 +256,8 @@ RSpec.shared_examples 'snippet visibility' do context "Without a given project and #{params[:user_type]} users" do it 'returns proper outcome' do results = described_class.new(user).execute - expect(results.include?(snippet)).to eq(outcome) - end - it 'returns no snippets when the user cannot read cross project' do - allow(Ability).to receive(:allowed?).and_call_original - allow(Ability).to receive(:allowed?).with(user, :read_cross_project) { false } - - snippets = described_class.new(user).execute - - expect(snippets).to be_empty + expect(results.include?(snippet)).to eq(outcome) end end end @@ -270,46 +275,55 @@ RSpec.shared_examples 'snippet visibility' do where(:snippet_visibility, :user_type, :outcome) do [ - [Snippet::PUBLIC, :unauthenticated, true], - [Snippet::PUBLIC, :external, true], - [Snippet::PUBLIC, :non_member, true], - [Snippet::PUBLIC, :author, true], - - [Snippet::INTERNAL, :unauthenticated, false], - [Snippet::INTERNAL, :external, false], - [Snippet::INTERNAL, :non_member, true], - [Snippet::INTERNAL, :author, true], - - [Snippet::PRIVATE, :unauthenticated, false], - [Snippet::PRIVATE, :external, false], - [Snippet::PRIVATE, :non_member, false], - [Snippet::PRIVATE, :author, true] + [:public, :unauthenticated, true], + [:public, :external, true], + [:public, :non_member, true], + [:public, :author, true], + + [:internal, :unauthenticated, false], + [:internal, :external, false], + [:internal, :non_member, true], + [:internal, :author, true], + + [:private, :unauthenticated, false], + [:private, :external, false], + [:private, :non_member, false], + [:private, :author, true] ] end with_them do - let!(:user) { users[user_type] } - let!(:snippet) { create(:personal_snippet, visibility_level: snippet_visibility, author: author) } + let_it_be(:private_snippet) { create(:personal_snippet, :private, author: author) } + let_it_be(:public_snippet) { create(:personal_snippet, :public, author: author) } + let_it_be(:internal_snippet) { create(:personal_snippet, :internal, author: author) } context "For personal and #{params[:snippet_visibility]} snippets with #{params[:user_type]} user" do - it 'agrees with read_snippet policy' do + it 'returns proper outcome' do expect(can?(user, :read_snippet, snippet)).to eq(outcome) - end - it 'returns proper outcome' do results = described_class.new(user).execute + expect(results.include?(snippet)).to eq(outcome) end + end + end + end - it 'returns personal snippets when the user cannot read cross project' do - allow(Ability).to receive(:allowed?).and_call_original - allow(Ability).to receive(:allowed?).with(user, :read_cross_project) { false } + context 'when the user cannot read cross project' do + it 'returns only personal snippets' do + personal_snippet = create(:personal_snippet, :public, author: author) + create(:project_snippet, :public, project: project, author: author) - results = described_class.new(user).execute + allow(Ability).to receive(:allowed?).and_call_original + allow(Ability).to receive(:allowed?).with(author, :read_cross_project) { false } - expect(results.include?(snippet)).to eq(outcome) - end - end + service = described_class.new(author) + + expect(service).to receive(:personal_snippets).and_call_original + expect(service).not_to receive(:snippets_of_visible_projects) + expect(service).not_to receive(:snippets_of_authorized_projects) + + expect(service.execute).to match_array([personal_snippet]) end end end diff --git a/spec/support/shared_examples/graphql/design_fields_shared_examples.rb b/spec/support/shared_examples/graphql/design_fields_shared_examples.rb index 029d7e677da..ef7086234c4 100644 --- a/spec/support/shared_examples/graphql/design_fields_shared_examples.rb +++ b/spec/support/shared_examples/graphql/design_fields_shared_examples.rb @@ -35,6 +35,7 @@ RSpec.shared_examples 'a GraphQL type with design fields' do object = GitlabSchema.sync_lazy(GitlabSchema.object_from_id(object_id)) object_type.authorized_new(object, query.context) end + let(:instance_b) do object_b = GitlabSchema.sync_lazy(GitlabSchema.object_from_id(object_id_b)) object_type.authorized_new(object_b, query.context) diff --git a/spec/support/shared_examples/graphql/mutations/resolves_subscription_shared_examples.rb b/spec/support/shared_examples/graphql/mutations/resolves_subscription_shared_examples.rb new file mode 100644 index 00000000000..ebba312e895 --- /dev/null +++ b/spec/support/shared_examples/graphql/mutations/resolves_subscription_shared_examples.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.shared_examples 'a subscribeable graphql resource' do + let(:project) { resource.project } + let_it_be(:user) { create(:user) } + + subject(:mutation) { described_class.new(object: nil, context: { current_user: user }, field: nil) } + + specify { expect(described_class).to require_graphql_authorizations(permission_name) } + + describe '#resolve' do + let(:subscribe) { true } + let(:mutated_resource) { subject[resource.class.name.underscore.to_sym] } + + subject { mutation.resolve(project_path: resource.project.full_path, iid: resource.iid, subscribed_state: subscribe) } + + it 'raises an error if the resource is not accessible to the user' do + expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) + end + + context 'when the user can update the resource' do + before do + resource.project.add_developer(user) + end + + it 'subscribes to the resource' do + expect(mutated_resource).to eq(resource) + expect(mutated_resource.subscribed?(user, project)).to eq(true) + expect(subject[:errors]).to be_empty + end + + context 'when passing subscribe as false' do + let(:subscribe) { false } + + it 'unsubscribes from the discussion' do + resource.subscribe(user, project) + + expect(mutated_resource.subscribed?(user, project)).to eq(false) + end + end + end + end +end diff --git a/spec/support/shared_examples/graphql/mutations/set_assignees_shared_examples.rb b/spec/support/shared_examples/graphql/mutations/set_assignees_shared_examples.rb new file mode 100644 index 00000000000..cfa12171b7e --- /dev/null +++ b/spec/support/shared_examples/graphql/mutations/set_assignees_shared_examples.rb @@ -0,0 +1,126 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.shared_examples 'an assignable resource' do + let_it_be(:user) { create(:user) } + + subject(:mutation) { described_class.new(object: nil, context: { current_user: user }, field: nil) } + + describe '#resolve' do + let_it_be(:assignee) { create(:user) } + let_it_be(:assignee2) { create(:user) } + let(:assignee_usernames) { [assignee.username] } + let(:mutated_resource) { subject[resource.class.name.underscore.to_sym] } + + subject { mutation.resolve(project_path: resource.project.full_path, iid: resource.iid, assignee_usernames: assignee_usernames) } + + before do + resource.project.add_developer(assignee) + resource.project.add_developer(assignee2) + end + + it 'raises an error if the resource is not accessible to the user' do + expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) + end + + context 'when the user can update the resource' do + before do + resource.project.add_developer(user) + end + + it 'replaces the assignee' do + resource.assignees = [assignee2] + resource.save! + + expect(mutated_resource).to eq(resource) + expect(mutated_resource.assignees).to contain_exactly(assignee) + expect(subject[:errors]).to be_empty + end + + it 'returns errors when resource could not be updated' do + allow(resource).to receive(:errors_on_object).and_return(['foo']) + + expect(subject[:errors]).not_to match_array(['foo']) + end + + context 'when passing an empty assignee list' do + let(:assignee_usernames) { [] } + + before do + resource.assignees = [assignee] + resource.save! + end + + it 'removes all assignees' do + expect(mutated_resource).to eq(resource) + expect(mutated_resource.assignees).to eq([]) + expect(subject[:errors]).to be_empty + end + end + + context 'when passing "append" as true' do + subject do + mutation.resolve( + project_path: resource.project.full_path, + iid: resource.iid, + assignee_usernames: assignee_usernames, + operation_mode: Types::MutationOperationModeEnum.enum[:append] + ) + end + + before do + resource.assignees = [assignee2] + resource.save! + + # In CE, APPEND is a NOOP as you can't have multiple assignees + # We test multiple assignment in EE specs + if resource.is_a?(MergeRequest) + stub_licensed_features(multiple_merge_request_assignees: false) + else + stub_licensed_features(multiple_issue_assignees: false) + end + end + + it 'is a NO-OP in FOSS' do + expect(mutated_resource).to eq(resource) + expect(mutated_resource.assignees).to contain_exactly(assignee2) + expect(subject[:errors]).to be_empty + end + end + + context 'when passing "remove" as true' do + before do + resource.assignees = [assignee] + resource.save! + end + + it 'removes named assignee' do + mutated_resource = mutation.resolve( + project_path: resource.project.full_path, + iid: resource.iid, + assignee_usernames: assignee_usernames, + operation_mode: Types::MutationOperationModeEnum.enum[:remove] + )[resource.class.name.underscore.to_sym] + + expect(mutated_resource).to eq(resource) + expect(mutated_resource.assignees).to eq([]) + expect(subject[:errors]).to be_empty + end + + it 'does not remove unnamed assignee' do + mutated_resource = mutation.resolve( + project_path: resource.project.full_path, + iid: resource.iid, + assignee_usernames: [assignee2.username], + operation_mode: Types::MutationOperationModeEnum.enum[:remove] + )[resource.class.name.underscore.to_sym] + + expect(mutated_resource).to eq(resource) + expect(mutated_resource.assignees).to contain_exactly(assignee) + expect(subject[:errors]).to be_empty + end + end + end + end +end diff --git a/spec/support/shared_examples/graphql/notes_on_noteables_shared_examples.rb b/spec/support/shared_examples/graphql/notes_on_noteables_shared_examples.rb index e1dd98814f1..41b7da07d2d 100644 --- a/spec/support/shared_examples/graphql/notes_on_noteables_shared_examples.rb +++ b/spec/support/shared_examples/graphql/notes_on_noteables_shared_examples.rb @@ -8,6 +8,7 @@ RSpec.shared_context 'exposing regular notes on a noteable in GraphQL' do noteable: noteable, project: (noteable.project if noteable.respond_to?(:project))) end + let(:user) { note.author } context 'for regular notes' do diff --git a/spec/support/shared_examples/graphql/projects/merge_request_n_plus_one_query_examples.rb b/spec/support/shared_examples/graphql/projects/merge_request_n_plus_one_query_examples.rb new file mode 100644 index 00000000000..397e22ace28 --- /dev/null +++ b/spec/support/shared_examples/graphql/projects/merge_request_n_plus_one_query_examples.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true +shared_examples 'N+1 query check' do + it 'prevents N+1 queries' do + execute_query # "warm up" to prevent undeterministic counts + + control_count = ActiveRecord::QueryRecorder.new { execute_query }.count + + search_params[:iids] << extra_iid_for_second_query + expect { execute_query }.not_to exceed_query_limit(control_count) + end +end diff --git a/spec/support/shared_examples/lib/api/ci/runner_shared_examples.rb b/spec/support/shared_examples/lib/api/ci/runner_shared_examples.rb new file mode 100644 index 00000000000..bdb0316bf5a --- /dev/null +++ b/spec/support/shared_examples/lib/api/ci/runner_shared_examples.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'API::CI::Runner application context metadata' do |api_route| + it 'contains correct context metadata' do + # Avoids popping the context from the thread so we can + # check its content after the request. + allow(Labkit::Context).to receive(:pop) + + send_request + + Labkit::Context.with_context do |context| + expected_context = { + 'meta.caller_id' => api_route, + 'meta.user' => job.user.username, + 'meta.project' => job.project.full_path, + 'meta.root_namespace' => job.project.full_path_components.first + } + + expect(context.to_h).to include(expected_context) + 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 af65b61021c..8cf6babe146 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 @@ -82,3 +82,25 @@ RSpec.shared_examples 'schedules resource mentions migration' do |resource_class end end end + +RSpec.shared_examples 'resource migration not run' do |migration_class, resource_class| + it 'does not migrate mentions' do + join = migration_class::JOIN + conditions = migration_class::QUERY_CONDITIONS + + expect do + 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| + 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)) + end.to change { user_mentions.count }.by(0) + end +end diff --git a/spec/support/shared_examples/lib/gitlab/kubernetes/network_policy_common_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/kubernetes/network_policy_common_shared_examples.rb new file mode 100644 index 00000000000..a3800f050bb --- /dev/null +++ b/spec/support/shared_examples/lib/gitlab/kubernetes/network_policy_common_shared_examples.rb @@ -0,0 +1,160 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'network policy common specs' do + let(:name) { 'example-name' } + let(:namespace) { 'example-namespace' } + let(:labels) { nil } + + describe 'as_json' do + let(:json_policy) do + { + name: name, + namespace: namespace, + creation_timestamp: nil, + manifest: YAML.dump( + { + metadata: metadata, + spec: spec + }.deep_stringify_keys + ), + is_autodevops: false, + is_enabled: true + } + end + + subject { policy.as_json } + + it { is_expected.to eq(json_policy) } + end + + describe 'autodevops?' do + subject { policy.autodevops? } + + let(:labels) { { chart: chart } } + let(:chart) { nil } + + it { is_expected.to be false } + + context 'with non-autodevops chart' do + let(:chart) { 'foo' } + + it { is_expected.to be false } + end + + context 'with autodevops chart' do + let(:chart) { 'auto-deploy-app-0.6.0' } + + it { is_expected.to be true } + end + end + + describe 'enabled?' do + subject { policy.enabled? } + + let(:selector) { nil } + + it { is_expected.to be true } + + context 'with empty selector' do + let(:selector) { {} } + + it { is_expected.to be true } + end + + context 'with nil matchLabels in selector' do + let(:selector) { { matchLabels: nil } } + + it { is_expected.to be true } + end + + context 'with empty matchLabels in selector' do + let(:selector) { { matchLabels: {} } } + + it { is_expected.to be true } + end + + context 'with disabled_by label in matchLabels in selector' do + let(:selector) do + { matchLabels: { Gitlab::Kubernetes::NetworkPolicyCommon::DISABLED_BY_LABEL => 'gitlab' } } + end + + it { is_expected.to be false } + end + end + + describe 'enable' do + subject { policy.enabled? } + + let(:selector) { nil } + + before do + policy.enable + end + + it { is_expected.to be true } + + context 'with empty selector' do + let(:selector) { {} } + + it { is_expected.to be true } + end + + context 'with nil matchLabels in selector' do + let(:selector) { { matchLabels: nil } } + + it { is_expected.to be true } + end + + context 'with empty matchLabels in selector' do + let(:selector) { { matchLabels: {} } } + + it { is_expected.to be true } + end + + context 'with disabled_by label in matchLabels in selector' do + let(:selector) do + { matchLabels: { Gitlab::Kubernetes::NetworkPolicyCommon::DISABLED_BY_LABEL => 'gitlab' } } + end + + it { is_expected.to be true } + end + end + + describe 'disable' do + subject { policy.enabled? } + + let(:selector) { nil } + + before do + policy.disable + end + + it { is_expected.to be false } + + context 'with empty selector' do + let(:selector) { {} } + + it { is_expected.to be false } + end + + context 'with nil matchLabels in selector' do + let(:selector) { { matchLabels: nil } } + + it { is_expected.to be false } + end + + context 'with empty matchLabels in selector' do + let(:selector) { { matchLabels: {} } } + + it { is_expected.to be false } + end + + context 'with disabled_by label in matchLabels in selector' do + let(:selector) do + { matchLabels: { Gitlab::Kubernetes::NetworkPolicyCommon::DISABLED_BY_LABEL => 'gitlab' } } + end + + it { is_expected.to be false } + end + end +end diff --git a/spec/support/shared_examples/lib/gitlab/template/template_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/template/template_shared_examples.rb new file mode 100644 index 00000000000..6b6e25ca1dd --- /dev/null +++ b/spec/support/shared_examples/lib/gitlab/template/template_shared_examples.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.shared_examples 'file template shared examples' do |filename, file_extension| + describe '.all' do + it "strips the #{file_extension} suffix" do + expect(subject.all.first.name).not_to end_with(file_extension) + end + + it 'ensures that the template name is used exactly once' do + all = subject.all.group_by(&:name) + duplicates = all.select { |_, templates| templates.length > 1 } + + expect(duplicates).to be_empty + end + end + + describe '.by_category' do + it 'returns sorted results' do + result = described_class.by_category('General') + + expect(result).to eq(result.sort) + end + end + + describe '.find' do + it 'returns nil if the file does not exist' do + expect(subject.find('nonexistent-file')).to be nil + end + + it 'returns the corresponding object of a valid file' do + template = subject.find(filename) + + expect(template).to be_a described_class + expect(template.name).to eq(filename) + end + end + + describe '#<=>' do + it 'sorts lexicographically' do + one = described_class.new("a.#{file_extension}") + other = described_class.new("z.#{file_extension}") + + expect(one.<=>(other)).to be(-1) + expect([other, one].sort).to eq([one, other]) + end + end +end diff --git a/spec/support/shared_examples/models/chat_service_shared_examples.rb b/spec/support/shared_examples/models/chat_service_shared_examples.rb index 0a1c27b32db..ad237ad9f49 100644 --- a/spec/support/shared_examples/models/chat_service_shared_examples.rb +++ b/spec/support/shared_examples/models/chat_service_shared_examples.rb @@ -198,6 +198,7 @@ RSpec.shared_examples "chat service" do |service_name| message: "user created page: Awesome wiki_page" } end + let(:wiki_page) { create(:wiki_page, wiki: project.wiki, **opts) } let(:sample_data) { Gitlab::DataBuilder::WikiPage.build(wiki_page, user, "create") } @@ -250,6 +251,7 @@ RSpec.shared_examples "chat service" do |service_name| project: project, status: status, sha: project.commit.sha, ref: project.default_branch) end + let(:sample_data) { Gitlab::DataBuilder::Pipeline.build(pipeline) } context "with failed pipeline" do diff --git a/spec/support/shared_examples/models/cluster_application_helm_cert_shared_examples.rb b/spec/support/shared_examples/models/cluster_application_helm_cert_shared_examples.rb index 239588d3b2f..394253fb699 100644 --- a/spec/support/shared_examples/models/cluster_application_helm_cert_shared_examples.rb +++ b/spec/support/shared_examples/models/cluster_application_helm_cert_shared_examples.rb @@ -28,46 +28,16 @@ RSpec.shared_examples 'cluster application helm specs' do |application_name| describe '#files' do subject { application.files } - context 'managed_apps_local_tiller feature flag is disabled' do - before do - stub_feature_flags(managed_apps_local_tiller: false) - end - - context 'when the helm application does not have a ca_cert' do - before do - application.cluster.application_helm.ca_cert = nil - end - - it 'does not include cert files when there is no ca_cert entry' do - expect(subject).not_to include(:'ca.pem', :'cert.pem', :'key.pem') - end - end - - it 'includes cert files when there is a ca_cert entry' do - expect(subject).to include(:'ca.pem', :'cert.pem', :'key.pem') - expect(subject[:'ca.pem']).to eq(application.cluster.application_helm.ca_cert) - - cert = OpenSSL::X509::Certificate.new(subject[:'cert.pem']) - expect(cert.not_after).to be < 60.minutes.from_now - end + it 'does not include cert files' do + expect(subject).not_to include(:'ca.pem', :'cert.pem', :'key.pem') end - context 'managed_apps_local_tiller feature flag is enabled' do - before do - stub_feature_flags(managed_apps_local_tiller: application.cluster.clusterable) - end + context 'when cluster does not have helm installed' do + let(:application) { create(application_name, :no_helm_installed) } it 'does not include cert files' do expect(subject).not_to include(:'ca.pem', :'cert.pem', :'key.pem') end - - context 'when cluster does not have helm installed' do - let(:application) { create(application_name, :no_helm_installed) } - - it 'does not include cert files' do - expect(subject).not_to include(:'ca.pem', :'cert.pem', :'key.pem') - end - end end end end diff --git a/spec/support/shared_examples/models/cluster_application_initial_status_shared_examples.rb b/spec/support/shared_examples/models/cluster_application_initial_status_shared_examples.rb index 7f0c60d4204..55e458db512 100644 --- a/spec/support/shared_examples/models/cluster_application_initial_status_shared_examples.rb +++ b/spec/support/shared_examples/models/cluster_application_initial_status_shared_examples.rb @@ -6,46 +6,8 @@ RSpec.shared_examples 'cluster application initial status specs' do subject { described_class.new(cluster: cluster) } - context 'local tiller feature flag is disabled' do - before do - stub_feature_flags(managed_apps_local_tiller: false) - end - - it 'sets a default status' do - expect(subject.status_name).to be(:not_installable) - end - end - - context 'local tiller feature flag is enabled' do - before do - stub_feature_flags(managed_apps_local_tiller: cluster.clusterable) - end - - it 'sets a default status' do - expect(subject.status_name).to be(:installable) - end - end - - context 'when application helm is scheduled' do - before do - stub_feature_flags(managed_apps_local_tiller: false) - - create(:clusters_applications_helm, :scheduled, cluster: cluster) - end - - it 'defaults to :not_installable' do - expect(subject.status_name).to be(:not_installable) - end - end - - context 'when application helm is installed' do - before do - create(:clusters_applications_helm, :installed, cluster: cluster) - end - - it 'sets a default status' do - expect(subject.status_name).to be(:installable) - end + it 'sets a default status' do + expect(subject.status_name).to be(:installable) end end end diff --git a/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb b/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb index f80ca235220..7603787a54e 100644 --- a/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb +++ b/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb @@ -48,43 +48,21 @@ RSpec.shared_examples 'cluster application status specs' do |application_name| expect(subject).to be_installed end - context 'managed_apps_local_tiller feature flag disabled' do - before do - stub_feature_flags(managed_apps_local_tiller: false) - end - - it 'updates helm version' do - subject.cluster.application_helm.update!(version: '1.2.3') + it 'does not update the helm version' do + subject.cluster.application_helm.update!(version: '1.2.3') + expect do subject.make_installed! subject.cluster.application_helm.reload - - expect(subject.cluster.application_helm.version).to eq(Gitlab::Kubernetes::Helm::HELM_VERSION) - end + end.not_to change { subject.cluster.application_helm.version } end - context 'managed_apps_local_tiller feature flag enabled' do - before do - stub_feature_flags(managed_apps_local_tiller: subject.cluster.clusterable) - end - - it 'does not update the helm version' do - subject.cluster.application_helm.update!(version: '1.2.3') - - expect do - subject.make_installed! - - subject.cluster.application_helm.reload - end.not_to change { subject.cluster.application_helm.version } - end - - context 'the cluster has no helm installed' do - subject { create(application_name, :installing, :no_helm_installed) } + context 'the cluster has no helm installed' do + subject { create(application_name, :installing, :no_helm_installed) } - it 'runs without errors' do - expect { subject.make_installed! }.not_to raise_error - end + it 'runs without errors' do + expect { subject.make_installed! }.not_to raise_error end end @@ -97,43 +75,21 @@ RSpec.shared_examples 'cluster application status specs' do |application_name| expect(subject).to be_updated end - context 'managed_apps_local_tiller feature flag disabled' do - before do - stub_feature_flags(managed_apps_local_tiller: false) - end - - it 'updates helm version' do - subject.cluster.application_helm.update!(version: '1.2.3') + it 'does not update the helm version' do + subject.cluster.application_helm.update!(version: '1.2.3') + expect do subject.make_installed! subject.cluster.application_helm.reload - - expect(subject.cluster.application_helm.version).to eq(Gitlab::Kubernetes::Helm::HELM_VERSION) - end + end.not_to change { subject.cluster.application_helm.version } end - context 'managed_apps_local_tiller feature flag enabled' do - before do - stub_feature_flags(managed_apps_local_tiller: true) - end - - it 'does not update the helm version' do - subject.cluster.application_helm.update!(version: '1.2.3') - - expect do - subject.make_installed! - - subject.cluster.application_helm.reload - end.not_to change { subject.cluster.application_helm.version } - end - - context 'the cluster has no helm installed' do - subject { create(application_name, :updating, :no_helm_installed) } + context 'the cluster has no helm installed' do + subject { create(application_name, :updating, :no_helm_installed) } - it 'runs without errors' do - expect { subject.make_installed! }.not_to raise_error - end + it 'runs without errors' do + expect { subject.make_installed! }.not_to raise_error end end end @@ -185,62 +141,26 @@ RSpec.shared_examples 'cluster application status specs' do |application_name| expect(subject).to be_installed end - context 'local tiller flag enabled' do - before do - stub_feature_flags(managed_apps_local_tiller: true) - end - - context 'helm record does not exist' do - subject { build(application_name, :installing, :no_helm_installed) } - - it 'does not create a helm record' do - subject.make_externally_installed! - - subject.cluster.reload - expect(subject.cluster.application_helm).to be_nil - end - end - - context 'helm record exists' do - subject { build(application_name, :installing, cluster: old_helm.cluster) } + context 'helm record does not exist' do + subject { build(application_name, :installing, :no_helm_installed) } - it 'does not update helm version' do - subject.make_externally_installed! + it 'does not create a helm record' do + subject.make_externally_installed! - subject.cluster.application_helm.reload - - expect(subject.cluster.application_helm.version).to eq('1.2.3') - end + subject.cluster.reload + expect(subject.cluster.application_helm).to be_nil end end - context 'local tiller flag disabled' do - before do - stub_feature_flags(managed_apps_local_tiller: false) - end - - context 'helm record does not exist' do - subject { build(application_name, :installing, :no_helm_installed) } - - it 'creates a helm record' do - subject.make_externally_installed! - - subject.cluster.reload - expect(subject.cluster.application_helm).to be_present - expect(subject.cluster.application_helm).to be_persisted - end - end - - context 'helm record exists' do - subject { build(application_name, :installing, cluster: old_helm.cluster) } + context 'helm record exists' do + subject { build(application_name, :installing, cluster: old_helm.cluster) } - it 'does not update helm version' do - subject.make_externally_installed! + it 'does not update helm version' do + subject.make_externally_installed! - subject.cluster.application_helm.reload + subject.cluster.application_helm.reload - expect(subject.cluster.application_helm.version).to eq('1.2.3') - end + expect(subject.cluster.application_helm.version).to eq('1.2.3') end end @@ -262,6 +182,14 @@ RSpec.shared_examples 'cluster application status specs' do |application_name| expect(subject).to be_installed end + + it 'clears #status_reason' do + expect(subject.status_reason).not_to be_nil + + subject.make_externally_installed! + + expect(subject.status_reason).to be_nil + end end end @@ -292,6 +220,14 @@ RSpec.shared_examples 'cluster application status specs' do |application_name| expect(subject).to be_uninstalled end + + it 'clears #status_reason' do + expect(subject.status_reason).not_to be_nil + + subject.make_externally_uninstalled! + + expect(subject.status_reason).to be_nil + end end end diff --git a/spec/support/shared_examples/models/concerns/counter_attribute_shared_examples.rb b/spec/support/shared_examples/models/concerns/counter_attribute_shared_examples.rb new file mode 100644 index 00000000000..99a09993900 --- /dev/null +++ b/spec/support/shared_examples/models/concerns/counter_attribute_shared_examples.rb @@ -0,0 +1,176 @@ +# frozen_string_literal: true +require 'spec_helper' + +RSpec.shared_examples_for CounterAttribute do |counter_attributes| + it 'defines a Redis counter_key' do + expect(model.counter_key(:counter_name)) + .to eq("project:{#{model.project_id}}:counters:CounterAttributeModel:#{model.id}:counter_name") + end + + it 'defines a method to store counters' do + expect(model.class.counter_attributes.to_a).to eq(counter_attributes) + end + + counter_attributes.each do |attribute| + describe attribute do + describe '#delayed_increment_counter', :redis do + let(:increment) { 10 } + + subject { model.delayed_increment_counter(attribute, increment) } + + context 'when attribute is a counter attribute' do + where(:increment) { [10, -3] } + + with_them do + it 'increments the counter in Redis' do + subject + + Gitlab::Redis::SharedState.with do |redis| + counter = redis.get(model.counter_key(attribute)) + expect(counter).to eq(increment.to_s) + end + end + + it 'does not increment the counter for the record' do + expect { subject }.not_to change { model.reset.read_attribute(attribute) } + end + + it 'schedules a worker to flush counter increments asynchronously' do + expect(FlushCounterIncrementsWorker).to receive(:perform_in) + .with(CounterAttribute::WORKER_DELAY, model.class.name, model.id, attribute) + .and_call_original + + subject + end + end + + context 'when increment is 0' do + let(:increment) { 0 } + + it 'does nothing' do + expect(FlushCounterIncrementsWorker).not_to receive(:perform_in) + expect(model).not_to receive(:update!) + + subject + end + end + end + + context 'when attribute is not a counter attribute' do + it 'delegates to ActiveRecord update!' do + expect { model.delayed_increment_counter(:unknown_attribute, 10) } + .to raise_error(ActiveModel::MissingAttributeError) + end + end + + context 'when feature flag is disabled' do + before do + stub_feature_flags(efficient_counter_attribute: false) + end + + it 'delegates to ActiveRecord update!' do + expect { subject } + .to change { model.reset.read_attribute(attribute) }.by(increment) + end + + it 'does not increment the counter in Redis' do + subject + + Gitlab::Redis::SharedState.with do |redis| + counter = redis.get(model.counter_key(attribute)) + expect(counter).to be_nil + end + end + end + end + end + end + + describe '.flush_increments_to_database!', :redis do + let(:incremented_attribute) { counter_attributes.first } + + subject { model.flush_increments_to_database!(incremented_attribute) } + + it 'obtains an exclusive lease during processing' do + expect(model) + .to receive(:in_lock) + .with(model.counter_lock_key(incremented_attribute), ttl: described_class::WORKER_LOCK_TTL) + .and_call_original + + subject + end + + context 'when there is a counter to flush' do + before do + model.delayed_increment_counter(incremented_attribute, 10) + model.delayed_increment_counter(incremented_attribute, -3) + end + + it 'updates the record' do + expect { subject }.to change { model.reset.read_attribute(incremented_attribute) }.by(7) + end + + it 'removes the increment entry from Redis' do + Gitlab::Redis::SharedState.with do |redis| + key_exists = redis.exists(model.counter_key(incremented_attribute)) + expect(key_exists).to be_truthy + end + + subject + + Gitlab::Redis::SharedState.with do |redis| + key_exists = redis.exists(model.counter_key(incremented_attribute)) + expect(key_exists).to be_falsey + end + end + end + + context 'when there are no counters to flush' do + context 'when there are no counters in the relative :flushed key' do + it 'does not change the record' do + expect { subject }.not_to change { model.reset.attributes } + end + end + + # This can be the case where updating counters in the database fails with error + # and retrying the worker will retry flushing the counters but the main key has + # disappeared and the increment has been moved to the "<...>:flushed" key. + context 'when there are counters in the relative :flushed key' do + before do + Gitlab::Redis::SharedState.with do |redis| + redis.incrby(model.counter_flushed_key(incremented_attribute), 10) + end + end + + it 'updates the record' do + expect { subject }.to change { model.reset.read_attribute(incremented_attribute) }.by(10) + end + + it 'deletes the relative :flushed key' do + subject + + Gitlab::Redis::SharedState.with do |redis| + key_exists = redis.exists(model.counter_flushed_key(incremented_attribute)) + expect(key_exists).to be_falsey + end + end + end + end + + context 'when deleting :flushed key fails' do + before do + Gitlab::Redis::SharedState.with do |redis| + redis.incrby(model.counter_flushed_key(incremented_attribute), 10) + + expect(redis).to receive(:del).and_raise('could not delete key') + end + end + + it 'does a rollback of the counter update' do + expect { subject }.to raise_error('could not delete key') + + expect(model.reset.read_attribute(incremented_attribute)).to eq(0) + end + end + end +end diff --git a/spec/support/shared_examples/models/concerns/file_store_mounter_shared_examples.rb b/spec/support/shared_examples/models/concerns/file_store_mounter_shared_examples.rb new file mode 100644 index 00000000000..4cb087c47ad --- /dev/null +++ b/spec/support/shared_examples/models/concerns/file_store_mounter_shared_examples.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'mounted file in local store' do + it 'is stored locally' do + expect(subject.file_store).to be(ObjectStorage::Store::LOCAL) + expect(subject.file).to be_file_storage + expect(subject.file.object_store).to eq(ObjectStorage::Store::LOCAL) + end +end + +RSpec.shared_examples 'mounted file in object store' do + it 'is stored remotely' do + expect(subject.file_store).to eq(ObjectStorage::Store::REMOTE) + expect(subject.file).not_to be_file_storage + expect(subject.file.object_store).to eq(ObjectStorage::Store::REMOTE) + end +end diff --git a/spec/support/shared_examples/models/concerns/timebox_shared_examples.rb b/spec/support/shared_examples/models/concerns/timebox_shared_examples.rb index 32d502af5a2..15ca1f56bd0 100644 --- a/spec/support/shared_examples/models/concerns/timebox_shared_examples.rb +++ b/spec/support/shared_examples/models/concerns/timebox_shared_examples.rb @@ -3,7 +3,8 @@ RSpec.shared_examples 'a timebox' do |timebox_type| let(:project) { create(:project, :public) } let(:group) { create(:group) } - let(:timebox) { create(timebox_type, project: project) } + let(:timebox_args) { [] } + let(:timebox) { create(timebox_type, *timebox_args, project: project) } let(:issue) { create(:issue, project: project) } let(:user) { create(:user) } let(:timebox_table_name) { timebox_type.to_s.pluralize.to_sym } @@ -12,7 +13,7 @@ RSpec.shared_examples 'a timebox' do |timebox_type| context 'with a project' do it_behaves_like 'AtomicInternalId' do let(:internal_id_attribute) { :iid } - let(:instance) { build(timebox_type, project: build(:project), group: nil) } + let(:instance) { build(timebox_type, *timebox_args, project: build(:project), group: nil) } let(:scope) { :project } let(:scope_attrs) { { project: instance.project } } let(:usage) { timebox_table_name } @@ -22,7 +23,7 @@ RSpec.shared_examples 'a timebox' do |timebox_type| context 'with a group' do it_behaves_like 'AtomicInternalId' do let(:internal_id_attribute) { :iid } - let(:instance) { build(timebox_type, project: nil, group: build(:group)) } + let(:instance) { build(timebox_type, *timebox_args, project: nil, group: build(:group)) } let(:scope) { :group } let(:scope_attrs) { { namespace: instance.group } } let(:usage) { timebox_table_name } @@ -37,14 +38,14 @@ RSpec.shared_examples 'a timebox' do |timebox_type| describe 'start_date' do it 'adds an error when start_date is greater then due_date' do - timebox = build(timebox_type, start_date: Date.tomorrow, due_date: Date.yesterday) + timebox = build(timebox_type, *timebox_args, start_date: Date.tomorrow, due_date: Date.yesterday) expect(timebox).not_to be_valid expect(timebox.errors[:due_date]).to include("must be greater than start date") end it 'adds an error when start_date is greater than 9999-12-31' do - timebox = build(timebox_type, start_date: Date.new(10000, 1, 1)) + timebox = build(timebox_type, *timebox_args, start_date: Date.new(10000, 1, 1)) expect(timebox).not_to be_valid expect(timebox.errors[:start_date]).to include("date must not be after 9999-12-31") @@ -53,7 +54,7 @@ RSpec.shared_examples 'a timebox' do |timebox_type| describe 'due_date' do it 'adds an error when due_date is greater than 9999-12-31' do - timebox = build(timebox_type, due_date: Date.new(10000, 1, 1)) + timebox = build(timebox_type, *timebox_args, due_date: Date.new(10000, 1, 1)) expect(timebox).not_to be_valid expect(timebox.errors[:due_date]).to include("date must not be after 9999-12-31") @@ -64,7 +65,7 @@ RSpec.shared_examples 'a timebox' do |timebox_type| it { is_expected.to validate_presence_of(:title) } it 'is invalid if title would be empty after sanitation' do - timebox = build(timebox_type, project: project, title: '') + timebox = build(timebox_type, *timebox_args, project: project, title: '') expect(timebox).not_to be_valid expect(timebox.errors[:title]).to include("can't be blank") @@ -73,7 +74,7 @@ RSpec.shared_examples 'a timebox' do |timebox_type| describe '#timebox_type_check' do it 'is invalid if it has both project_id and group_id' do - timebox = build(timebox_type, group: group) + timebox = build(timebox_type, *timebox_args, group: group) timebox.project = project expect(timebox).not_to be_valid @@ -98,7 +99,7 @@ RSpec.shared_examples 'a timebox' do |timebox_type| end context "per group" do - let(:timebox) { create(timebox_type, group: group) } + let(:timebox) { create(timebox_type, *timebox_args, group: group) } before do project.update(group: group) @@ -111,7 +112,7 @@ RSpec.shared_examples 'a timebox' do |timebox_type| end it "does not accept the same title of a child project timebox" do - create(timebox_type, project: group.projects.first) + create(timebox_type, *timebox_args, project: group.projects.first) new_timebox = described_class.new(group: group, title: timebox.title) @@ -143,7 +144,7 @@ RSpec.shared_examples 'a timebox' do |timebox_type| end context 'when project_id is not present' do - let(:timebox) { build(timebox_type, group: group) } + let(:timebox) { build(timebox_type, *timebox_args, group: group) } it 'returns false' do expect(timebox.project_timebox?).to be_falsey @@ -153,7 +154,7 @@ RSpec.shared_examples 'a timebox' do |timebox_type| describe '#group_timebox?' do context 'when group_id is present' do - let(:timebox) { build(timebox_type, group: group) } + let(:timebox) { build(timebox_type, *timebox_args, group: group) } it 'returns true' do expect(timebox.group_timebox?).to be_truthy @@ -168,7 +169,7 @@ RSpec.shared_examples 'a timebox' do |timebox_type| end describe '#safe_title' do - let(:timebox) { create(timebox_type, title: "foo & bar -> 2.2") } + let(:timebox) { create(timebox_type, *timebox_args, title: "foo & bar -> 2.2") } it 'normalizes the title for use as a slug' do expect(timebox.safe_title).to eq('foo-bar-22') @@ -177,7 +178,7 @@ RSpec.shared_examples 'a timebox' do |timebox_type| describe '#resource_parent' do context 'when group is present' do - let(:timebox) { build(timebox_type, group: group) } + let(:timebox) { build(timebox_type, *timebox_args, group: group) } it 'returns the group' do expect(timebox.resource_parent).to eq(group) @@ -192,7 +193,7 @@ RSpec.shared_examples 'a timebox' do |timebox_type| end describe "#title" do - let(:timebox) { create(timebox_type, title: "foo & bar -> 2.2") } + let(:timebox) { create(timebox_type, *timebox_args, title: "foo & bar -> 2.2") } it "sanitizes title" do expect(timebox.title).to eq("foo & bar -> 2.2") @@ -203,28 +204,28 @@ RSpec.shared_examples 'a timebox' do |timebox_type| context "per project" do it "is true for projects with MRs enabled" do project = create(:project, :merge_requests_enabled) - timebox = create(timebox_type, project: project) + timebox = create(timebox_type, *timebox_args, project: project) expect(timebox.merge_requests_enabled?).to be_truthy end it "is false for projects with MRs disabled" do project = create(:project, :repository_enabled, :merge_requests_disabled) - timebox = create(timebox_type, project: project) + timebox = create(timebox_type, *timebox_args, project: project) expect(timebox.merge_requests_enabled?).to be_falsey end it "is false for projects with repository disabled" do project = create(:project, :repository_disabled) - timebox = create(timebox_type, project: project) + timebox = create(timebox_type, *timebox_args, project: project) expect(timebox.merge_requests_enabled?).to be_falsey end end context "per group" do - let(:timebox) { create(timebox_type, group: group) } + let(:timebox) { create(timebox_type, *timebox_args, group: group) } it "is always true for groups, for performance reasons" do expect(timebox.merge_requests_enabled?).to be_truthy @@ -234,7 +235,7 @@ RSpec.shared_examples 'a timebox' do |timebox_type| describe '#to_ability_name' do it 'returns timebox' do - timebox = build(timebox_type) + timebox = build(timebox_type, *timebox_args) expect(timebox.to_ability_name).to eq(timebox_type.to_s) end diff --git a/spec/support/shared_examples/models/issuable_hook_data_shared_examples.rb b/spec/support/shared_examples/models/issuable_hook_data_shared_examples.rb index 21ab9b06c33..13ffc1b7f87 100644 --- a/spec/support/shared_examples/models/issuable_hook_data_shared_examples.rb +++ b/spec/support/shared_examples/models/issuable_hook_data_shared_examples.rb @@ -38,6 +38,7 @@ RSpec.shared_examples 'issuable hook data' do |kind| title_html: %w[foo bar] } end + let(:data) { builder.build(user: user, changes: changes) } it 'populates the :changes hash' do diff --git a/spec/support/shared_examples/models/relative_positioning_shared_examples.rb b/spec/support/shared_examples/models/relative_positioning_shared_examples.rb index 99e62ebf422..e4668926d74 100644 --- a/spec/support/shared_examples/models/relative_positioning_shared_examples.rb +++ b/spec/support/shared_examples/models/relative_positioning_shared_examples.rb @@ -1,11 +1,11 @@ # frozen_string_literal: true RSpec.shared_examples 'a class that supports relative positioning' do - let(:item1) { create(factory, default_params) } - let(:item2) { create(factory, default_params) } - let(:new_item) { create(factory, default_params) } + let(:item1) { create_item } + let(:item2) { create_item } + let(:new_item) { create_item } - def create_item(params) + def create_item(params = {}) create(factory, params.merge(default_params)) end @@ -16,31 +16,119 @@ RSpec.shared_examples 'a class that supports relative positioning' do end describe '.move_nulls_to_end' do + let(:item3) { create_item } + it 'moves items with null relative_position to the end' do + item1.update!(relative_position: 1000) + item2.update!(relative_position: nil) + item3.update!(relative_position: nil) + + items = [item1, item2, item3] + expect(described_class.move_nulls_to_end(items)).to be(2) + + expect(items.sort_by(&:relative_position)).to eq(items) + expect(item1.relative_position).to be(1000) + expect(item1.prev_relative_position).to be_nil + expect(item1.next_relative_position).to eq(item2.relative_position) + expect(item2.next_relative_position).to eq(item3.relative_position) + expect(item3.next_relative_position).to be_nil + end + + it 'preserves relative position' do item1.update!(relative_position: nil) item2.update!(relative_position: nil) described_class.move_nulls_to_end([item1, item2]) - expect(item2.prev_relative_position).to eq item1.relative_position - expect(item1.prev_relative_position).to eq nil - expect(item2.next_relative_position).to eq nil + expect(item1.relative_position).to be < item2.relative_position end it 'moves the item near the start position when there are no existing positions' do item1.update!(relative_position: nil) described_class.move_nulls_to_end([item1]) - - expect(item1.relative_position).to eq(described_class::START_POSITION + described_class::IDEAL_DISTANCE) + expect(item1.reset.relative_position).to eq(described_class::START_POSITION + described_class::IDEAL_DISTANCE) end it 'does not perform any moves if all items have their relative_position set' do item1.update!(relative_position: 1) - expect(item1).not_to receive(:save) + expect(described_class.move_nulls_to_start([item1])).to be(0) + expect(item1.reload.relative_position).to be(1) + end + + it 'manages to move nulls to the end even if there is a sequence at the end' do + bunch = create_items_with_positions(run_at_end) + item1.update!(relative_position: nil) described_class.move_nulls_to_end([item1]) + + items = [*bunch, item1] + items.each(&:reset) + + expect(items.map(&:relative_position)).to all(be_valid_position) + expect(items.sort_by(&:relative_position)).to eq(items) + end + + it 'does not have an N+1 issue' do + create_items_with_positions(10..12) + + a, b, c, d, e, f = create_items_with_positions([nil, nil, nil, nil, nil, nil]) + + baseline = ActiveRecord::QueryRecorder.new do + described_class.move_nulls_to_end([a, e]) + end + + expect { described_class.move_nulls_to_end([b, c, d]) } + .not_to exceed_query_limit(baseline) + + expect { described_class.move_nulls_to_end([f]) } + .not_to exceed_query_limit(baseline.count) + end + end + + describe '.move_nulls_to_start' do + let(:item3) { create_item } + + it 'moves items with null relative_position to the start' do + item1.update!(relative_position: nil) + item2.update!(relative_position: nil) + item3.update!(relative_position: 1000) + + items = [item1, item2, item3] + expect(described_class.move_nulls_to_start(items)).to be(2) + items.map(&:reload) + + expect(items.sort_by(&:relative_position)).to eq(items) + expect(item1.prev_relative_position).to eq nil + expect(item1.next_relative_position).to eq item2.relative_position + expect(item2.next_relative_position).to eq item3.relative_position + expect(item3.next_relative_position).to eq nil + expect(item3.relative_position).to be(1000) + end + + it 'moves the item near the start position when there are no existing positions' do + item1.update!(relative_position: nil) + + described_class.move_nulls_to_start([item1]) + + expect(item1.relative_position).to eq(described_class::START_POSITION - described_class::IDEAL_DISTANCE) + end + + it 'preserves relative position' do + item1.update!(relative_position: nil) + item2.update!(relative_position: nil) + + described_class.move_nulls_to_start([item1, item2]) + + expect(item1.relative_position).to be < item2.relative_position + end + + it 'does not perform any moves if all items have their relative_position set' do + item1.update!(relative_position: 1) + + expect(described_class.move_nulls_to_start([item1])).to be(0) + expect(item1.reload.relative_position).to be(1) end end @@ -52,8 +140,8 @@ RSpec.shared_examples 'a class that supports relative positioning' do describe '#prev_relative_position' do it 'returns previous position if there is an item above' do - item1.update(relative_position: 5) - item2.update(relative_position: 15) + item1.update!(relative_position: 5) + item2.update!(relative_position: 15) expect(item2.prev_relative_position).to eq item1.relative_position end @@ -65,8 +153,8 @@ RSpec.shared_examples 'a class that supports relative positioning' do describe '#next_relative_position' do it 'returns next position if there is an item below' do - item1.update(relative_position: 5) - item2.update(relative_position: 15) + item1.update!(relative_position: 5) + item2.update!(relative_position: 15) expect(item1.next_relative_position).to eq item2.relative_position end @@ -76,9 +164,172 @@ RSpec.shared_examples 'a class that supports relative positioning' do end end + describe '#find_next_gap_before' do + context 'there is no gap' do + let(:items) { create_items_with_positions(run_at_start) } + + it 'returns nil' do + items.each do |item| + expect(item.send(:find_next_gap_before)).to be_nil + end + end + end + + context 'there is a sequence ending at MAX_POSITION' do + let(:items) { create_items_with_positions(run_at_end) } + + let(:gaps) do + items.map { |item| item.send(:find_next_gap_before) } + end + + it 'can find the gap at the start for any item in the sequence' do + gap = { start: items.first.relative_position, end: RelativePositioning::MIN_POSITION } + + expect(gaps).to all(eq(gap)) + end + + it 'respects lower bounds' do + gap = { start: items.first.relative_position, end: 10 } + new_item.update!(relative_position: 10) + + expect(gaps).to all(eq(gap)) + end + end + + specify do + item1.update!(relative_position: 5) + + (0..10).each do |pos| + item2.update!(relative_position: pos) + + gap = item2.send(:find_next_gap_before) + + expect(gap[:start]).to be <= item2.relative_position + expect((gap[:end] - gap[:start]).abs).to be >= RelativePositioning::MIN_GAP + expect(gap[:start]).to be_valid_position + expect(gap[:end]).to be_valid_position + end + end + + it 'deals with there not being any items to the left' do + create_items_with_positions([1, 2, 3]) + new_item.update!(relative_position: 0) + + expect(new_item.send(:find_next_gap_before)).to eq(start: 0, end: RelativePositioning::MIN_POSITION) + end + + it 'finds the next gap to the left, skipping adjacent values' do + create_items_with_positions([1, 9, 10]) + new_item.update!(relative_position: 11) + + expect(new_item.send(:find_next_gap_before)).to eq(start: 9, end: 1) + end + + it 'finds the next gap to the left' do + create_items_with_positions([2, 10]) + + new_item.update!(relative_position: 15) + expect(new_item.send(:find_next_gap_before)).to eq(start: 15, end: 10) + + new_item.update!(relative_position: 11) + expect(new_item.send(:find_next_gap_before)).to eq(start: 10, end: 2) + + new_item.update!(relative_position: 9) + expect(new_item.send(:find_next_gap_before)).to eq(start: 9, end: 2) + + new_item.update!(relative_position: 5) + expect(new_item.send(:find_next_gap_before)).to eq(start: 5, end: 2) + end + end + + describe '#find_next_gap_after' do + context 'there is no gap' do + let(:items) { create_items_with_positions(run_at_end) } + + it 'returns nil' do + items.each do |item| + expect(item.send(:find_next_gap_after)).to be_nil + end + end + end + + context 'there is a sequence starting at MIN_POSITION' do + let(:items) { create_items_with_positions(run_at_start) } + + let(:gaps) do + items.map { |item| item.send(:find_next_gap_after) } + end + + it 'can find the gap at the end for any item in the sequence' do + gap = { start: items.last.relative_position, end: RelativePositioning::MAX_POSITION } + + expect(gaps).to all(eq(gap)) + end + + it 'respects upper bounds' do + gap = { start: items.last.relative_position, end: 10 } + new_item.update!(relative_position: 10) + + expect(gaps).to all(eq(gap)) + end + end + + specify do + item1.update!(relative_position: 5) + + (0..10).each do |pos| + item2.update!(relative_position: pos) + + gap = item2.send(:find_next_gap_after) + + expect(gap[:start]).to be >= item2.relative_position + expect((gap[:end] - gap[:start]).abs).to be >= RelativePositioning::MIN_GAP + expect(gap[:start]).to be_valid_position + expect(gap[:end]).to be_valid_position + end + end + + it 'deals with there not being any items to the right' do + create_items_with_positions([1, 2, 3]) + new_item.update!(relative_position: 5) + + expect(new_item.send(:find_next_gap_after)).to eq(start: 5, end: RelativePositioning::MAX_POSITION) + end + + it 'finds the next gap to the right, skipping adjacent values' do + create_items_with_positions([1, 2, 10]) + new_item.update!(relative_position: 0) + + expect(new_item.send(:find_next_gap_after)).to eq(start: 2, end: 10) + end + + it 'finds the next gap to the right' do + create_items_with_positions([2, 10]) + + new_item.update!(relative_position: 0) + expect(new_item.send(:find_next_gap_after)).to eq(start: 0, end: 2) + + new_item.update!(relative_position: 1) + expect(new_item.send(:find_next_gap_after)).to eq(start: 2, end: 10) + + new_item.update!(relative_position: 3) + expect(new_item.send(:find_next_gap_after)).to eq(start: 3, end: 10) + + new_item.update!(relative_position: 5) + expect(new_item.send(:find_next_gap_after)).to eq(start: 5, end: 10) + end + end + describe '#move_before' do + let(:item3) { create(factory, default_params) } + it 'moves item before' do - [item2, item1].each(&:move_to_end) + [item2, item1].each do |item| + item.move_to_end + item.save! + end + + expect(item1.relative_position).to be > item2.relative_position item1.move_before(item2) @@ -86,12 +337,10 @@ RSpec.shared_examples 'a class that supports relative positioning' do end context 'when there is no space' do - let(:item3) { create(factory, default_params) } - before do - item1.update(relative_position: 1000) - item2.update(relative_position: 1001) - item3.update(relative_position: 1002) + item1.update!(relative_position: 1000) + item2.update!(relative_position: 1001) + item3.update!(relative_position: 1002) end it 'moves items correctly' do @@ -100,6 +349,73 @@ RSpec.shared_examples 'a class that supports relative positioning' do expect(item3.relative_position).to be_between(item1.reload.relative_position, item2.reload.relative_position).exclusive end end + + it 'can move the item before an item at the start' do + item1.update!(relative_position: RelativePositioning::START_POSITION) + + new_item.move_before(item1) + + expect(new_item.relative_position).to be_valid_position + expect(new_item.relative_position).to be < item1.reload.relative_position + end + + it 'can move the item before an item at MIN_POSITION' do + item1.update!(relative_position: RelativePositioning::MIN_POSITION) + + new_item.move_before(item1) + + expect(new_item.relative_position).to be >= RelativePositioning::MIN_POSITION + expect(new_item.relative_position).to be < item1.reload.relative_position + end + + it 'can move the item before an item bunched up at MIN_POSITION' do + item1, item2, item3 = create_items_with_positions(run_at_start) + + new_item.move_before(item3) + new_item.save! + + items = [item1, item2, new_item, item3] + + items.each do |item| + expect(item.reset.relative_position).to be_valid_position + end + + expect(items.sort_by(&:relative_position)).to eq(items) + end + + context 'leap-frogging to the left' do + before do + start = RelativePositioning::START_POSITION + item1.update!(relative_position: start - RelativePositioning::IDEAL_DISTANCE * 0) + item2.update!(relative_position: start - RelativePositioning::IDEAL_DISTANCE * 1) + item3.update!(relative_position: start - RelativePositioning::IDEAL_DISTANCE * 2) + end + + let(:item3) { create(factory, default_params) } + + def leap_frog(steps) + a = item1 + b = item2 + + steps.times do |i| + a.move_before(b) + a.save! + a, b = b, a + end + end + + it 'can leap-frog STEPS - 1 times before needing to rebalance' do + # This is less efficient than going right, due to the flooring of + # integer division + expect { leap_frog(RelativePositioning::STEPS - 1) } + .not_to change { item3.reload.relative_position } + end + + it 'rebalances after leap-frogging STEPS times' do + expect { leap_frog(RelativePositioning::STEPS) } + .to change { item3.reload.relative_position } + end + end end describe '#move_after' do @@ -115,9 +431,17 @@ RSpec.shared_examples 'a class that supports relative positioning' do let(:item3) { create(factory, default_params) } before do - item1.update(relative_position: 1000) - item2.update(relative_position: 1001) - item3.update(relative_position: 1002) + item1.update!(relative_position: 1000) + item2.update!(relative_position: 1001) + item3.update!(relative_position: 1002) + end + + it 'can move the item after an item at MAX_POSITION' do + item1.update!(relative_position: RelativePositioning::MAX_POSITION) + + new_item.move_after(item1) + expect(new_item.relative_position).to be_valid_position + expect(new_item.relative_position).to be > item1.reset.relative_position end it 'moves items correctly' do @@ -126,12 +450,96 @@ RSpec.shared_examples 'a class that supports relative positioning' do expect(item1.relative_position).to be_between(item2.reload.relative_position, item3.reload.relative_position).exclusive end end + + it 'can move the item after an item bunched up at MAX_POSITION' do + item1, item2, item3 = create_items_with_positions(run_at_end) + + new_item.move_after(item1) + new_item.save! + + items = [item1, new_item, item2, item3] + + items.each do |item| + expect(item.reset.relative_position).to be_valid_position + end + + expect(items.sort_by(&:relative_position)).to eq(items) + end + + context 'leap-frogging' do + before do + start = RelativePositioning::START_POSITION + item1.update!(relative_position: start + RelativePositioning::IDEAL_DISTANCE * 0) + item2.update!(relative_position: start + RelativePositioning::IDEAL_DISTANCE * 1) + item3.update!(relative_position: start + RelativePositioning::IDEAL_DISTANCE * 2) + end + + let(:item3) { create(factory, default_params) } + + def leap_frog(steps) + a = item1 + b = item2 + + steps.times do |i| + a.move_after(b) + a.save! + a, b = b, a + end + end + + it 'can leap-frog STEPS times before needing to rebalance' do + expect { leap_frog(RelativePositioning::STEPS) } + .not_to change { item3.reload.relative_position } + end + + it 'rebalances after leap-frogging STEPS+1 times' do + expect { leap_frog(RelativePositioning::STEPS + 1) } + .to change { item3.reload.relative_position } + end + end + end + + describe '#move_to_start' do + before do + [item1, item2].each do |item1| + item1.move_to_start && item1.save! + end + end + + it 'moves item to the end' do + new_item.move_to_start + + expect(new_item.relative_position).to be < item2.relative_position + end + + it 'rebalances when there is already an item at the MIN_POSITION' do + item2.update!(relative_position: RelativePositioning::MIN_POSITION) + + new_item.move_to_start + item2.reset + + expect(new_item.relative_position).to be < item2.relative_position + expect(new_item.relative_position).to be >= RelativePositioning::MIN_POSITION + end + + it 'deals with a run of elements at the start' do + item1.update!(relative_position: RelativePositioning::MIN_POSITION + 1) + item2.update!(relative_position: RelativePositioning::MIN_POSITION) + + new_item.move_to_start + item1.reset + item2.reset + + expect(item2.relative_position).to be < item1.relative_position + expect(new_item.relative_position).to be < item2.relative_position + expect(new_item.relative_position).to be >= RelativePositioning::MIN_POSITION + end end describe '#move_to_end' do before do [item1, item2].each do |item1| - item1.move_to_end && item1.save + item1.move_to_end && item1.save! end end @@ -140,12 +548,44 @@ RSpec.shared_examples 'a class that supports relative positioning' do expect(new_item.relative_position).to be > item2.relative_position end + + it 'rebalances when there is already an item at the MAX_POSITION' do + item2.update!(relative_position: RelativePositioning::MAX_POSITION) + + new_item.move_to_end + item2.reset + + expect(new_item.relative_position).to be > item2.relative_position + expect(new_item.relative_position).to be <= RelativePositioning::MAX_POSITION + end + + it 'deals with a run of elements at the end' do + item1.update!(relative_position: RelativePositioning::MAX_POSITION - 1) + item2.update!(relative_position: RelativePositioning::MAX_POSITION) + + new_item.move_to_end + item1.reset + item2.reset + + expect(item2.relative_position).to be > item1.relative_position + expect(new_item.relative_position).to be > item2.relative_position + expect(new_item.relative_position).to be <= RelativePositioning::MAX_POSITION + end end describe '#move_between' do before do - [item1, item2].each do |item1| - item1.move_to_end && item1.save + [item1, item2].each do |item| + item.move_to_end && item.save! + end + end + + shared_examples 'moves item between' do + it 'moves the middle item to between left and right' do + expect do + middle.move_between(left, right) + middle.save! + end.to change { between_exclusive?(left, middle, right) }.from(false).to(true) end end @@ -169,26 +609,26 @@ RSpec.shared_examples 'a class that supports relative positioning' do end it 'positions items even when after and before positions are the same' do - item2.update relative_position: item1.relative_position + item2.update! relative_position: item1.relative_position new_item.move_between(item1, item2) + [item1, item2].each(&:reset) expect(new_item.relative_position).to be > item1.relative_position expect(item1.relative_position).to be < item2.relative_position end - it 'positions items between other two if distance is 1' do - item2.update relative_position: item1.relative_position + 1 - - new_item.move_between(item1, item2) + context 'the two items are next to each other' do + let(:left) { item1 } + let(:middle) { new_item } + let(:right) { create_item(relative_position: item1.relative_position + 1) } - expect(new_item.relative_position).to be > item1.relative_position - expect(item1.relative_position).to be < item2.relative_position + it_behaves_like 'moves item between' end it 'positions item in the middle of other two if distance is big enough' do - item1.update relative_position: 6000 - item2.update relative_position: 10000 + item1.update! relative_position: 6000 + item2.update! relative_position: 10000 new_item.move_between(item1, item2) @@ -196,7 +636,8 @@ RSpec.shared_examples 'a class that supports relative positioning' do end it 'positions item closer to the middle if we are at the very top' do - item2.update relative_position: 6000 + item1.update!(relative_position: 6001) + item2.update!(relative_position: 6000) new_item.move_between(nil, item2) @@ -204,51 +645,53 @@ RSpec.shared_examples 'a class that supports relative positioning' do end it 'positions item closer to the middle if we are at the very bottom' do - new_item.update relative_position: 1 - item1.update relative_position: 6000 - item2.destroy + new_item.update!(relative_position: 1) + item1.update!(relative_position: 6000) + item2.update!(relative_position: 5999) new_item.move_between(item1, nil) expect(new_item.relative_position).to eq(6000 + RelativePositioning::IDEAL_DISTANCE) end - it 'positions item in the middle of other two if distance is not big enough' do - item1.update relative_position: 100 - item2.update relative_position: 400 + it 'positions item in the middle of other two' do + item1.update! relative_position: 100 + item2.update! relative_position: 400 new_item.move_between(item1, item2) expect(new_item.relative_position).to eq(250) end - it 'positions item in the middle of other two is there is no place' do - item1.update relative_position: 100 - item2.update relative_position: 101 + context 'there is no space' do + let(:middle) { new_item } + let(:left) { create_item(relative_position: 100) } + let(:right) { create_item(relative_position: 101) } - new_item.move_between(item1, item2) - - expect(new_item.relative_position).to be_between(item1.relative_position, item2.relative_position).exclusive + it_behaves_like 'moves item between' end - it 'uses rebalancing if there is no place' do - item1.update relative_position: 100 - item2.update relative_position: 101 - item3 = create_item(relative_position: 102) - new_item.update relative_position: 103 + context 'there is a bunch of items' do + let(:items) { create_items_with_positions(100..104) } + let(:left) { items[1] } + let(:middle) { items[3] } + let(:right) { items[2] } - new_item.move_between(item2, item3) - new_item.save! + it_behaves_like 'moves item between' + + it 'handles bunches correctly' do + middle.move_between(left, right) + middle.save! - expect(new_item.relative_position).to be_between(item2.relative_position, item3.relative_position).exclusive - expect(item1.reload.relative_position).not_to eq(100) + expect(items.first.reset.relative_position).to be < middle.relative_position + end end - it 'positions item right if we pass none-sequential parameters' do - item1.update relative_position: 99 - item2.update relative_position: 101 + it 'positions item right if we pass non-sequential parameters' do + item1.update! relative_position: 99 + item2.update! relative_position: 101 item3 = create_item(relative_position: 102) - new_item.update relative_position: 103 + new_item.update! relative_position: 103 new_item.move_between(item1, item3) new_item.save! @@ -280,6 +723,12 @@ RSpec.shared_examples 'a class that supports relative positioning' do expect(positions).to eq([90, 95, 96, 102]) end + it 'raises an error if there is no space' do + items = create_items_with_positions(run_at_start) + + expect { items.last.move_sequence_before }.to raise_error(RelativePositioning::NoSpaceLeft) + end + it 'finds a gap if there are unused positions' do items = create_items_with_positions([100, 101, 102]) @@ -287,7 +736,8 @@ RSpec.shared_examples 'a class that supports relative positioning' do items.last.save! positions = items.map { |item| item.reload.relative_position } - expect(positions).to eq([50, 51, 102]) + + expect(positions.last - positions.second).to be > RelativePositioning::MIN_GAP end end @@ -309,7 +759,33 @@ RSpec.shared_examples 'a class that supports relative positioning' do items.first.save! positions = items.map { |item| item.reload.relative_position } - expect(positions).to eq([100, 601, 602]) + expect(positions.second - positions.first).to be > RelativePositioning::MIN_GAP end + + it 'raises an error if there is no space' do + items = create_items_with_positions(run_at_end) + + expect { items.first.move_sequence_after }.to raise_error(RelativePositioning::NoSpaceLeft) + end + end + + def be_valid_position + be_between(RelativePositioning::MIN_POSITION, RelativePositioning::MAX_POSITION) + end + + def between_exclusive?(left, middle, right) + a, b, c = [left, middle, right].map { |item| item.reset.relative_position } + return false if a.nil? || b.nil? + return a < b if c.nil? + + a < b && b < c + end + + def run_at_end(size = 3) + (RelativePositioning::MAX_POSITION - size)..RelativePositioning::MAX_POSITION + end + + def run_at_start(size = 3) + (RelativePositioning::MIN_POSITION..).take(size) end end diff --git a/spec/support/shared_examples/models/resource_event_shared_examples.rb b/spec/support/shared_examples/models/resource_event_shared_examples.rb new file mode 100644 index 00000000000..c0158f9b24b --- /dev/null +++ b/spec/support/shared_examples/models/resource_event_shared_examples.rb @@ -0,0 +1,155 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.shared_examples 'a resource event' do + let_it_be(:user1) { create(:user) } + let_it_be(:user2) { create(:user) } + + let_it_be(:issue1) { create(:issue, author: user1) } + let_it_be(:issue2) { create(:issue, author: user1) } + let_it_be(:issue3) { create(:issue, author: user2) } + + describe 'importable' do + it { is_expected.to respond_to(:importing?) } + it { is_expected.to respond_to(:imported?) } + end + + describe 'validations' do + it { is_expected.not_to allow_value(nil).for(:user) } + + context 'when importing' do + before do + allow(subject).to receive(:importing?).and_return(true) + end + + it { is_expected.to allow_value(nil).for(:user) } + end + end + + describe 'associations' do + it { is_expected.to belong_to(:user) } + end + + describe '.created_after' do + let!(:created_at1) { 1.day.ago } + let!(:created_at2) { 2.days.ago } + let!(:created_at3) { 3.days.ago } + + let!(:event1) { create(described_class.name.underscore.to_sym, issue: issue1, created_at: created_at1) } + let!(:event2) { create(described_class.name.underscore.to_sym, issue: issue2, created_at: created_at2) } + let!(:event3) { create(described_class.name.underscore.to_sym, issue: issue2, created_at: created_at3) } + + it 'returns the expected events' do + events = described_class.created_after(created_at3) + + expect(events).to contain_exactly(event1, event2) + end + + it 'returns no events if time is after last record time' do + events = described_class.created_after(1.minute.ago) + + expect(events).to be_empty + end + end +end + +RSpec.shared_examples 'a resource event for issues' do + let_it_be(:user1) { create(:user) } + let_it_be(:user2) { create(:user) } + + let_it_be(:issue1) { create(:issue, author: user1) } + let_it_be(:issue2) { create(:issue, author: user1) } + let_it_be(:issue3) { create(:issue, author: user2) } + + describe 'associations' do + it { is_expected.to belong_to(:issue) } + end + + describe '.by_issue' do + let_it_be(:event1) { create(described_class.name.underscore.to_sym, issue: issue1) } + let_it_be(:event2) { create(described_class.name.underscore.to_sym, issue: issue2) } + let_it_be(:event3) { create(described_class.name.underscore.to_sym, issue: issue1) } + + it 'returns the expected records for an issue with events' do + events = described_class.by_issue(issue1) + + expect(events).to contain_exactly(event1, event3) + end + + it 'returns the expected records for an issue with no events' do + events = described_class.by_issue(issue3) + + expect(events).to be_empty + end + end + + describe '.by_issue_ids_and_created_at_earlier_or_equal_to' do + let_it_be(:event1) { create(described_class.name.underscore.to_sym, issue: issue1, created_at: '2020-03-10') } + let_it_be(:event2) { create(described_class.name.underscore.to_sym, issue: issue2, created_at: '2020-03-10') } + let_it_be(:event3) { create(described_class.name.underscore.to_sym, issue: issue1, created_at: '2020-03-12') } + + it 'returns the expected records for an issue with events' do + events = described_class.by_issue_ids_and_created_at_earlier_or_equal_to([issue1.id, issue2.id], '2020-03-11 23:59:59') + + expect(events).to contain_exactly(event1, event2) + end + + it 'returns the expected records for an issue with no events' do + events = described_class.by_issue_ids_and_created_at_earlier_or_equal_to(issue3, '2020-03-12') + + expect(events).to be_empty + end + end + + if described_class.method_defined?(:issuable) + describe '#issuable' do + let_it_be(:event1) { create(described_class.name.underscore.to_sym, issue: issue2) } + + it 'returns the expected issuable' do + expect(event1.issuable).to eq(issue2) + end + end + end +end + +RSpec.shared_examples 'a resource event for merge requests' do + let_it_be(:user1) { create(:user) } + let_it_be(:user2) { create(:user) } + + let_it_be(:merge_request1) { create(:merge_request, author: user1) } + let_it_be(:merge_request2) { create(:merge_request, author: user1) } + let_it_be(:merge_request3) { create(:merge_request, author: user2) } + + describe 'associations' do + it { is_expected.to belong_to(:merge_request) } + end + + describe '.by_merge_request' do + let_it_be(:event1) { create(described_class.name.underscore.to_sym, merge_request: merge_request1) } + let_it_be(:event2) { create(described_class.name.underscore.to_sym, merge_request: merge_request2) } + let_it_be(:event3) { create(described_class.name.underscore.to_sym, merge_request: merge_request1) } + + it 'returns the expected records for an issue with events' do + events = described_class.by_merge_request(merge_request1) + + expect(events).to contain_exactly(event1, event3) + end + + it 'returns the expected records for an issue with no events' do + events = described_class.by_merge_request(merge_request3) + + expect(events).to be_empty + end + end + + if described_class.method_defined?(:issuable) + describe '#issuable' do + let_it_be(:event1) { create(described_class.name.underscore.to_sym, merge_request: merge_request2) } + + it 'returns the expected issuable' do + expect(event1.issuable).to eq(merge_request2) + 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 new file mode 100644 index 00000000000..07552b62cdd --- /dev/null +++ b/spec/support/shared_examples/models/resource_timebox_event_shared_examples.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.shared_examples 'timebox resource event validations' do + describe 'validations' do + context 'when issue and merge_request are both nil' do + subject { build(described_class.name.underscore.to_sym, issue: nil, merge_request: nil) } + + it { is_expected.not_to be_valid } + end + + context 'when issue and merge_request are both set' do + subject { build(described_class.name.underscore.to_sym, issue: build(:issue), merge_request: build(:merge_request)) } + + it { is_expected.not_to be_valid } + end + + context 'when issue is set' do + subject { create(described_class.name.underscore.to_sym, issue: create(:issue), merge_request: nil) } + + it { is_expected.to be_valid } + end + + context 'when merge_request is set' do + subject { create(described_class.name.underscore.to_sym, issue: nil, merge_request: create(:merge_request)) } + + it { is_expected.to be_valid } + end + end +end + +RSpec.shared_examples 'timebox resource event states' do + describe 'states' do + [Issue, MergeRequest].each do |klass| + klass.available_states.each do |state| + it "supports state #{state.first} for #{klass.name.underscore}" do + model = create(klass.name.underscore, state: state[0]) + key = model.class.name.underscore + event = build(described_class.name.underscore.to_sym, key => model, state: model.state) + + expect(event.state).to eq(state[0]) + end + end + end + end +end + +RSpec.shared_examples 'queryable timebox action resource event' do |expected_results_for_actions| + [Issue, MergeRequest].each do |klass| + expected_results_for_actions.each do |action, expected_result| + it "is #{expected_result} for action #{action} on #{klass.name.underscore}" do + model = build(klass.name.underscore) + key = model.class.name.underscore + event = build(described_class.name.underscore.to_sym, key => model, action: action) + + expect(event.send(query_method)).to eq(expected_result) + end + end + end +end + +RSpec.shared_examples 'timebox resource event actions' do + describe '#added?' do + it_behaves_like 'queryable timebox action resource event', { add: true, remove: false } do + let(:query_method) { :add? } + end + end + + describe '#removed?' do + it_behaves_like 'queryable timebox action resource event', { add: false, remove: true } do + let(:query_method) { :remove? } + end + end +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 7d70df82ec7..7f0da19996e 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 @@ -17,11 +17,14 @@ RSpec.shared_examples 'UpdateProjectStatistics' do context 'when creating' do it 'updates the project statistics' do - delta = read_attribute + delta0 = reload_stat - expect { subject.save! } - .to change { reload_stat } - .by(delta) + subject.save! + + delta1 = reload_stat + + expect(delta1).to eq(delta0 + read_attribute) + expect(delta1).to be > delta0 end it 'schedules a namespace statistics worker' do @@ -80,15 +83,14 @@ RSpec.shared_examples 'UpdateProjectStatistics' do end it 'updates the project statistics' do - delta = -read_attribute + delta0 = reload_stat - expect(ProjectStatistics) - .to receive(:increment_statistic) - .and_call_original + subject.destroy! - expect { subject.destroy! } - .to change { reload_stat } - .by(delta) + delta1 = reload_stat + + expect(delta1).to eq(delta0 - read_attribute) + expect(delta1).to be < delta0 end it 'schedules a namespace statistics worker' do diff --git a/spec/support/shared_examples/path_extraction_shared_examples.rb b/spec/support/shared_examples/path_extraction_shared_examples.rb index 19c6f2404e5..ff55bc9a490 100644 --- a/spec/support/shared_examples/path_extraction_shared_examples.rb +++ b/spec/support/shared_examples/path_extraction_shared_examples.rb @@ -88,9 +88,16 @@ RSpec.shared_examples 'extracts refs' do expect(extract_ref('stable')).to eq(['stable', '']) end - it 'extracts the longest matching ref' do - expect(extract_ref('release/app/v1.0.0/README.md')).to eq( - ['release/app/v1.0.0', 'README.md']) + it 'does not fetch ref names when there is no slash' do + expect(self).not_to receive(:ref_names) + + extract_ref('master') + end + + it 'fetches ref names when there is a slash' do + expect(self).to receive(:ref_names).and_call_original + + extract_ref('release/app/v1.0.0') end end @@ -113,6 +120,61 @@ RSpec.shared_examples 'extracts refs' do it 'falls back to a primitive split for an invalid ref' do expect(extract_ref('stable/CHANGELOG')).to eq(%w(stable CHANGELOG)) end + + it 'extracts the longest matching ref' do + expect(extract_ref('release/app/v1.0.0/README.md')).to eq( + ['release/app/v1.0.0', 'README.md']) + end + + context 'when the repository does not have ambiguous refs' do + before do + allow(container.repository).to receive(:has_ambiguous_refs?).and_return(false) + end + + it 'does not fetch all ref names when the first path component is a ref' do + expect(self).not_to receive(:ref_names) + expect(container.repository).to receive(:branch_names_include?).with('v1.0.0').and_return(false) + expect(container.repository).to receive(:tag_names_include?).with('v1.0.0').and_return(true) + + expect(extract_ref('v1.0.0/doc/README.md')).to eq(['v1.0.0', 'doc/README.md']) + end + + it 'fetches all ref names when the first path component is not a ref' do + expect(self).to receive(:ref_names).and_call_original + expect(container.repository).to receive(:branch_names_include?).with('release').and_return(false) + expect(container.repository).to receive(:tag_names_include?).with('release').and_return(false) + + expect(extract_ref('release/app/doc/README.md')).to eq(['release/app', 'doc/README.md']) + end + + context 'when the extracts_path_optimization feature flag is disabled' do + before do + stub_feature_flags(extracts_path_optimization: false) + end + + it 'always fetches all ref names' do + expect(self).to receive(:ref_names).and_call_original + expect(container.repository).not_to receive(:branch_names_include?) + expect(container.repository).not_to receive(:tag_names_include?) + + expect(extract_ref('v1.0.0/doc/README.md')).to eq(['v1.0.0', 'doc/README.md']) + end + end + end + + context 'when the repository has ambiguous refs' do + before do + allow(container.repository).to receive(:has_ambiguous_refs?).and_return(true) + end + + it 'always fetches all ref names' do + expect(self).to receive(:ref_names).and_call_original + expect(container.repository).not_to receive(:branch_names_include?) + expect(container.repository).not_to receive(:tag_names_include?) + + expect(extract_ref('v1.0.0/doc/README.md')).to eq(['v1.0.0', 'doc/README.md']) + end + end end end end diff --git a/spec/support/shared_examples/policies/project_policy_shared_examples.rb b/spec/support/shared_examples/policies/project_policy_shared_examples.rb index df8e4bc96dd..d8476f5dcc2 100644 --- a/spec/support/shared_examples/policies/project_policy_shared_examples.rb +++ b/spec/support/shared_examples/policies/project_policy_shared_examples.rb @@ -2,24 +2,13 @@ RSpec.shared_examples 'archived project policies' do let(:feature_write_abilities) do - described_class::READONLY_FEATURES_WHEN_ARCHIVED.flat_map do |feature| + described_class.readonly_features.flat_map do |feature| described_class.create_update_admin_destroy(feature) end + additional_maintainer_permissions end let(:other_write_abilities) do - %i[ - create_merge_request_in - create_merge_request_from - push_to_delete_protected_branch - push_code - request_access - upload_file - resolve_note - award_emoji - admin_tag - admin_issue_link - ] + described_class.readonly_abilities end context 'when the project is archived' do 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 5257980d7df..09743c20fba 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 @@ -7,13 +7,17 @@ RSpec.shared_context 'Composer user type' do |user_type, add_member| end end -RSpec.shared_examples 'Composer package index' do |user_type, status, add_member = true| +RSpec.shared_examples 'Composer package index' do |user_type, status, add_member, include_package| include_context 'Composer user type', user_type, add_member do + let(:expected_packages) { include_package == :include_package ? [package] : [] } + let(:presenter) { ::Packages::Composer::PackagesPresenter.new(group, expected_packages ) } + it 'returns the package index' do subject expect(response).to have_gitlab_http_status(status) expect(response).to match_response_schema('public_api/v4/packages/composer/index') + expect(json_response).to eq presenter.root end end end @@ -68,7 +72,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, 'register_package' + it_behaves_like 'a gitlab tracking event', described_class.name, 'push_package' end end @@ -85,7 +89,7 @@ end RSpec.shared_context 'Composer auth headers' do |user_role, user_token| let(:token) { user_token ? personal_access_token.token : 'wrong' } - let(:headers) { user_role == :anonymous ? {} : build_basic_auth_header(user.username, token) } + let(:headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) } end RSpec.shared_context 'Composer api project access' do |project_visibility_level, user_role, user_token| @@ -114,7 +118,7 @@ RSpec.shared_examples 'rejects Composer access with unknown group id' do end context 'as authenticated user' do - subject { get api(url), headers: build_basic_auth_header(user.username, personal_access_token.token) } + subject { get api(url), headers: basic_auth_header(user.username, personal_access_token.token) } it_behaves_like 'process Composer api request', :anonymous, :not_found end @@ -130,7 +134,7 @@ RSpec.shared_examples 'rejects Composer access with unknown project id' do end context 'as authenticated user' do - subject { get api(url), headers: build_basic_auth_header(user.username, personal_access_token.token) } + subject { get api(url), headers: basic_auth_header(user.username, personal_access_token.token) } it_behaves_like 'process Composer api request', :anonymous, :not_found end diff --git a/spec/support/shared_examples/requests/api/graphql/mutations/subscription_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/mutations/subscription_shared_examples.rb new file mode 100644 index 00000000000..40b88ef370f --- /dev/null +++ b/spec/support/shared_examples/requests/api/graphql/mutations/subscription_shared_examples.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.shared_examples 'a subscribable resource api' do + include GraphqlHelpers + + let_it_be(:current_user) { create(:user) } + let(:project) { resource.project } + let(:input) { { subscribed_state: true } } + let(:resource_ref) { resource.class.name.camelize(:lower) } + + let(:mutation) do + variables = { + project_path: project.full_path, + iid: resource.iid.to_s + } + + graphql_mutation( + mutation_name, + variables.merge(input), + <<-QL.strip_heredoc + clientMutationId + errors + #{resource_ref} { + id + subscribed + } + QL + ) + end + + def mutation_response + graphql_mutation_response(mutation_name)[resource_ref]['subscribed'] + end + + context 'when the user is not authorized' do + it_behaves_like 'a mutation that returns top-level errors', + errors: ["The resource that you are attempting to access "\ + "does not exist or you don't have permission to "\ + "perform this action"] + end + + context 'when user is authorized' do + before do + project.add_developer(current_user) + end + + it 'marks the resource as subscribed' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(response).to have_gitlab_http_status(:success) + expect(mutation_response).to eq(true) + end + + context 'when passing subscribe false as input' do + let(:input) { { subscribed_state: false } } + + it 'unmarks the resource as subscribed' do + resource.subscribe(current_user, project) + + post_graphql_mutation(mutation, current_user: current_user) + + expect(response).to have_gitlab_http_status(:success) + expect(mutation_response).to eq(false) + end + end + end +end diff --git a/spec/support/shared_examples/requests/api/milestones_shared_examples.rb b/spec/support/shared_examples/requests/api/milestones_shared_examples.rb index 77b49b7caef..249a7b7cdac 100644 --- a/spec/support/shared_examples/requests/api/milestones_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/milestones_shared_examples.rb @@ -266,6 +266,7 @@ RSpec.shared_examples 'group and project milestones' do |route_definition| let!(:milestone) do context_group ? create(:milestone, group: context_group) : create(:milestone, project: public_project) end + let!(:issue) { create(:issue, project: public_project) } let!(:confidential_issue) { create(:issue, confidential: true, project: public_project) } let!(:issues_route) do diff --git a/spec/support/shared_examples/requests/api/notes_shared_examples.rb b/spec/support/shared_examples/requests/api/notes_shared_examples.rb index a34c48a5ba4..7066f803f9d 100644 --- a/spec/support/shared_examples/requests/api/notes_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/notes_shared_examples.rb @@ -158,9 +158,11 @@ RSpec.shared_examples 'noteable API' do |parent_type, noteable_type, id_name| end it "creates an activity event when a note is created", :sidekiq_might_not_need_inline do - expect(Event).to receive(:create!) + uri = "/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes" - post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", user), params: { body: 'hi!' } + expect do + post api(uri, user), params: { body: 'hi!' } + end.to change(Event, :count).by(1) end context 'setting created_at' do @@ -275,12 +277,53 @@ RSpec.shared_examples 'noteable API' do |parent_type, noteable_type, id_name| end describe "PUT /#{parent_type}/:id/#{noteable_type}/:noteable_id/notes/:note_id" do - it 'returns modified note' do - put api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/"\ - "notes/#{note.id}", user), params: { body: 'Hello!' } + let(:params) { { body: 'Hello!', confidential: false } } - expect(response).to have_gitlab_http_status(:ok) - expect(json_response['body']).to eq('Hello!') + subject do + put api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes/#{note.id}", user), params: params + end + + context 'when eveything is ok' do + before do + note.update!(confidential: true) + end + + context 'with multiple params present' do + before do + subject + end + + it 'returns modified note' do + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['body']).to eq('Hello!') + expect(json_response['confidential']).to be_falsey + end + + it 'updates the note' do + expect(note.reload.note).to eq('Hello!') + expect(note.confidential).to be_falsey + end + end + + context 'when only body param is present' do + let(:params) { { body: 'Hello!' } } + + it 'updates only the note text' do + expect { subject }.not_to change { note.reload.confidential } + + expect(note.note).to eq('Hello!') + end + end + + context 'when only confidential param is present' do + let(:params) { { confidential: false } } + + it 'updates only the note text' do + expect { subject }.not_to change { note.reload.note } + + expect(note.confidential).to be_falsey + end + end end it 'returns a 404 error when note id not found' do @@ -290,9 +333,9 @@ RSpec.shared_examples 'noteable API' do |parent_type, noteable_type, id_name| expect(response).to have_gitlab_http_status(:not_found) end - it 'returns a 400 bad request error if body not given' do + it 'returns a 400 bad request error if body is empty' do put api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/"\ - "notes/#{note.id}", user) + "notes/#{note.id}", user), params: { body: '' } expect(response).to have_gitlab_http_status(:bad_request) 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 8d8483cae72..fcdc594f258 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 @@ -122,7 +122,7 @@ RSpec.shared_examples 'process nuget workhorse authorization' do |user_type, sta context 'with a request that bypassed gitlab-workhorse' do let(:headers) do - build_basic_auth_header(user.username, personal_access_token.token) + basic_auth_header(user.username, personal_access_token.token) .merge(workhorse_header) .tap { |h| h.delete(Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER) } end @@ -180,6 +180,7 @@ RSpec.shared_examples 'process nuget upload' do |user_type, status, add_member = body: 'content' ) end + let(:fog_file) { fog_to_uploaded_file(tmp_object) } let(:params) { { package: fog_file, 'package.remote_id' => file_name } } @@ -400,7 +401,7 @@ RSpec.shared_examples 'rejects nuget access with unknown project id' do end context 'as authenticated user' do - subject { get api(url), headers: build_basic_auth_header(user.username, personal_access_token.token) } + subject { get api(url), headers: basic_auth_header(user.username, personal_access_token.token) } it_behaves_like 'rejects nuget packages access', :anonymous, :not_found end 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 ec15d7a4d2e..6f4a0236b66 100644 --- a/spec/support/shared_examples/requests/api/packages_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/packages_shared_examples.rb @@ -2,7 +2,7 @@ RSpec.shared_examples 'deploy token for package GET requests' do context 'with deploy token headers' do - let(:headers) { build_basic_auth_header(deploy_token.username, deploy_token.token) } + let(:headers) { basic_auth_header(deploy_token.username, deploy_token.token) } subject { get api(url), headers: headers } @@ -15,7 +15,7 @@ RSpec.shared_examples 'deploy token for package GET requests' do end context 'invalid token' do - let(:headers) { build_basic_auth_header(deploy_token.username, 'bar') } + let(:headers) { basic_auth_header(deploy_token.username, 'bar') } it_behaves_like 'returning response status', :unauthorized end @@ -24,7 +24,7 @@ end RSpec.shared_examples 'deploy token for package uploads' do context 'with deploy token headers' do - let(:headers) { build_basic_auth_header(deploy_token.username, deploy_token.token).merge(workhorse_header) } + let(:headers) { basic_auth_header(deploy_token.username, deploy_token.token).merge(workhorse_header) } before do project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE) @@ -35,7 +35,7 @@ RSpec.shared_examples 'deploy token for package uploads' do end context 'invalid token' do - let(:headers) { build_basic_auth_header(deploy_token.username, 'bar').merge(workhorse_header) } + let(:headers) { basic_auth_header(deploy_token.username, 'bar').merge(workhorse_header) } it_behaves_like 'returning response status', :unauthorized 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 fcc166ac87d..4954151b93b 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 @@ -24,6 +24,20 @@ RSpec.shared_examples 'PyPi package creation' do |user_type, status, add_member it_behaves_like 'creating pypi package files' + context 'with a pre-existing file' do + it 'rejects the duplicated file' do + existing_package = create(:pypi_package, name: base_params[:name], version: base_params[:version], project: project) + create(:package_file, :pypi, package: existing_package, file_name: params[:content].original_filename) + + expect { subject } + .to change { project.packages.pypi.count }.by(0) + .and change { Packages::PackageFile.count }.by(0) + .and change { Packages::Pypi::Metadatum.count }.by(0) + + expect(response).to have_gitlab_http_status(:bad_request) + end + end + context 'with object storage disabled' do before do stub_package_file_object_storage(enabled: false) @@ -49,6 +63,7 @@ RSpec.shared_examples 'PyPi package creation' do |user_type, status, add_member body: 'content' ) end + let(:fog_file) { fog_to_uploaded_file(tmp_object) } let(:params) { base_params.merge(content: fog_file, 'content.remote_id' => file_name) } @@ -144,7 +159,7 @@ RSpec.shared_examples 'rejects PyPI access with unknown project id' do end context 'as authenticated user' do - subject { get api(url), headers: build_basic_auth_header(user.username, personal_access_token.token) } + subject { get api(url), headers: basic_auth_header(user.username, personal_access_token.token) } it_behaves_like 'process PyPi api request', :anonymous, :not_found end diff --git a/spec/support/shared_examples/requests/snippet_shared_examples.rb b/spec/support/shared_examples/requests/snippet_shared_examples.rb index 644abb191a6..a17163328f4 100644 --- a/spec/support/shared_examples/requests/snippet_shared_examples.rb +++ b/spec/support/shared_examples/requests/snippet_shared_examples.rb @@ -106,3 +106,80 @@ RSpec.shared_examples 'snippet_multiple_files feature disabled' do expect(json_response).not_to have_key('files') end end + +RSpec.shared_examples 'snippet creation with files parameter' do + using RSpec::Parameterized::TableSyntax + + where(:path, :content, :status, :error) do + '.gitattributes' | 'file content' | :created | nil + 'valid/path/file.rb' | 'file content' | :created | nil + + '.gitattributes' | nil | :bad_request | 'files[0][content] is empty' + '.gitattributes' | '' | :bad_request | 'files[0][content] is empty' + + '' | 'file content' | :bad_request | 'files[0][file_path] is empty' + nil | 'file content' | :bad_request | 'files[0][file_path] should be a valid file path, files[0][file_path] is empty' + '../../etc/passwd' | 'file content' | :bad_request | 'files[0][file_path] should be a valid file path' + end + + with_them do + let(:file_path) { path } + let(:file_content) { content } + + before do + subject + end + + it 'responds correctly' do + expect(response).to have_gitlab_http_status(status) + expect(json_response['error']).to eq(error) + end + end + + it 'returns 400 if both files and content are provided' do + params[:file_name] = 'foo.rb' + params[:content] = 'bar' + + subject + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['error']).to eq 'files, content are mutually exclusive' + end + + it 'returns 400 when neither files or content are provided' do + params.delete(:files) + + subject + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['error']).to eq 'files, content are missing, exactly one parameter must be provided' + end +end + +RSpec.shared_examples 'snippet creation without files parameter' do + let(:file_params) { { file_name: 'testing.rb', content: 'snippet content' } } + + it 'allows file_name and content parameters' do + subject + + expect(response).to have_gitlab_http_status(:created) + end + + it 'returns 400 if file_name and content are not both provided' do + params.delete(:file_name) + + subject + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['error']).to eq 'file_name is missing' + end + + it 'returns 400 if content is blank' do + params[:content] = '' + + subject + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['error']).to eq 'content is empty' + end +end diff --git a/spec/support/shared_examples/resource_events.rb b/spec/support/shared_examples/resource_events.rb deleted file mode 100644 index c0158f9b24b..00000000000 --- a/spec/support/shared_examples/resource_events.rb +++ /dev/null @@ -1,155 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.shared_examples 'a resource event' do - let_it_be(:user1) { create(:user) } - let_it_be(:user2) { create(:user) } - - let_it_be(:issue1) { create(:issue, author: user1) } - let_it_be(:issue2) { create(:issue, author: user1) } - let_it_be(:issue3) { create(:issue, author: user2) } - - describe 'importable' do - it { is_expected.to respond_to(:importing?) } - it { is_expected.to respond_to(:imported?) } - end - - describe 'validations' do - it { is_expected.not_to allow_value(nil).for(:user) } - - context 'when importing' do - before do - allow(subject).to receive(:importing?).and_return(true) - end - - it { is_expected.to allow_value(nil).for(:user) } - end - end - - describe 'associations' do - it { is_expected.to belong_to(:user) } - end - - describe '.created_after' do - let!(:created_at1) { 1.day.ago } - let!(:created_at2) { 2.days.ago } - let!(:created_at3) { 3.days.ago } - - let!(:event1) { create(described_class.name.underscore.to_sym, issue: issue1, created_at: created_at1) } - let!(:event2) { create(described_class.name.underscore.to_sym, issue: issue2, created_at: created_at2) } - let!(:event3) { create(described_class.name.underscore.to_sym, issue: issue2, created_at: created_at3) } - - it 'returns the expected events' do - events = described_class.created_after(created_at3) - - expect(events).to contain_exactly(event1, event2) - end - - it 'returns no events if time is after last record time' do - events = described_class.created_after(1.minute.ago) - - expect(events).to be_empty - end - end -end - -RSpec.shared_examples 'a resource event for issues' do - let_it_be(:user1) { create(:user) } - let_it_be(:user2) { create(:user) } - - let_it_be(:issue1) { create(:issue, author: user1) } - let_it_be(:issue2) { create(:issue, author: user1) } - let_it_be(:issue3) { create(:issue, author: user2) } - - describe 'associations' do - it { is_expected.to belong_to(:issue) } - end - - describe '.by_issue' do - let_it_be(:event1) { create(described_class.name.underscore.to_sym, issue: issue1) } - let_it_be(:event2) { create(described_class.name.underscore.to_sym, issue: issue2) } - let_it_be(:event3) { create(described_class.name.underscore.to_sym, issue: issue1) } - - it 'returns the expected records for an issue with events' do - events = described_class.by_issue(issue1) - - expect(events).to contain_exactly(event1, event3) - end - - it 'returns the expected records for an issue with no events' do - events = described_class.by_issue(issue3) - - expect(events).to be_empty - end - end - - describe '.by_issue_ids_and_created_at_earlier_or_equal_to' do - let_it_be(:event1) { create(described_class.name.underscore.to_sym, issue: issue1, created_at: '2020-03-10') } - let_it_be(:event2) { create(described_class.name.underscore.to_sym, issue: issue2, created_at: '2020-03-10') } - let_it_be(:event3) { create(described_class.name.underscore.to_sym, issue: issue1, created_at: '2020-03-12') } - - it 'returns the expected records for an issue with events' do - events = described_class.by_issue_ids_and_created_at_earlier_or_equal_to([issue1.id, issue2.id], '2020-03-11 23:59:59') - - expect(events).to contain_exactly(event1, event2) - end - - it 'returns the expected records for an issue with no events' do - events = described_class.by_issue_ids_and_created_at_earlier_or_equal_to(issue3, '2020-03-12') - - expect(events).to be_empty - end - end - - if described_class.method_defined?(:issuable) - describe '#issuable' do - let_it_be(:event1) { create(described_class.name.underscore.to_sym, issue: issue2) } - - it 'returns the expected issuable' do - expect(event1.issuable).to eq(issue2) - end - end - end -end - -RSpec.shared_examples 'a resource event for merge requests' do - let_it_be(:user1) { create(:user) } - let_it_be(:user2) { create(:user) } - - let_it_be(:merge_request1) { create(:merge_request, author: user1) } - let_it_be(:merge_request2) { create(:merge_request, author: user1) } - let_it_be(:merge_request3) { create(:merge_request, author: user2) } - - describe 'associations' do - it { is_expected.to belong_to(:merge_request) } - end - - describe '.by_merge_request' do - let_it_be(:event1) { create(described_class.name.underscore.to_sym, merge_request: merge_request1) } - let_it_be(:event2) { create(described_class.name.underscore.to_sym, merge_request: merge_request2) } - let_it_be(:event3) { create(described_class.name.underscore.to_sym, merge_request: merge_request1) } - - it 'returns the expected records for an issue with events' do - events = described_class.by_merge_request(merge_request1) - - expect(events).to contain_exactly(event1, event3) - end - - it 'returns the expected records for an issue with no events' do - events = described_class.by_merge_request(merge_request3) - - expect(events).to be_empty - end - end - - if described_class.method_defined?(:issuable) - describe '#issuable' do - let_it_be(:event1) { create(described_class.name.underscore.to_sym, merge_request: merge_request2) } - - it 'returns the expected issuable' do - expect(event1.issuable).to eq(merge_request2) - end - end - end -end diff --git a/spec/support/shared_examples/services/boards/issues_list_service_shared_examples.rb b/spec/support/shared_examples/services/boards/issues_list_service_shared_examples.rb index 756c4136059..06e2b715e6d 100644 --- a/spec/support/shared_examples/services/boards/issues_list_service_shared_examples.rb +++ b/spec/support/shared_examples/services/boards/issues_list_service_shared_examples.rb @@ -19,6 +19,15 @@ RSpec.shared_examples 'issues list service' do end end + it 'avoids N+1' do + params = { board_id: board.id } + control = ActiveRecord::QueryRecorder.new { described_class.new(parent, user, params).execute } + + create(:list, board: board) + + expect { described_class.new(parent, user, params).execute }.not_to exceed_query_limit(control) + end + context 'issues are ordered by priority' do it 'returns opened issues when list_id is missing' do params = { board_id: board.id } @@ -71,4 +80,17 @@ RSpec.shared_examples 'issues list service' do expect { service.execute }.to raise_error(ActiveRecord::RecordNotFound) end end + + context 'when :all_lists is used' do + it 'returns issues from all lists' do + params = { board_id: board.id, all_lists: true } + + issues = described_class.new(parent, user, params).execute + + expected = [opened_issue2, reopened_issue1, opened_issue1, list1_issue1, + list1_issue2, list1_issue3, list2_issue1, closed_issue1, + closed_issue2, closed_issue3, closed_issue4, closed_issue5] + expect(issues).to match_array(expected) + end + end end diff --git a/spec/support/shared_examples/services/boards/lists_list_service_shared_examples.rb b/spec/support/shared_examples/services/boards/lists_list_service_shared_examples.rb index 07a6353296d..41fd286682e 100644 --- a/spec/support/shared_examples/services/boards/lists_list_service_shared_examples.rb +++ b/spec/support/shared_examples/services/boards/lists_list_service_shared_examples.rb @@ -26,4 +26,22 @@ RSpec.shared_examples 'lists list service' do expect(service.execute(board)).to eq [board.backlog_list, list, board.closed_list] end end + + context 'when wanting a specific list' do + let!(:list1) { create(:list, board: board) } + + it 'returns list specified by id' do + service = described_class.new(parent, user, list_id: list1.id) + + expect(service.execute(board, create_default_lists: false)).to eq [list1] + end + + it 'returns empty result when list is not found' do + external_board = create(:board, resource_parent: create(:project)) + external_list = create(:list, board: external_board) + service = described_class.new(parent, user, list_id: external_list.id) + + expect(service.execute(board, create_default_lists: false)).to eq(List.none) + end + end end diff --git a/spec/support/shared_examples/services/jira_import/user_mapper_services_shared_examples.rb b/spec/support/shared_examples/services/jira_import/user_mapper_services_shared_examples.rb new file mode 100644 index 00000000000..7fc7ff8a8de --- /dev/null +++ b/spec/support/shared_examples/services/jira_import/user_mapper_services_shared_examples.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'mapping jira users' do + let(:client) { double } + + let_it_be(:project) { create(:project) } + let_it_be(:jira_service) { create(:jira_service, project: project, active: true) } + + before do + allow(subject).to receive(:client).and_return(client) + allow(client).to receive(:get).with(url).and_return(jira_users) + end + + subject { described_class.new(jira_service, start_at) } + + context 'jira_users is nil' do + let(:jira_users) { nil } + + it 'returns an empty array' do + expect(subject.execute).to be_empty + end + end + + context 'when jira_users is present' do + # TODO: now we only create an array in a proper format + # mapping is tracked in https://gitlab.com/gitlab-org/gitlab/-/issues/219023 + let(:mapped_users) do + [ + { jira_account_id: 'abcd', jira_display_name: 'user1', jira_email: nil, gitlab_id: nil, gitlab_username: nil, gitlab_name: nil }, + { jira_account_id: 'efg', jira_display_name: nil, jira_email: nil, gitlab_id: nil, gitlab_username: nil, gitlab_name: nil }, + { jira_account_id: 'hij', jira_display_name: 'user3', jira_email: 'user3@example.com', gitlab_id: nil, gitlab_username: nil, gitlab_name: nil } + ] + end + + it 'returns users mapped to Gitlab' do + expect(subject.execute).to eq(mapped_users) + end + end +end diff --git a/spec/support/shared_examples/services/metrics/dashboard_shared_examples.rb b/spec/support/shared_examples/services/metrics/dashboard_shared_examples.rb index c8fabfe30b9..1501a2a0f52 100644 --- a/spec/support/shared_examples/services/metrics/dashboard_shared_examples.rb +++ b/spec/support/shared_examples/services/metrics/dashboard_shared_examples.rb @@ -62,7 +62,7 @@ end RSpec.shared_examples 'dashboard_version contains SHA256 hash of dashboard file content' do specify do dashboard = File.read(Rails.root.join(dashboard_path)) - expect(Digest::SHA256.hexdigest(dashboard)).to eq(dashboard_version) + expect(dashboard_version).to eq(Digest::SHA256.hexdigest(dashboard)) end end @@ -78,6 +78,12 @@ RSpec.shared_examples 'raises error for users with insufficient permissions' do it_behaves_like 'misconfigured dashboard service response', :unauthorized end + + context 'when the user is anonymous' do + let(:user) { nil } + + it_behaves_like 'misconfigured dashboard service response', :unauthorized + end end RSpec.shared_examples 'valid dashboard cloning process' do |dashboard_template, sequence| diff --git a/spec/support/shared_examples/services/projects/update_repository_storage_service_shared_examples.rb b/spec/support/shared_examples/services/projects/update_repository_storage_service_shared_examples.rb index 2ddbdebdb97..f201c7b1780 100644 --- a/spec/support/shared_examples/services/projects/update_repository_storage_service_shared_examples.rb +++ b/spec/support/shared_examples/services/projects/update_repository_storage_service_shared_examples.rb @@ -2,9 +2,11 @@ RSpec.shared_examples 'moves repository to another storage' do |repository_type| let(:project_repository_double) { double(:repository) } + let(:original_project_repository_double) { double(:repository) } let!(:project_repository_checksum) { project.repository.checksum } let(:repository_double) { double(:repository) } + let(:original_repository_double) { double(:repository) } let(:repository_checksum) { repository.checksum } before do @@ -14,10 +16,16 @@ RSpec.shared_examples 'moves repository to another storage' do |repository_type| allow(Gitlab::Git::Repository).to receive(:new) .with('test_second_storage', project.repository.raw.relative_path, project.repository.gl_repository, project.repository.full_path) .and_return(project_repository_double) + allow(Gitlab::Git::Repository).to receive(:new) + .with('default', project.repository.raw.relative_path, nil, nil) + .and_return(original_project_repository_double) allow(Gitlab::Git::Repository).to receive(:new) .with('test_second_storage', repository.raw.relative_path, repository.gl_repository, repository.full_path) .and_return(repository_double) + allow(Gitlab::Git::Repository).to receive(:new) + .with('default', repository.raw.relative_path, nil, nil) + .and_return(original_repository_double) end context 'when the move succeeds', :clean_gitlab_redis_shared_state do @@ -35,8 +43,8 @@ RSpec.shared_examples 'moves repository to another storage' do |repository_type| allow(repository_double).to receive(:checksum) .and_return(repository_checksum) - expect(GitlabShellWorker).to receive(:perform_async).with(:mv_repository, 'default', anything, anything) - .twice.and_call_original + expect(original_project_repository_double).to receive(:remove) + expect(original_repository_double).to receive(:remove) end it "moves the project and its #{repository_type} repository to the new storage and unmarks the repository as read only" do @@ -110,13 +118,36 @@ RSpec.shared_examples 'moves repository to another storage' do |repository_type| .with(repository.raw) .and_raise(Gitlab::Git::CommandError) - expect(GitlabShellWorker).not_to receive(:perform_async) - result = subject.execute expect(result).to be_error expect(project).not_to be_repository_read_only expect(project.repository_storage).to eq('default') + expect(repository_storage_move).to be_failed + end + end + + context "when the cleanup of the #{repository_type} repository fails" do + it 'sets the correct state' do + allow(Gitlab::GitalyClient).to receive(:filesystem_id).with('default').and_call_original + allow(Gitlab::GitalyClient).to receive(:filesystem_id).with('test_second_storage').and_return(SecureRandom.uuid) + allow(project_repository_double).to receive(:replicate) + .with(project.repository.raw) + allow(project_repository_double).to receive(:checksum) + .and_return(project_repository_checksum) + allow(original_project_repository_double).to receive(:remove) + allow(repository_double).to receive(:replicate) + .with(repository.raw) + allow(repository_double).to receive(:checksum) + .and_return(repository_checksum) + + expect(original_repository_double).to receive(:remove) + .and_raise(Gitlab::Git::CommandError) + + result = subject.execute + + expect(result).to be_error + expect(repository_storage_move).to be_cleanup_failed end end @@ -134,8 +165,6 @@ RSpec.shared_examples 'moves repository to another storage' do |repository_type| allow(repository_double).to receive(:checksum) .and_return('not matching checksum') - expect(GitlabShellWorker).not_to receive(:perform_async) - result = subject.execute expect(result).to be_error diff --git a/spec/support/shared_examples/services/resource_events/change_milestone_service_shared_examples.rb b/spec/support/shared_examples/services/resource_events/change_milestone_service_shared_examples.rb index ef41c2fcc13..d70ed707822 100644 --- a/spec/support/shared_examples/services/resource_events/change_milestone_service_shared_examples.rb +++ b/spec/support/shared_examples/services/resource_events/change_milestone_service_shared_examples.rb @@ -1,49 +1,63 @@ # frozen_string_literal: true -RSpec.shared_examples 'a milestone events creator' do +RSpec.shared_examples 'timebox(milestone or iteration) resource events creator' do |timebox_event_class| let_it_be(:user) { create(:user) } - let(:created_at_time) { Time.utc(2019, 12, 30) } - let(:service) { described_class.new(resource, user, created_at: created_at_time, old_milestone: nil) } - - context 'when milestone is present' do - let_it_be(:milestone) { create(:milestone) } + context 'when milestone/iteration is added' do + let(:service) { described_class.new(resource, user, add_timebox_args) } before do - resource.milestone = milestone + set_timebox(timebox_event_class, timebox) end it 'creates the expected event record' do - expect { service.execute }.to change { ResourceMilestoneEvent.count }.by(1) + expect { service.execute }.to change { timebox_event_class.count }.by(1) - expect_event_record(ResourceMilestoneEvent.last, action: 'add', milestone: milestone, state: 'opened') + expect_event_record(timebox_event_class, timebox_event_class.last, action: 'add', state: 'opened', timebox: timebox) end end - context 'when milestones is not present' do + context 'when milestone/iteration is removed' do + let(:service) { described_class.new(resource, user, remove_timebox_args) } + before do - resource.milestone = nil + set_timebox(timebox_event_class, nil) end - let(:old_milestone) { create(:milestone, project: resource.project) } - let(:service) { described_class.new(resource, user, created_at: created_at_time, old_milestone: old_milestone) } - it 'creates the expected event records' do - expect { service.execute }.to change { ResourceMilestoneEvent.count }.by(1) + expect { service.execute }.to change { timebox_event_class.count }.by(1) - expect_event_record(ResourceMilestoneEvent.last, action: 'remove', milestone: old_milestone, state: 'opened') + expect_event_record(timebox_event_class, timebox_event_class.last, action: 'remove', timebox: timebox, state: 'opened') end end - def expect_event_record(event, expected_attrs) + def expect_event_record(timebox_event_class, event, expected_attrs) expect(event.action).to eq(expected_attrs[:action]) - expect(event.state).to eq(expected_attrs[:state]) expect(event.user).to eq(user) expect(event.issue).to eq(resource) if resource.is_a?(Issue) expect(event.issue).to be_nil unless resource.is_a?(Issue) expect(event.merge_request).to eq(resource) if resource.is_a?(MergeRequest) expect(event.merge_request).to be_nil unless resource.is_a?(MergeRequest) - expect(event.milestone).to eq(expected_attrs[:milestone]) expect(event.created_at).to eq(created_at_time) + expect_timebox(timebox_event_class, event, expected_attrs) + end + + def set_timebox(timebox_event_class, timebox) + case timebox_event_class.name + when 'ResourceMilestoneEvent' + resource.milestone = timebox + when 'ResourceIterationEvent' + resource.iteration = timebox + end + end + + def expect_timebox(timebox_event_class, event, expected_attrs) + case timebox_event_class.name + when 'ResourceMilestoneEvent' + expect(event.state).to eq(expected_attrs[:state]) + expect(event.milestone).to eq(expected_attrs[:timebox]) + when 'ResourceIterationEvent' + expect(event.iteration).to eq(expected_attrs[:timebox]) + end end end diff --git a/spec/support/shared_examples/services/wiki_pages/create_service_shared_examples.rb b/spec/support/shared_examples/services/wiki_pages/create_service_shared_examples.rb index ebe78c299a5..980a752cf86 100644 --- a/spec/support/shared_examples/services/wiki_pages/create_service_shared_examples.rb +++ b/spec/support/shared_examples/services/wiki_pages/create_service_shared_examples.rb @@ -16,8 +16,10 @@ RSpec.shared_examples 'WikiPages::CreateService#execute' do |container_type| subject(:service) { described_class.new(container: container, current_user: user, params: opts) } it 'creates wiki page with valid attributes' do - page = service.execute + response = service.execute + page = response.payload[:page] + expect(response).to be_success expect(page).to be_valid expect(page).to be_persisted expect(page.title).to eq(opts[:title]) @@ -77,7 +79,12 @@ RSpec.shared_examples 'WikiPages::CreateService#execute' do |container_type| end it 'reports the error' do - expect(service.execute).to be_invalid + response = service.execute + page = response.payload[:page] + + expect(response).to be_error + + expect(page).to be_invalid .and have_attributes(errors: be_present) end end diff --git a/spec/support/shared_examples/services/wikis/create_attachment_service_shared_examples.rb b/spec/support/shared_examples/services/wikis/create_attachment_service_shared_examples.rb index 541e332e3a1..555a6d5eed0 100644 --- a/spec/support/shared_examples/services/wikis/create_attachment_service_shared_examples.rb +++ b/spec/support/shared_examples/services/wikis/create_attachment_service_shared_examples.rb @@ -14,6 +14,7 @@ RSpec.shared_examples 'Wikis::CreateAttachmentService#execute' do |container_typ file_content: 'Content of attachment' } end + let(:opts) { file_opts } let(:service) { Wikis::CreateAttachmentService.new(container: container, current_user: user, params: opts) } diff --git a/spec/support/shared_examples/snippet_blob_shared_examples.rb b/spec/support/shared_examples/snippet_blob_shared_examples.rb index ba97688d017..3ed777ee4b8 100644 --- a/spec/support/shared_examples/snippet_blob_shared_examples.rb +++ b/spec/support/shared_examples/snippet_blob_shared_examples.rb @@ -22,3 +22,24 @@ RSpec.shared_examples 'snippet blob raw path' do end end end + +RSpec.shared_examples 'snippet blob raw url' do + let(:blob) { snippet.blobs.first } + let(:ref) { blob.repository.root_ref } + + context 'for PersonalSnippets' do + let(:snippet) { personal_snippet } + + it 'returns the raw personal snippet blob url' do + expect(subject).to eq("http://test.host/-/snippets/#{snippet.id}/raw/#{ref}/#{blob.path}") + end + end + + context 'for ProjectSnippets' do + let(:snippet) { project_snippet } + + it 'returns the raw project snippet blob url' do + expect(subject).to eq("http://test.host/#{snippet.project.full_path}/-/snippets/#{snippet.id}/raw/#{ref}/#{blob.path}") + end + end +end -- cgit v1.2.3