summaryrefslogtreecommitdiff
path: root/spec/support
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-08-20 18:42:06 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2020-08-20 18:42:06 +0000
commit6e4e1050d9dba2b7b2523fdd1768823ab85feef4 (patch)
tree78be5963ec075d80116a932011d695dd33910b4e /spec/support
parent1ce776de4ae122aba3f349c02c17cebeaa8ecf07 (diff)
downloadgitlab-ce-6e4e1050d9dba2b7b2523fdd1768823ab85feef4.tar.gz
Add latest changes from gitlab-org/gitlab@13-3-stable-ee
Diffstat (limited to 'spec/support')
-rw-r--r--spec/support/counter_attribute.rb14
-rw-r--r--spec/support/csv_response.rb5
-rw-r--r--spec/support/gitlab_stubs/gitlab_ci_for_sast.yml13
-rw-r--r--spec/support/helpers/bare_repo_operations.rb2
-rw-r--r--spec/support/helpers/cycle_analytics_helpers.rb2
-rw-r--r--spec/support/helpers/design_management_test_helpers.rb4
-rw-r--r--spec/support/helpers/filtered_search_helpers.rb2
-rw-r--r--spec/support/helpers/http_basic_auth_helpers.rb17
-rw-r--r--spec/support/helpers/jira_service_helper.rb2
-rw-r--r--spec/support/helpers/login_helpers.rb2
-rw-r--r--spec/support/helpers/memory_usage_helper.rb2
-rw-r--r--spec/support/helpers/metrics_dashboard_helpers.rb18
-rw-r--r--spec/support/helpers/metrics_dashboard_url_helpers.rb10
-rw-r--r--spec/support/helpers/navbar_structure_helper.rb18
-rw-r--r--spec/support/helpers/notification_helpers.rb4
-rw-r--r--spec/support/helpers/packages_manager_api_spec_helper.rb12
-rw-r--r--spec/support/helpers/require_migration.rb31
-rw-r--r--spec/support/helpers/stub_configuration.rb4
-rw-r--r--spec/support/helpers/stub_feature_flags.rb31
-rw-r--r--spec/support/helpers/stub_object_storage.rb13
-rw-r--r--spec/support/helpers/stubbed_feature.rb49
-rw-r--r--spec/support/helpers/test_env.rb6
-rw-r--r--spec/support/helpers/trigger_helpers.rb2
-rw-r--r--spec/support/helpers/usage_data_helpers.rb1
-rw-r--r--spec/support/helpers/wait_for_requests.rb6
-rw-r--r--spec/support/matchers/exceed_query_limit.rb4
-rw-r--r--spec/support/migrations_helpers/track_untracked_uploads_helpers.rb130
-rw-r--r--spec/support/protected_branch_helpers.rb5
-rw-r--r--spec/support/shared_contexts/change_access_checks_shared_context.rb2
-rw-r--r--spec/support/shared_contexts/csv_response_shared_context.rb5
-rw-r--r--spec/support/shared_contexts/finders/merge_requests_finder_shared_contexts.rb6
-rw-r--r--spec/support/shared_contexts/lib/gitlab/git_access_shared_examples.rb17
-rw-r--r--spec/support/shared_contexts/navbar_structure_context.rb3
-rw-r--r--spec/support/shared_contexts/policies/group_policy_shared_context.rb3
-rw-r--r--spec/support/shared_contexts/prometheus/alert_shared_context.rb2
-rw-r--r--spec/support/shared_contexts/read_ci_configuration_shared_context.rb9
-rw-r--r--spec/support/shared_contexts/requests/api/graphql/jira_import/jira_projects_context.rb2
-rw-r--r--spec/support/shared_contexts/services/projects/container_repository/delete_tags_service_shared_context.rb78
-rw-r--r--spec/support/shared_examples/alert_notification_service_shared_examples.rb29
-rw-r--r--spec/support/shared_examples/controllers/binary_blob_shared_examples.rb34
-rw-r--r--spec/support/shared_examples/controllers/concerns/graceful_timeout_handling_shared_examples.rb7
-rw-r--r--spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb83
-rw-r--r--spec/support/shared_examples/controllers/metrics/dashboard/prometheus_api_proxy_shared_examples.rb1
-rw-r--r--spec/support/shared_examples/controllers/variables_shared_examples.rb1
-rw-r--r--spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb54
-rw-r--r--spec/support/shared_examples/create_alert_issue_shared_examples.rb27
-rw-r--r--spec/support/shared_examples/features/master_manages_access_requests_shared_example.rb21
-rw-r--r--spec/support/shared_examples/features/packages_shared_examples.rb113
-rw-r--r--spec/support/shared_examples/features/protected_branches_access_control_ce_shared_examples.rb8
-rw-r--r--spec/support/shared_examples/features/rss_shared_examples.rb6
-rw-r--r--spec/support/shared_examples/features/snippets_shared_examples.rb222
-rw-r--r--spec/support/shared_examples/finders/snippet_visibility_shared_examples.rb386
-rw-r--r--spec/support/shared_examples/graphql/design_fields_shared_examples.rb1
-rw-r--r--spec/support/shared_examples/graphql/mutations/resolves_subscription_shared_examples.rb45
-rw-r--r--spec/support/shared_examples/graphql/mutations/set_assignees_shared_examples.rb126
-rw-r--r--spec/support/shared_examples/graphql/notes_on_noteables_shared_examples.rb1
-rw-r--r--spec/support/shared_examples/graphql/projects/merge_request_n_plus_one_query_examples.rb11
-rw-r--r--spec/support/shared_examples/lib/api/ci/runner_shared_examples.rb22
-rw-r--r--spec/support/shared_examples/lib/gitlab/background_migration/mentions_migration_shared_examples.rb22
-rw-r--r--spec/support/shared_examples/lib/gitlab/kubernetes/network_policy_common_shared_examples.rb160
-rw-r--r--spec/support/shared_examples/lib/gitlab/template/template_shared_examples.rb49
-rw-r--r--spec/support/shared_examples/models/chat_service_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/models/cluster_application_helm_cert_shared_examples.rb38
-rw-r--r--spec/support/shared_examples/models/cluster_application_initial_status_shared_examples.rb42
-rw-r--r--spec/support/shared_examples/models/cluster_application_status_shared_examples.rb152
-rw-r--r--spec/support/shared_examples/models/concerns/counter_attribute_shared_examples.rb176
-rw-r--r--spec/support/shared_examples/models/concerns/file_store_mounter_shared_examples.rb17
-rw-r--r--spec/support/shared_examples/models/concerns/timebox_shared_examples.rb41
-rw-r--r--spec/support/shared_examples/models/issuable_hook_data_shared_examples.rb1
-rw-r--r--spec/support/shared_examples/models/relative_positioning_shared_examples.rb602
-rw-r--r--spec/support/shared_examples/models/resource_event_shared_examples.rb (renamed from spec/support/shared_examples/resource_events.rb)0
-rw-r--r--spec/support/shared_examples/models/resource_timebox_event_shared_examples.rb75
-rw-r--r--spec/support/shared_examples/models/update_project_statistics_shared_examples.rb24
-rw-r--r--spec/support/shared_examples/path_extraction_shared_examples.rb68
-rw-r--r--spec/support/shared_examples/policies/project_policy_shared_examples.rb15
-rw-r--r--spec/support/shared_examples/requests/api/composer_packages_shared_examples.rb14
-rw-r--r--spec/support/shared_examples/requests/api/graphql/mutations/subscription_shared_examples.rb69
-rw-r--r--spec/support/shared_examples/requests/api/milestones_shared_examples.rb1
-rw-r--r--spec/support/shared_examples/requests/api/notes_shared_examples.rb61
-rw-r--r--spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb5
-rw-r--r--spec/support/shared_examples/requests/api/packages_shared_examples.rb8
-rw-r--r--spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb17
-rw-r--r--spec/support/shared_examples/requests/snippet_shared_examples.rb77
-rw-r--r--spec/support/shared_examples/services/boards/issues_list_service_shared_examples.rb22
-rw-r--r--spec/support/shared_examples/services/boards/lists_list_service_shared_examples.rb18
-rw-r--r--spec/support/shared_examples/services/jira_import/user_mapper_services_shared_examples.rb39
-rw-r--r--spec/support/shared_examples/services/metrics/dashboard_shared_examples.rb8
-rw-r--r--spec/support/shared_examples/services/projects/update_repository_storage_service_shared_examples.rb41
-rw-r--r--spec/support/shared_examples/services/resource_events/change_milestone_service_shared_examples.rb52
-rw-r--r--spec/support/shared_examples/services/wiki_pages/create_service_shared_examples.rb11
-rw-r--r--spec/support/shared_examples/services/wikis/create_attachment_service_shared_examples.rb1
-rw-r--r--spec/support/shared_examples/snippet_blob_shared_examples.rb21
92 files changed, 2855 insertions, 828 deletions
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<string, string>] - 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 = /.+\/(?<file_name>.+)_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: '<img src=x onerror=prompt(1)>')
+ timebox = build(timebox_type, *timebox_args, project: project, title: '<img src=x onerror=prompt(1)>')
expect(timebox).not_to be_valid
expect(timebox.errors[:title]).to include("can't be blank")
@@ -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: "<b>foo & bar -> 2.2</b>") }
+ let(:timebox) { create(timebox_type, *timebox_args, title: "<b>foo & bar -> 2.2</b>") }
it 'normalizes the title for use as a slug' do
expect(timebox.safe_title).to eq('foo-bar-22')
@@ -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: "<b>foo & bar -> 2.2</b>") }
+ let(:timebox) { create(timebox_type, *timebox_args, title: "<b>foo & bar -> 2.2</b>") }
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/resource_events.rb b/spec/support/shared_examples/models/resource_event_shared_examples.rb
index c0158f9b24b..c0158f9b24b 100644
--- a/spec/support/shared_examples/resource_events.rb
+++ b/spec/support/shared_examples/models/resource_event_shared_examples.rb
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/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