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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'spec/support')
-rw-r--r--spec/support/database/auto_explain.rb135
-rw-r--r--spec/support/database/prevent_cross_database_modification.rb2
-rw-r--r--spec/support/database/query_recorder.rb17
-rw-r--r--spec/support/fast_quarantine.rb7
-rw-r--r--spec/support/formatters/json_formatter.rb3
-rw-r--r--spec/support/helpers/content_editor_helpers.rb4
-rw-r--r--spec/support/helpers/features/dom_helpers.rb13
-rw-r--r--spec/support/helpers/features/runners_helpers.rb13
-rw-r--r--spec/support/helpers/filter_spec_helper.rb4
-rw-r--r--spec/support/helpers/graphql_helpers.rb4
-rw-r--r--spec/support/helpers/kubernetes_helpers.rb4
-rw-r--r--spec/support/helpers/metrics_dashboard_helpers.rb8
-rw-r--r--spec/support/helpers/migrations_helpers/project_statistics_helper.rb37
-rw-r--r--spec/support/helpers/models/ci/partitioning_testing/rspec_hooks.rb12
-rw-r--r--spec/support/helpers/models/ci/partitioning_testing/schema_helpers.rb35
-rw-r--r--spec/support/helpers/prometheus_helpers.rb22
-rw-r--r--spec/support/helpers/stub_gitlab_calls.rb4
-rw-r--r--spec/support/matchers/exceed_query_limit.rb19
-rw-r--r--spec/support/protected_branch_helpers.rb4
-rw-r--r--spec/support/rspec_order_todo.yml14
-rw-r--r--spec/support/shared_contexts/features/integrations/integrations_shared_context.rb2
-rw-r--r--spec/support/shared_contexts/glfm/api_markdown_snapshot_shared_context.rb1
-rw-r--r--spec/support/shared_contexts/graphql/types/query_type_shared_context.rb4
-rw-r--r--spec/support/shared_contexts/lib/gitlab/background_migration/backfill_project_statistics.rb106
-rw-r--r--spec/support/shared_contexts/lib/gitlab/sidekiq_middleware/server_metrics_shared_context.rb10
-rw-r--r--spec/support/shared_contexts/lib/sbom/package_url_shared_contexts.rb6
-rw-r--r--spec/support/shared_contexts/navbar_structure_context.rb1
-rw-r--r--spec/support/shared_contexts/policies/group_policy_shared_context.rb3
-rw-r--r--spec/support/shared_contexts/services/packages/rubygems/invalid_metadata.rb9
-rw-r--r--spec/support/shared_contexts/user_contribution_events_shared_context.rb52
-rw-r--r--spec/support/shared_examples/bulk_imports/visibility_level_examples.rb147
-rw-r--r--spec/support/shared_examples/channels/noteable/notes_channel_shared_examples.rb30
-rw-r--r--spec/support/shared_examples/ci/deployable_policy_shared_examples.rb25
-rw-r--r--spec/support/shared_examples/ci/deployable_policy_shared_examples_ee.rb57
-rw-r--r--spec/support/shared_examples/ci/deployable_shared_examples.rb582
-rw-r--r--spec/support/shared_examples/ci/deployable_shared_examples_ee.rb34
-rw-r--r--spec/support/shared_examples/ci/pipeline_schedules_create_or_update_shared_examples.rb121
-rw-r--r--spec/support/shared_examples/ci/stage_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/ci/waiting_for_approval_status_shared_examples.rb79
-rw-r--r--spec/support/shared_examples/controllers/internal_event_tracking_examples.rb12
-rw-r--r--spec/support/shared_examples/controllers/known_sign_in_shared_examples.rb18
-rw-r--r--spec/support/shared_examples/controllers/unique_visits_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/database_health_status_indicators/prometheus_alert_based_shared_examples.rb133
-rw-r--r--spec/support/shared_examples/deployments/create_for_job_shared_examples.rb153
-rw-r--r--spec/support/shared_examples/environments/create_for_job_shared_examples.rb299
-rw-r--r--spec/support/shared_examples/features/access_tokens_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/features/content_editor_shared_examples.rb13
-rw-r--r--spec/support/shared_examples/features/deploy_token_shared_examples.rb8
-rw-r--r--spec/support/shared_examples/features/discussion_comments_shared_example.rb2
-rw-r--r--spec/support/shared_examples/features/editable_merge_request_shared_examples.rb5
-rw-r--r--spec/support/shared_examples/features/manage_applications_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/features/protected_branches_access_control_ce_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/features/protected_branches_with_deploy_keys_examples.rb3
-rw-r--r--spec/support/shared_examples/features/protected_tags_with_deploy_keys_examples.rb3
-rw-r--r--spec/support/shared_examples/features/runners_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/features/sidebar_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/features/variable_list_drawer_shared_examples.rb23
-rw-r--r--spec/support/shared_examples/features/work_items_shared_examples.rb12
-rw-r--r--spec/support/shared_examples/finders/issues_finder_shared_examples.rb8
-rw-r--r--spec/support/shared_examples/graphql/mutations/update_time_estimate_shared_examples.rb59
-rw-r--r--spec/support/shared_examples/graphql/notes_quick_actions_for_work_items_shared_examples.rb100
-rw-r--r--spec/support/shared_examples/helpers/runners_shared_examples.rb12
-rw-r--r--spec/support/shared_examples/helpers/super_sidebar_shared_examples.rb37
-rw-r--r--spec/support/shared_examples/lib/gitlab/cache/json_cache_shared_examples.rb46
-rw-r--r--spec/support/shared_examples/lib/gitlab/search_archived_filter_shared_examples.rb33
-rw-r--r--spec/support/shared_examples/metrics/active_record_subscriber_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/migrations/add_work_item_widget_shared_examples.rb5
-rw-r--r--spec/support/shared_examples/models/concerns/auto_disabling_hooks_shared_examples.rb130
-rw-r--r--spec/support/shared_examples/models/concerns/has_repository_shared_examples.rb14
-rw-r--r--spec/support/shared_examples/models/concerns/integrations/slack_mattermost_notifier_shared_examples.rb10
-rw-r--r--spec/support/shared_examples/models/concerns/linkable_items_shared_examples.rb82
-rw-r--r--spec/support/shared_examples/models/concerns/unstoppable_hooks_shared_examples.rb30
-rw-r--r--spec/support/shared_examples/models/issuable_link_shared_examples.rb7
-rw-r--r--spec/support/shared_examples/protected_tags/access_control_ce_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/quick_actions/issuable/time_tracking_quick_action_shared_examples.rb9
-rw-r--r--spec/support/shared_examples/quick_actions/merge_request/merge_quick_action_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/quick_actions/work_item/type_change_quick_actions_shared_examples.rb10
-rw-r--r--spec/support/shared_examples/requests/api/draft_notes_shared_examples.rb91
-rw-r--r--spec/support/shared_examples/requests/api/graphql/packages/group_and_project_packages_list_shared_examples.rb14
-rw-r--r--spec/support/shared_examples/requests/api/graphql/remote_development_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/requests/api/hooks_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/requests/api/nuget_endpoints_shared_examples.rb14
-rw-r--r--spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb120
-rw-r--r--spec/support/shared_examples/requests/api/time_tracking_shared_examples.rb43
-rw-r--r--spec/support/shared_examples/requests/graphql_shared_examples.rb28
-rw-r--r--spec/support/shared_examples/services/container_registry_auth_service_shared_examples.rb28
-rw-r--r--spec/support/shared_examples/services/import_csv_service_shared_examples.rb12
-rw-r--r--spec/support/shared_examples/services/issuable/issuable_update_service_shared_examples.rb81
-rw-r--r--spec/support/shared_examples/services/issuable_links/create_links_shared_examples.rb80
-rw-r--r--spec/support/shared_examples/services/metrics/dashboard_shared_examples.rb21
-rw-r--r--spec/support/shared_examples/services/namespace_package_settings_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/services/notification_service_shared_examples.rb10
-rw-r--r--spec/support/shared_examples/services/security/ci_configuration/create_service_shared_examples.rb30
-rw-r--r--spec/support/shared_examples/usage_data_counters/work_item_activity_unique_counter_shared_examples.rb34
-rw-r--r--spec/support/shared_examples/work_item_hierarchy_restrictions_importer.rb4
95 files changed, 2995 insertions, 553 deletions
diff --git a/spec/support/database/auto_explain.rb b/spec/support/database/auto_explain.rb
new file mode 100644
index 00000000000..108d88e37b9
--- /dev/null
+++ b/spec/support/database/auto_explain.rb
@@ -0,0 +1,135 @@
+# frozen_string_literal: true
+
+module AutoExplain
+ class << self
+ def setup
+ Gitlab::Database::EachDatabase.each_connection do |connection|
+ next unless record_auto_explain?(connection)
+
+ connection.execute("LOAD 'auto_explain'")
+
+ # This param can only be set on pg14+ so we can't set it when starting postgres.
+ connection.execute('ALTER SYSTEM SET compute_query_id TO on')
+ connection.execute('SELECT pg_reload_conf()')
+ end
+ end
+
+ def record
+ Gitlab::Database::EachDatabase.each_connection do |connection, connection_name|
+ next unless record_auto_explain?(connection)
+
+ connection.execute(<<~SQL.squish)
+ CREATE EXTENSION IF NOT EXISTS file_fdw;
+ CREATE SERVER IF NOT EXISTS pglog FOREIGN DATA WRAPPER file_fdw;
+ SQL
+
+ csvlog_columns = [
+ 'log_time timestamp(3) with time zone',
+ 'user_name text',
+ 'database_name text',
+ 'process_id integer',
+ 'connection_from text',
+ 'session_id text',
+ 'session_line_num bigint',
+ 'command_tag text',
+ 'session_start_time timestamp with time zone',
+ 'virtual_transaction_id text',
+ 'transaction_id bigint',
+ 'error_severity text',
+ 'sql_state_code text',
+ 'message text',
+ 'detail text',
+ 'hint text',
+ 'internal_query text',
+ 'internal_query_pos integer',
+ 'context text',
+ 'query text',
+ 'query_pos integer',
+ 'location text',
+ 'application_name text',
+ 'backend_type text',
+ 'leader_pid integer',
+ 'query_id bigint'
+ ]
+
+ connection.transaction do
+ connection.execute(<<~SQL.squish)
+ CREATE FOREIGN TABLE IF NOT EXISTS pglog (#{csvlog_columns.join(', ')})
+ SERVER pglog
+ OPTIONS ( filename 'log/pglog.csv', format 'csv' );
+ SQL
+
+ log_file = Rails.root.join(
+ File.dirname(ENV.fetch('RSPEC_AUTO_EXPLAIN_LOG_PATH', 'auto_explain/auto_explain.ndjson.gz')),
+ "#{ENV.fetch('CI_JOB_NAME_SLUG', 'rspec')}.#{Process.pid}.#{connection_name}.ndjson.gz"
+ )
+
+ FileUtils.mkdir_p(File.dirname(log_file))
+
+ fingerprints = Set.new
+ recording_start = Time.now
+
+ Zlib::GzipWriter.open(log_file) do |gz|
+ pg = connection.raw_connection
+
+ pg.exec('SET statement_timeout TO 0;')
+
+ pg.send_query(<<~SQL.squish)
+ SELECT DISTINCT ON (m.query_id)
+ m.message->>'Query Text' as query, m.message->'Plan' as plan
+ FROM (
+ SELECT substring(message from '\{.*$')::jsonb AS message, query_id
+ FROM pglog
+ WHERE message LIKE '%{%'
+ ) m
+ ORDER BY m.query_id;
+ SQL
+
+ pg.set_single_row_mode
+ pg.get_result.stream_each do |row|
+ query = row['query']
+ fingerprint = PgQuery.fingerprint(query)
+ next unless fingerprints.add?(fingerprint)
+
+ plan = Gitlab::Json.parse(row['plan'])
+
+ output = {
+ query: query,
+ plan: plan,
+ fingerprint: fingerprint,
+ normalized: PgQuery.normalize(query)
+ }
+
+ gz.puts Gitlab::Json.generate(output)
+ end
+
+ puts "auto_explain log contains #{fingerprints.size} entries for #{connection_name}, writing to #{log_file}"
+ puts "took #{Time.now - recording_start}"
+ end
+
+ raise ActiveRecord::Rollback
+ end
+ end
+ end
+
+ private
+
+ def record_auto_explain?(connection)
+ ENV['CI'] \
+ && ENV['CI_MERGE_REQUEST_LABELS']&.include?('pipeline:record-queries') \
+ && ENV['CI_JOB_NAME_SLUG'] != 'db-migrate-non-superuser' \
+ && connection.database_version.to_s[0..1].to_i >= 14 \
+ && connection.select_one('SHOW is_superuser')['is_superuser'] == 'on'
+ end
+ end
+end
+
+RSpec.configure do |config|
+ config.before(:suite) do
+ AutoExplain.setup
+ end
+
+ config.after(:suite) do
+ AutoExplain.record
+ end
+end
diff --git a/spec/support/database/prevent_cross_database_modification.rb b/spec/support/database/prevent_cross_database_modification.rb
index cd0cbe733d1..77fa7feacd4 100644
--- a/spec/support/database/prevent_cross_database_modification.rb
+++ b/spec/support/database/prevent_cross_database_modification.rb
@@ -25,7 +25,7 @@ RSpec.configure do |config|
end
# Reset after execution to preferred state
- config.after do |example_file|
+ config.after do |_example_file|
::Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModification.suppress_in_rspec = true
::ApplicationRecord.gitlab_transactions_stack.clear
diff --git a/spec/support/database/query_recorder.rb b/spec/support/database/query_recorder.rb
deleted file mode 100644
index c0736221af3..00000000000
--- a/spec/support/database/query_recorder.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-# frozen_string_literal: true
-
-RSpec.configure do |config|
- # Truncate the query_recorder log file before starting the suite
- config.before(:suite) do
- log_file = Rails.root.join(Gitlab::Database::QueryAnalyzers::QueryRecorder.log_file)
- File.write(log_file, '') if File.exist?(log_file)
- File.delete("#{log_file}.gz") if File.exist?("#{log_file}.gz")
- end
-
- config.after(:suite) do
- if ENV['CI']
- log_file = Rails.root.join(Gitlab::Database::QueryAnalyzers::QueryRecorder.log_file)
- system("gzip #{log_file}") if File.exist?(log_file)
- end
- end
-end
diff --git a/spec/support/fast_quarantine.rb b/spec/support/fast_quarantine.rb
index b5ed1a2aa96..9732a287cb2 100644
--- a/spec/support/fast_quarantine.rb
+++ b/spec/support/fast_quarantine.rb
@@ -7,10 +7,9 @@ return if ENV['CI_MERGE_REQUEST_LABELS'].to_s.include?('pipeline:run-flaky-tests
require_relative '../../tooling/lib/tooling/fast_quarantine'
RSpec.configure do |config|
- fast_quarantine_local_path = ENV.fetch('RSPEC_FAST_QUARANTINE_LOCAL_PATH', 'rspec/fast_quarantine-gitlab.txt')
fast_quarantine_path = ENV.fetch(
'RSPEC_FAST_QUARANTINE_PATH',
- File.expand_path("../../#{fast_quarantine_local_path}", __dir__)
+ File.expand_path("../../rspec/fast_quarantine-gitlab.txt", __dir__)
)
fast_quarantine = Tooling::FastQuarantine.new(fast_quarantine_path: fast_quarantine_path)
skipped_examples = []
@@ -28,10 +27,12 @@ RSpec.configure do |config|
next if skipped_examples.empty?
skipped_tests_report_path = ENV.fetch(
- 'SKIPPED_TESTS_REPORT_PATH',
+ 'RSPEC_SKIPPED_TESTS_REPORT_PATH',
File.expand_path("../../rspec/flaky/skipped_tests.txt", __dir__)
)
+ next warn("#{skipped_tests_report_path} doesn't exist!") unless File.exist?(skipped_tests_report_path.to_s)
+
File.write(skipped_tests_report_path, "#{ENV.fetch('CI_JOB_URL', 'local-run')}\n#{skipped_examples.join("\n")}\n\n")
end
end
diff --git a/spec/support/formatters/json_formatter.rb b/spec/support/formatters/json_formatter.rb
index 1fb0c7c91ec..a54004b3024 100644
--- a/spec/support/formatters/json_formatter.rb
+++ b/spec/support/formatters/json_formatter.rb
@@ -79,7 +79,8 @@ module Support
feature_category: example.metadata[:feature_category],
ci_job_url: ENV['CI_JOB_URL'],
retry_attempts: example.metadata[:retry_attempts],
- level: example.metadata[:level]
+ level: example.metadata[:level],
+ allowed_to_be_slow: example.metadata[:allowed_to_be_slow]
}
end
diff --git a/spec/support/helpers/content_editor_helpers.rb b/spec/support/helpers/content_editor_helpers.rb
index a6cc2560d0b..7597a13e475 100644
--- a/spec/support/helpers/content_editor_helpers.rb
+++ b/spec/support/helpers/content_editor_helpers.rb
@@ -9,6 +9,10 @@ module ContentEditorHelpers
end
end
+ def switch_to_markdown_editor
+ click_button("Switch to plain text editing")
+ end
+
def switch_to_content_editor
click_button("Switch to rich text editing")
end
diff --git a/spec/support/helpers/features/dom_helpers.rb b/spec/support/helpers/features/dom_helpers.rb
new file mode 100644
index 00000000000..ac6523f3360
--- /dev/null
+++ b/spec/support/helpers/features/dom_helpers.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Features
+ module DomHelpers
+ def find_by_testid(testid)
+ page.find("[data-testid='#{testid}']")
+ end
+
+ def within_testid(testid, &block)
+ page.within("[data-testid='#{testid}']", &block)
+ end
+ end
+end
diff --git a/spec/support/helpers/features/runners_helpers.rb b/spec/support/helpers/features/runners_helpers.rb
index 0504e883b82..dbd1edade8c 100644
--- a/spec/support/helpers/features/runners_helpers.rb
+++ b/spec/support/helpers/features/runners_helpers.rb
@@ -23,11 +23,11 @@ module Features
def input_filtered_search_keys(search_term)
focus_filtered_search
- page.within(search_bar_selector) do
- page.find('input').send_keys(search_term)
- click_on 'Search'
- end
+ page.find(search_bar_selector).find('input').send_keys(search_term)
+ # blur input
+ find('body').click
+ page.click_on 'Search'
wait_for_requests
end
@@ -49,9 +49,8 @@ module Features
# For OPERATORS_IS, clicking the filter
# immediately preselects "=" operator
-
- page.find('input').send_keys(value)
- page.find('input').send_keys(:enter)
+ send_keys(value)
+ send_keys(:enter)
click_on 'Search'
end
diff --git a/spec/support/helpers/filter_spec_helper.rb b/spec/support/helpers/filter_spec_helper.rb
index 7beed9c7755..dc282bf0a68 100644
--- a/spec/support/helpers/filter_spec_helper.rb
+++ b/spec/support/helpers/filter_spec_helper.rb
@@ -94,9 +94,9 @@ module FilterSpecHelper
when /\A(.+)?[^\d]\d+\z/
# Integer-based reference with optional project prefix
reference.gsub(/\d+\z/) { |i| i.to_i + 10_000 }
- when /\A(.+@)?(\h{7,40}\z)/
+ when /\A(.+@)?(#{Gitlab::Git::Commit::RAW_SHA_PATTERN}\z)/o
# SHA-based reference with optional prefix
- reference.gsub(/\h{7,40}\z/) { |v| v.reverse }
+ reference.gsub(/#{Gitlab::Git::Commit::RAW_SHA_PATTERN}\z/o) { |v| v.reverse }
else
reference.gsub(/\w+\z/) { |v| v.reverse }
end
diff --git a/spec/support/helpers/graphql_helpers.rb b/spec/support/helpers/graphql_helpers.rb
index 62e05129fb2..19a637d4893 100644
--- a/spec/support/helpers/graphql_helpers.rb
+++ b/spec/support/helpers/graphql_helpers.rb
@@ -151,7 +151,7 @@ module GraphqlHelpers
raise UnauthorizedObject unless parent
# we enable the request store so we can track gitaly calls.
- ::Gitlab::WithRequestStore.with_request_store do
+ ::Gitlab::SafeRequestStore.ensure_request_store do
prepared_args = case arg_style
when :internal_prepared
args_internal_prepared(field, args: args, query_ctx: query_ctx, parent: parent, extras: extras, query: query)
@@ -267,7 +267,7 @@ module GraphqlHelpers
# authentication (token set-up, license checks)
# It clears the request store, rails cache, and BatchLoader Executor between runs.
def run_with_clean_state(query, **args)
- ::Gitlab::WithRequestStore.with_request_store do
+ ::Gitlab::SafeRequestStore.ensure_request_store do
with_clean_rails_cache do
with_clean_batchloader_executor do
::GitlabSchema.execute(query, **args)
diff --git a/spec/support/helpers/kubernetes_helpers.rb b/spec/support/helpers/kubernetes_helpers.rb
index c3076a2c359..92a49d6a196 100644
--- a/spec/support/helpers/kubernetes_helpers.rb
+++ b/spec/support/helpers/kubernetes_helpers.rb
@@ -676,7 +676,6 @@ module KubernetesHelpers
}
end
- # noinspection RubyStringKeysInHashInspection
def knative_06_service(name: 'kubetest', namespace: 'default', domain: 'example.com', description: 'a knative service', environment: 'production', cluster_id: 9)
{ "apiVersion" => "serving.knative.dev/v1alpha1",
"kind" => "Service",
@@ -736,7 +735,6 @@ module KubernetesHelpers
"podcount" => 0 }
end
- # noinspection RubyStringKeysInHashInspection
def knative_07_service(name: 'kubetest', namespace: 'default', domain: 'example.com', description: 'a knative service', environment: 'production', cluster_id: 5)
{ "apiVersion" => "serving.knative.dev/v1alpha1",
"kind" => "Service",
@@ -788,7 +786,6 @@ module KubernetesHelpers
"podcount" => 0 }
end
- # noinspection RubyStringKeysInHashInspection
def knative_09_service(name: 'kubetest', namespace: 'default', domain: 'example.com', description: 'a knative service', environment: 'production', cluster_id: 5)
{ "apiVersion" => "serving.knative.dev/v1alpha1",
"kind" => "Service",
@@ -840,7 +837,6 @@ module KubernetesHelpers
"podcount" => 0 }
end
- # noinspection RubyStringKeysInHashInspection
def knative_05_service(name: 'kubetest', namespace: 'default', domain: 'example.com', description: 'a knative service', environment: 'production', cluster_id: 8)
{ "apiVersion" => "serving.knative.dev/v1alpha1",
"kind" => "Service",
diff --git a/spec/support/helpers/metrics_dashboard_helpers.rb b/spec/support/helpers/metrics_dashboard_helpers.rb
index 417baeda33a..1aae3964669 100644
--- a/spec/support/helpers/metrics_dashboard_helpers.rb
+++ b/spec/support/helpers/metrics_dashboard_helpers.rb
@@ -38,14 +38,6 @@ module MetricsDashboardHelpers
::Gitlab::Config::Loader::Yaml.new(data).load_raw!
end
- def system_dashboard_path
- Metrics::Dashboard::SystemDashboardService::DASHBOARD_PATH
- end
-
- def pod_dashboard_path
- Metrics::Dashboard::PodDashboardService::DASHBOARD_PATH
- end
-
def business_metric_title
Enums::PrometheusMetric.group_details[:business][:group_title]
end
diff --git a/spec/support/helpers/migrations_helpers/project_statistics_helper.rb b/spec/support/helpers/migrations_helpers/project_statistics_helper.rb
new file mode 100644
index 00000000000..4e7d83a38ac
--- /dev/null
+++ b/spec/support/helpers/migrations_helpers/project_statistics_helper.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+module MigrationHelpers
+ module ProjectStatisticsHelper
+ def generate_records(projects, table, values = {})
+ projects.map do |proj|
+ table.create!(
+ values.merge({
+ project_id: proj.id,
+ namespace_id: proj.namespace_id
+ })
+ )
+ end
+ end
+
+ def create_migration(end_id:)
+ described_class.new(start_id: 1, end_id: end_id,
+ batch_table: 'project_statistics', batch_column: 'project_id',
+ sub_batch_size: 1_000, pause_ms: 0,
+ connection: ApplicationRecord.connection)
+ end
+
+ def create_project_stats(project_table, namespace, default_stats, override_stats = {})
+ stats = default_stats.merge(override_stats)
+
+ group = namespace.create!(name: 'group_a', path: 'group-a', type: 'Group')
+ project_namespace = namespace.create!(name: 'project_a', path: 'project_a', type: 'Project', parent_id: group.id)
+ proj = project_table.create!(name: 'project_a', path: 'project-a', namespace_id: group.id,
+ project_namespace_id: project_namespace.id)
+ project_statistics_table.create!(
+ project_id: proj.id,
+ namespace_id: group.id,
+ **stats
+ )
+ end
+ end
+end
diff --git a/spec/support/helpers/models/ci/partitioning_testing/rspec_hooks.rb b/spec/support/helpers/models/ci/partitioning_testing/rspec_hooks.rb
index 3f0a2bb7f3b..a764e751bf5 100644
--- a/spec/support/helpers/models/ci/partitioning_testing/rspec_hooks.rb
+++ b/spec/support/helpers/models/ci/partitioning_testing/rspec_hooks.rb
@@ -1,23 +1,19 @@
# frozen_string_literal: true
RSpec.configure do |config|
- config.include Ci::PartitioningTesting::PartitionIdentifiers
+ config.include ::Ci::PartitioningTesting::PartitionIdentifiers
config.around(:each, :ci_partitionable) do |example|
- unless Ci::Build.table_name.to_s.starts_with?('p_')
- skip 'Skipping partitioning tests until `ci_builds` is partitioned'
- end
-
- Ci::PartitioningTesting::SchemaHelpers.with_routing_tables do
+ ::Ci::PartitioningTesting::SchemaHelpers.with_routing_tables do
example.run
end
end
config.before(:all) do
- Ci::PartitioningTesting::SchemaHelpers.setup
+ ::Ci::PartitioningTesting::SchemaHelpers.setup
end
config.after(:all) do
- Ci::PartitioningTesting::SchemaHelpers.teardown
+ ::Ci::PartitioningTesting::SchemaHelpers.teardown
end
end
diff --git a/spec/support/helpers/models/ci/partitioning_testing/schema_helpers.rb b/spec/support/helpers/models/ci/partitioning_testing/schema_helpers.rb
index 849d9ea117e..a47aaffdb43 100644
--- a/spec/support/helpers/models/ci/partitioning_testing/schema_helpers.rb
+++ b/spec/support/helpers/models/ci/partitioning_testing/schema_helpers.rb
@@ -3,36 +3,27 @@
module Ci
module PartitioningTesting
module SchemaHelpers
- DEFAULT_PARTITION = 100
-
module_function
def with_routing_tables
- # model.table_name = :routing_table
+ # previous_table_name = Model.table_name
+ # Model.table_name = routing_table_name
+
yield
# ensure
- # model.table_name = :regular_table
+ # Model.table_name = previous_table_name
end
- # We're dropping the default values here to ensure that the application code
- # populates the `partition_id` value and it's not falling back on the
- # database default one. We should be able to clean this up after
- # partitioning the tables and substituting the routing table in the model:
- # https://gitlab.com/gitlab-org/gitlab/-/issues/377822
- #
def setup(connection: Ci::ApplicationRecord.connection)
each_partitionable_table do |table_name|
- change_column_default(table_name, from: DEFAULT_PARTITION, to: nil, connection: connection)
- change_column_default("p_#{table_name}", from: DEFAULT_PARTITION, to: nil, connection: connection)
create_test_partition("p_#{table_name}", connection: connection)
end
+ ensure_builds_id_uniquness(connection: connection)
end
def teardown(connection: Ci::ApplicationRecord.connection)
each_partitionable_table do |table_name|
drop_test_partition("p_#{table_name}", connection: connection)
- change_column_default(table_name, from: nil, to: DEFAULT_PARTITION, connection: connection)
- change_column_default("p_#{table_name}", from: nil, to: DEFAULT_PARTITION, connection: connection)
end
end
@@ -47,12 +38,6 @@ module Ci
end
end
- def change_column_default(table_name, from:, to:, connection:)
- return unless table_available?(table_name, connection: connection)
-
- connection.change_column_default(table_name, :partition_id, from: from, to: to)
- end
-
def create_test_partition(table_name, connection:)
return unless table_available?(table_name, connection: connection)
@@ -75,6 +60,16 @@ module Ci
SQL
end
+ # This can be removed after https://gitlab.com/gitlab-org/gitlab/-/issues/421173
+ # is implemented
+ def ensure_builds_id_uniquness(connection:)
+ connection.execute(<<~SQL.squish)
+ CREATE TRIGGER assign_p_ci_builds_id_trigger
+ BEFORE INSERT ON #{full_partition_name('ci_builds')}
+ FOR EACH ROW EXECUTE FUNCTION assign_p_ci_builds_id_value();
+ SQL
+ end
+
def table_available?(table_name, connection:)
connection.table_exists?(table_name) &&
connection.column_exists?(table_name, :partition_id)
diff --git a/spec/support/helpers/prometheus_helpers.rb b/spec/support/helpers/prometheus_helpers.rb
index e1f5e6dee14..da80f6f08c2 100644
--- a/spec/support/helpers/prometheus_helpers.rb
+++ b/spec/support/helpers/prometheus_helpers.rb
@@ -240,12 +240,11 @@ module PrometheusHelpers
def prometheus_alert_payload(firing: [], resolved: [])
status = firing.any? ? 'firing' : 'resolved'
alerts = firing + resolved
- alert_name = alerts.first&.title || ''
- prometheus_metric_id = alerts.first&.prometheus_metric_id&.to_s
+ alert_name = alerts.first || ''
alerts_map = \
- firing.map { |alert| prometheus_map_alert_payload('firing', alert) } +
- resolved.map { |alert| prometheus_map_alert_payload('resolved', alert) }
+ firing.map { |title| prometheus_map_alert_payload('firing', title) } +
+ resolved.map { |title| prometheus_map_alert_payload('resolved', title) }
# See https://prometheus.io/docs/alerting/configuration/#%3Cwebhook_config%3E
{
@@ -257,9 +256,7 @@ module PrometheusHelpers
'alertname' => alert_name
},
'commonLabels' => {
- 'alertname' => alert_name,
- 'gitlab' => 'hook',
- 'gitlab_alert_id' => prometheus_metric_id
+ 'alertname' => alert_name
},
'commonAnnotations' => {},
'externalURL' => '',
@@ -267,22 +264,21 @@ module PrometheusHelpers
}
end
- def prometheus_alert_payload_fingerprint(prometheus_alert)
+ def prometheus_alert_payload_fingerprint(title)
# timestamp is hard-coded in #prometheus_map_alert_payload
- fingerprint = "#{prometheus_alert.prometheus_metric_id}/2018-09-24T08:57:31.095725221Z"
+ # sample fingerprint format comes from AlertManagement::Payload::Prometheus
+ fingerprint = ["2018-09-24T08:57:31.095725221Z", title].join('/')
Gitlab::AlertManagement::Fingerprint.generate(fingerprint)
end
private
- def prometheus_map_alert_payload(status, alert)
+ def prometheus_map_alert_payload(status, title)
{
'status' => status,
'labels' => {
- 'alertname' => alert.title,
- 'gitlab' => 'hook',
- 'gitlab_alert_id' => alert.prometheus_metric_id.to_s
+ 'alertname' => title
},
'annotations' => {},
'startsAt' => '2018-09-24T08:57:31.095725221Z',
diff --git a/spec/support/helpers/stub_gitlab_calls.rb b/spec/support/helpers/stub_gitlab_calls.rb
index 748ea525e40..6d0e97b0a75 100644
--- a/spec/support/helpers/stub_gitlab_calls.rb
+++ b/spec/support/helpers/stub_gitlab_calls.rb
@@ -23,6 +23,10 @@ module StubGitlabCalls
end
def stub_ci_pipeline_yaml_file(ci_yaml_content)
+ allow_any_instance_of(Gitlab::Ci::ProjectConfig::Repository)
+ .to receive(:file_in_repository?)
+ .and_return(ci_yaml_content.present?)
+
allow_any_instance_of(Repository)
.to receive(:gitlab_ci_yml_for)
.and_return(ci_yaml_content)
diff --git a/spec/support/matchers/exceed_query_limit.rb b/spec/support/matchers/exceed_query_limit.rb
index 29ebe5a3918..cc912d8de66 100644
--- a/spec/support/matchers/exceed_query_limit.rb
+++ b/spec/support/matchers/exceed_query_limit.rb
@@ -271,15 +271,11 @@ RSpec::Matchers.define :issue_fewer_queries_than do
end
end
-RSpec::Matchers.define :issue_same_number_of_queries_as do
+RSpec::Matchers.define :issue_same_number_of_queries_as do |expected|
supports_block_expectations
include ExceedQueryLimitHelpers
- def control
- block_arg
- end
-
chain :or_fewer do
@or_fewer = true
end
@@ -288,12 +284,15 @@ RSpec::Matchers.define :issue_same_number_of_queries_as do
@skip_cached = true
end
- def control_recorder
- @control_recorder ||= ActiveRecord::QueryRecorder.new(&control)
- end
-
def expected_count
- control_recorder.count
+ # Some tests pass a query recorder, others pass a block that executes an action.
+ # Maybe, we need to clear the block usage and only accept query recorders.
+
+ @expected_count ||= if expected.is_a?(ActiveRecord::QueryRecorder)
+ query_recorder_count(expected)
+ else
+ ActiveRecord::QueryRecorder.new(&block_arg).count
+ end
end
def verify_count(&block)
diff --git a/spec/support/protected_branch_helpers.rb b/spec/support/protected_branch_helpers.rb
index d983d03fd2e..576275e9d1d 100644
--- a/spec/support/protected_branch_helpers.rb
+++ b/spec/support/protected_branch_helpers.rb
@@ -9,6 +9,10 @@ module ProtectedBranchHelpers
end
end
+ def show_add_form
+ click_button 'Add protected branch'
+ end
+
def set_protected_branch_name(branch_name)
find('.js-protected-branch-select').click
find('.dropdown-input-field').set(branch_name)
diff --git a/spec/support/rspec_order_todo.yml b/spec/support/rspec_order_todo.yml
index 3cce22c00e6..f52f843e56a 100644
--- a/spec/support/rspec_order_todo.yml
+++ b/spec/support/rspec_order_todo.yml
@@ -4149,7 +4149,7 @@
- './spec/features/projects/settings/user_changes_default_branch_spec.rb'
- './spec/features/projects/settings/user_interacts_with_deploy_keys_spec.rb'
- './spec/features/projects/settings/user_manages_merge_requests_settings_spec.rb'
-- './spec/features/projects/settings/user_manages_project_members_spec.rb'
+- './spec/features/projects/members/user_manages_project_members_spec.rb'
- './spec/features/projects/settings/user_renames_a_project_spec.rb'
- './spec/features/projects/settings/user_searches_in_settings_spec.rb'
- './spec/features/projects/settings/user_sees_revoke_deploy_token_modal_spec.rb'
@@ -4368,7 +4368,6 @@
- './spec/finders/merge_requests_finder_spec.rb'
- './spec/finders/merge_requests/oldest_per_commit_finder_spec.rb'
- './spec/finders/merge_request_target_project_finder_spec.rb'
-- './spec/finders/metrics/dashboards/annotations_finder_spec.rb'
- './spec/finders/metrics/users_starred_dashboards_finder_spec.rb'
- './spec/finders/milestones_finder_spec.rb'
- './spec/finders/namespaces/projects_finder_spec.rb'
@@ -5026,7 +5025,6 @@
- './spec/helpers/enable_search_settings_helper_spec.rb'
- './spec/helpers/environment_helper_spec.rb'
- './spec/helpers/environments_helper_spec.rb'
-- './spec/helpers/events_helper_spec.rb'
- './spec/helpers/explore_helper_spec.rb'
- './spec/helpers/export_helper_spec.rb'
- './spec/helpers/external_link_helper_spec.rb'
@@ -5453,8 +5451,6 @@
- './spec/lib/container_registry/path_spec.rb'
- './spec/lib/container_registry/registry_spec.rb'
- './spec/lib/container_registry/tag_spec.rb'
-- './spec/lib/csv_builder_spec.rb'
-- './spec/lib/csv_builders/stream_spec.rb'
- './spec/lib/declarative_enum_spec.rb'
- './spec/lib/error_tracking/stacktrace_builder_spec.rb'
- './spec/lib/event_filter_spec.rb'
@@ -6808,7 +6804,6 @@
- './spec/lib/gitlab/net_http_adapter_spec.rb'
- './spec/lib/gitlab/no_cache_headers_spec.rb'
- './spec/lib/gitlab/noteable_metadata_spec.rb'
-- './spec/lib/gitlab/null_request_store_spec.rb'
- './spec/lib/gitlab/object_hierarchy_spec.rb'
- './spec/lib/gitlab/octokit/middleware_spec.rb'
- './spec/lib/gitlab/omniauth_initializer_spec.rb'
@@ -6933,7 +6928,6 @@
- './spec/lib/gitlab/saas_spec.rb'
- './spec/lib/gitlab/safe_request_loader_spec.rb'
- './spec/lib/gitlab/safe_request_purger_spec.rb'
-- './spec/lib/gitlab/safe_request_store_spec.rb'
- './spec/lib/gitlab/sample_data_template_spec.rb'
- './spec/lib/gitlab/sanitizers/exception_message_spec.rb'
- './spec/lib/gitlab/sanitizers/exif_spec.rb'
@@ -7141,9 +7135,6 @@
- './spec/lib/gitlab/usage/metrics/instrumentations/snowplow_enabled_metric_spec.rb'
- './spec/lib/gitlab/usage/metrics/instrumentations/uuid_metric_spec.rb'
- './spec/lib/gitlab/usage/metrics/key_path_processor_spec.rb'
-- './spec/lib/gitlab/usage/metrics/names_suggestions/generator_spec.rb'
-- './spec/lib/gitlab/usage/metrics/names_suggestions/relation_parsers/joins_spec.rb'
-- './spec/lib/gitlab/usage/metrics/name_suggestion_spec.rb'
- './spec/lib/gitlab/usage/metric_spec.rb'
- './spec/lib/gitlab/usage/metrics/query_spec.rb'
- './spec/lib/gitlab/usage/service_ping/instrumented_payload_spec.rb'
@@ -7173,7 +7164,6 @@
- './spec/lib/gitlab/webpack/manifest_spec.rb'
- './spec/lib/gitlab/wiki_file_finder_spec.rb'
- './spec/lib/gitlab/wiki_pages/front_matter_parser_spec.rb'
-- './spec/lib/gitlab/with_request_store_spec.rb'
- './spec/lib/gitlab/word_diff/chunk_collection_spec.rb'
- './spec/lib/gitlab/word_diff/line_processor_spec.rb'
- './spec/lib/gitlab/word_diff/parser_spec.rb'
@@ -8160,6 +8150,7 @@
- './spec/requests/admin/version_check_controller_spec.rb'
- './spec/requests/api/access_requests_spec.rb'
- './spec/requests/api/admin/batched_background_migrations_spec.rb'
+- './spec/requests/api/admin/broadcast_messages_spec.rb'
- './spec/requests/api/admin/ci/variables_spec.rb'
- './spec/requests/api/admin/instance_clusters_spec.rb'
- './spec/requests/api/admin/plan_limits_spec.rb'
@@ -8175,7 +8166,6 @@
- './spec/requests/api/badges_spec.rb'
- './spec/requests/api/boards_spec.rb'
- './spec/requests/api/branches_spec.rb'
-- './spec/requests/api/broadcast_messages_spec.rb'
- './spec/requests/api/bulk_imports_spec.rb'
- './spec/requests/api/ci/job_artifacts_spec.rb'
- './spec/requests/api/ci/jobs_spec.rb'
diff --git a/spec/support/shared_contexts/features/integrations/integrations_shared_context.rb b/spec/support/shared_contexts/features/integrations/integrations_shared_context.rb
index 8c17136b1e2..848e333d88b 100644
--- a/spec/support/shared_contexts/features/integrations/integrations_shared_context.rb
+++ b/spec/support/shared_contexts/features/integrations/integrations_shared_context.rb
@@ -92,6 +92,8 @@ RSpec.shared_context 'with integration' do
hash.merge!(k => File.read('spec/fixtures/service_account.json'))
elsif integration == 'google_play' && k == :service_account_key_file_name
hash.merge!(k => 'service_account.json')
+ elsif integration == 'google_play' && k == :google_play_protected_refs # rubocop:disable Lint/DuplicateBranch
+ hash.merge!(k => true)
else
hash.merge!(k => "someword")
end
diff --git a/spec/support/shared_contexts/glfm/api_markdown_snapshot_shared_context.rb b/spec/support/shared_contexts/glfm/api_markdown_snapshot_shared_context.rb
index 3623fa0850d..a0d91d813ae 100644
--- a/spec/support/shared_contexts/glfm/api_markdown_snapshot_shared_context.rb
+++ b/spec/support/shared_contexts/glfm/api_markdown_snapshot_shared_context.rb
@@ -29,7 +29,6 @@ RSpec.shared_context 'with API::Markdown Snapshot shared context' do |ee_only: f
let(:normalizations) { normalizations_by_example_name.dig(name, :html, :static, :snapshot) }
it "verifies conversion of GLFM to HTML", :unlimited_max_formatted_output_length do
- # noinspection RubyResolve
normalized_html = normalize_html(html, normalizations)
api_url = metadata_by_example_name&.dig(name, :api_request_override_path) || (api "/markdown")
diff --git a/spec/support/shared_contexts/graphql/types/query_type_shared_context.rb b/spec/support/shared_contexts/graphql/types/query_type_shared_context.rb
index 26f550b9b40..434592ccd38 100644
--- a/spec/support/shared_contexts/graphql/types/query_type_shared_context.rb
+++ b/spec/support/shared_contexts/graphql/types/query_type_shared_context.rb
@@ -41,7 +41,9 @@ RSpec.shared_context 'with FOSS query type fields' do
:user,
:users,
:work_item,
- :audit_event_definitions
+ :audit_event_definitions,
+ :abuse_report,
+ :abuse_report_labels
]
end
end
diff --git a/spec/support/shared_contexts/lib/gitlab/background_migration/backfill_project_statistics.rb b/spec/support/shared_contexts/lib/gitlab/background_migration/backfill_project_statistics.rb
new file mode 100644
index 00000000000..1b835e1392d
--- /dev/null
+++ b/spec/support/shared_contexts/lib/gitlab/background_migration/backfill_project_statistics.rb
@@ -0,0 +1,106 @@
+# frozen_string_literal: true
+
+RSpec.shared_context 'when backfilling project statistics' do
+ let!(:namespaces) { table(:namespaces) }
+ let!(:project_statistics_table) { table(:project_statistics) }
+ let!(:projects) { table(:projects) }
+ let!(:count_of_columns) { ProjectStatistics::STORAGE_SIZE_COMPONENTS.count }
+ let(:default_storage_size) { 12 }
+
+ let!(:root_group) do
+ namespaces.create!(name: 'root-group', path: 'root-group', type: 'Group') do |new_group|
+ new_group.update!(traversal_ids: [new_group.id])
+ end
+ end
+
+ let!(:group) do
+ namespaces.create!(name: 'group', path: 'group', parent_id: root_group.id, type: 'Group') do |new_group|
+ new_group.update!(traversal_ids: [root_group.id, new_group.id])
+ end
+ end
+
+ let!(:sub_group) do
+ namespaces.create!(name: 'subgroup', path: 'subgroup', parent_id: group.id, type: 'Group') do |new_group|
+ new_group.update!(traversal_ids: [root_group.id, group.id, new_group.id])
+ end
+ end
+
+ let!(:namespace1) do
+ namespaces.create!(
+ name: 'namespace1', type: 'Group', path: 'space1'
+ )
+ end
+
+ let!(:proj_namespace1) do
+ namespaces.create!(
+ name: 'proj1', path: 'proj1', type: 'Project', parent_id: namespace1.id
+ )
+ end
+
+ let!(:proj_namespace2) do
+ namespaces.create!(
+ name: 'proj2', path: 'proj2', type: 'Project', parent_id: namespace1.id
+ )
+ end
+
+ let!(:proj_namespace3) do
+ namespaces.create!(
+ name: 'proj3', path: 'proj3', type: 'Project', parent_id: sub_group.id
+ )
+ end
+
+ let!(:proj_namespace4) do
+ namespaces.create!(
+ name: 'proj4', path: 'proj4', type: 'Project', parent_id: sub_group.id
+ )
+ end
+
+ let!(:proj_namespace5) do
+ namespaces.create!(
+ name: 'proj5', path: 'proj5', type: 'Project', parent_id: sub_group.id
+ )
+ end
+
+ let!(:proj1) do
+ projects.create!(
+ name: 'proj1', path: 'proj1', namespace_id: namespace1.id, project_namespace_id: proj_namespace1.id
+ )
+ end
+
+ let!(:proj2) do
+ projects.create!(
+ name: 'proj2', path: 'proj2', namespace_id: namespace1.id, project_namespace_id: proj_namespace2.id
+ )
+ end
+
+ let!(:proj3) do
+ projects.create!(
+ name: 'proj3', path: 'proj3', namespace_id: sub_group.id, project_namespace_id: proj_namespace3.id
+ )
+ end
+
+ let!(:proj4) do
+ projects.create!(
+ name: 'proj4', path: 'proj4', namespace_id: sub_group.id, project_namespace_id: proj_namespace4.id
+ )
+ end
+
+ let!(:proj5) do
+ projects.create!(
+ name: 'proj5', path: 'proj5', namespace_id: sub_group.id, project_namespace_id: proj_namespace5.id
+ )
+ end
+
+ let(:migration) do
+ described_class.new(start_id: 1, end_id: proj4.id,
+ batch_table: 'project_statistics', batch_column: 'project_id',
+ sub_batch_size: 1_000, pause_ms: 0,
+ connection: ApplicationRecord.connection)
+ end
+
+ let(:default_projects) do
+ [
+ proj1, proj2, proj3, proj4
+ ]
+ end
+end
diff --git a/spec/support/shared_contexts/lib/gitlab/sidekiq_middleware/server_metrics_shared_context.rb b/spec/support/shared_contexts/lib/gitlab/sidekiq_middleware/server_metrics_shared_context.rb
index b6c54e902a2..d9b2b44980c 100644
--- a/spec/support/shared_contexts/lib/gitlab/sidekiq_middleware/server_metrics_shared_context.rb
+++ b/spec/support/shared_contexts/lib/gitlab/sidekiq_middleware/server_metrics_shared_context.rb
@@ -18,6 +18,7 @@ RSpec.shared_context 'server metrics with mocked prometheus' do
let(:elasticsearch_requests_total) { double('elasticsearch calls total metric') }
let(:load_balancing_metric) { double('load balancing metric') }
let(:sidekiq_mem_total_bytes) { double('sidekiq mem total bytes') }
+ let(:completion_seconds_sum_metric) { double('sidekiq completion seconds sum metric') }
before do
allow(Gitlab::Metrics).to receive(:histogram).and_call_original
@@ -36,6 +37,7 @@ RSpec.shared_context 'server metrics with mocked prometheus' do
allow(Gitlab::Metrics).to receive(:counter).with(:sidekiq_redis_requests_total, anything).and_return(redis_requests_total)
allow(Gitlab::Metrics).to receive(:counter).with(:sidekiq_elasticsearch_requests_total, anything).and_return(elasticsearch_requests_total)
allow(Gitlab::Metrics).to receive(:counter).with(:sidekiq_load_balancing_count, anything).and_return(load_balancing_metric)
+ allow(Gitlab::Metrics).to receive(:counter).with(:sidekiq_jobs_completion_seconds_sum, anything).and_return(completion_seconds_sum_metric)
allow(Gitlab::Metrics).to receive(:gauge).with(:sidekiq_running_jobs, anything, {}, :all).and_return(running_jobs_metric)
allow(Gitlab::Metrics).to receive(:gauge).with(:sidekiq_concurrency, anything, {}, :all).and_return(concurrency_metric)
allow(Gitlab::Metrics).to receive(:gauge).with(:sidekiq_mem_total_bytes, anything, {}, :all).and_return(sidekiq_mem_total_bytes)
@@ -76,8 +78,13 @@ RSpec.shared_context 'server metrics call' do
}
end
+ let(:stub_subject) { true }
+
before do
- allow(subject).to receive(:get_thread_cputime).and_return(thread_cputime_before, thread_cputime_after)
+ if stub_subject
+ allow(subject).to receive(:get_thread_cputime).and_return(thread_cputime_before, thread_cputime_after)
+ end
+
allow(Gitlab::Metrics::System).to receive(:monotonic_time).and_return(monotonic_time_before, monotonic_time_after)
allow(Gitlab::InstrumentationHelper).to receive(:queue_duration_for_job).with(job).and_return(queue_duration_for_job)
allow(ActiveRecord::LogSubscriber).to receive(:runtime).and_return(db_duration * 1000)
@@ -93,6 +100,7 @@ RSpec.shared_context 'server metrics call' do
allow(running_jobs_metric).to receive(:increment)
allow(redis_requests_total).to receive(:increment)
allow(elasticsearch_requests_total).to receive(:increment)
+ allow(completion_seconds_sum_metric).to receive(:increment)
allow(queue_duration_seconds).to receive(:observe)
allow(user_execution_seconds_metric).to receive(:observe)
allow(db_seconds_metric).to receive(:observe)
diff --git a/spec/support/shared_contexts/lib/sbom/package_url_shared_contexts.rb b/spec/support/shared_contexts/lib/sbom/package_url_shared_contexts.rb
index 263cf9f5e19..a4c454ea264 100644
--- a/spec/support/shared_contexts/lib/sbom/package_url_shared_contexts.rb
+++ b/spec/support/shared_contexts/lib/sbom/package_url_shared_contexts.rb
@@ -4,10 +4,12 @@ require 'oj'
def parameterized_test_matrix(invalid: false)
test_cases_path = File.join(
- File.expand_path(__dir__), '..', '..', '..', '..', 'fixtures', 'lib', 'sbom', 'package-url-test-cases.json')
+ File.expand_path(__dir__), '../../../../fixtures/lib/sbom/package-url-test-cases.json')
test_cases = Gitlab::Json.parse(File.read(test_cases_path))
- test_cases.filter { _1.delete('is_invalid') == invalid }.each_with_object({}) do |test_case, memo|
+ test_cases
+ .filter { |test_case| test_case.delete('is_invalid') == invalid }
+ .each_with_object({}) do |test_case, memo|
description = test_case.delete('description')
memo[description] = test_case.symbolize_keys
end
diff --git a/spec/support/shared_contexts/navbar_structure_context.rb b/spec/support/shared_contexts/navbar_structure_context.rb
index 0abf688566a..112b90029b8 100644
--- a/spec/support/shared_contexts/navbar_structure_context.rb
+++ b/spec/support/shared_contexts/navbar_structure_context.rb
@@ -112,6 +112,7 @@ RSpec.shared_context 'project navbar structure' do
_('CI/CD'),
_('Packages and registries'),
_('Monitor'),
+ (_('Analytics') if Gitlab.ee?),
s_('UsageQuota|Usage Quotas')
]
}
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 22caf2b3530..07a4cbdb534 100644
--- a/spec/support/shared_contexts/policies/group_policy_shared_context.rb
+++ b/spec/support/shared_contexts/policies/group_policy_shared_context.rb
@@ -12,7 +12,7 @@ RSpec.shared_context 'GroupPolicy context' do
let(:public_permissions) do
%i[
- read_group read_counts
+ read_group read_counts read_issue
read_label read_issue_board_list read_milestone read_issue_board
]
end
@@ -74,7 +74,6 @@ RSpec.shared_context 'GroupPolicy context' do
read_statistics
update_default_branch_protection
read_group_runners
- admin_group_runners
register_group_runners
read_billing
edit_billing
diff --git a/spec/support/shared_contexts/services/packages/rubygems/invalid_metadata.rb b/spec/support/shared_contexts/services/packages/rubygems/invalid_metadata.rb
new file mode 100644
index 00000000000..b6b962e5c08
--- /dev/null
+++ b/spec/support/shared_contexts/services/packages/rubygems/invalid_metadata.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+RSpec.shared_context 'with invalid Rubygems metadata' do
+ before do
+ allow_next_instance_of(::Packages::Rubygems::MetadataExtractionService) do |instance|
+ allow(instance).to receive(:execute).and_raise(ActiveRecord::StatementInvalid)
+ end
+ end
+end
diff --git a/spec/support/shared_contexts/user_contribution_events_shared_context.rb b/spec/support/shared_contexts/user_contribution_events_shared_context.rb
index 48f0ac1e4ac..b6f9b6bed44 100644
--- a/spec/support/shared_contexts/user_contribution_events_shared_context.rb
+++ b/spec/support/shared_contexts/user_contribution_events_shared_context.rb
@@ -14,16 +14,20 @@ RSpec.shared_context 'with user contribution events' do
# milestone
let_it_be(:milestone) { create(:milestone, project: project) }
- # note
- let_it_be(:note_on_issue) { create(:note_on_issue, noteable: issue, project: project) }
-
# design
let_it_be(:design) { create(:design, project: project, issue: issue, author: user) }
+ # note
+ let_it_be(:note_on_issue) { create(:note_on_issue, noteable: issue, project: project) }
+ let_it_be(:note_on_merge_request) { create(:note_on_merge_request, noteable: merge_request, project: project) }
+ let_it_be(:note_on_project_snippet) { create(:note_on_project_snippet, project: project) }
+ let_it_be(:note_on_design) { create(:note_on_design, noteable: design) }
+ let_it_be(:note_on_personal_snippet) do
+ create(:note, project: nil, noteable: create(:personal_snippet, author: user))
+ end
+
# work item
let_it_be(:incident) { create(:work_item, :incident, author: user, project: project) }
- let_it_be(:test_case) { create(:work_item, :test_case, author: user, project: project) }
- let_it_be(:requirement) { create(:work_item, :requirement, author: user, project: project) }
let_it_be(:task) { create(:work_item, :task, author: user, project: project) }
# events
@@ -36,17 +40,39 @@ RSpec.shared_context 'with user contribution events' do
# closed
let_it_be(:closed_issue_event) { create(:event, :closed, author: user, project: project, target: issue) }
let_it_be(:closed_milestone_event) { create(:event, :closed, author: user, project: project, target: milestone) }
- let_it_be(:closed_incident_event) { create(:event, :closed, author: user, project: project, target: incident) }
- let_it_be(:closed_test_case_event) { create(:event, :closed, author: user, project: project, target: test_case) }
let_it_be(:closed_merge_request_event) do
create(:event, :closed, author: user, project: project, target: merge_request)
end
+ let_it_be(:closed_task_event) do
+ create(:event, :closed, :for_work_item, author: user, project: project, target: task)
+ end
+
+ let_it_be(:closed_incident_event) do
+ create(:event, :closed, :for_work_item, author: user, project: project, target: incident)
+ end
+
# commented
- let_it_be(:commented_event) do
+ let_it_be(:commented_issue_event) do
create(:event, :commented, author: user, project: project, target: note_on_issue)
end
+ let_it_be(:commented_merge_request_event) do
+ create(:event, :commented, author: user, project: project, target: note_on_merge_request)
+ end
+
+ let_it_be(:commented_project_snippet_event) do
+ create(:event, :commented, author: user, target: note_on_project_snippet)
+ end
+
+ let_it_be(:commented_personal_snippet_event) do
+ create(:event, :commented, project: nil, author: user, target: note_on_personal_snippet)
+ end
+
+ let_it_be(:commented_design_event) do
+ create(:event, :commented, author: user, target: note_on_design)
+ end
+
# created
let_it_be(:created_issue_event) { create(:event, :created, author: user, project: project, target: issue) }
let_it_be(:created_milestone_event) { create(:event, :created, author: user, project: project, target: milestone) }
@@ -57,14 +83,6 @@ RSpec.shared_context 'with user contribution events' do
create(:event, :created, :for_work_item, author: user, project: project, target: incident)
end
- let_it_be(:created_test_case_event) do
- create(:event, :created, :for_work_item, author: user, project: project, target: test_case)
- end
-
- let_it_be(:created_requirement_event) do
- create(:event, :created, :for_work_item, author: user, project: project, target: requirement)
- end
-
let_it_be(:created_task_event) do
create(:event, :created, :for_work_item, author: user, project: project, target: task)
end
@@ -147,8 +165,8 @@ RSpec.shared_context 'with user contribution events' do
# reopened
let_it_be(:reopened_issue_event) { create(:event, :reopened, author: user, project: project, target: issue) }
let_it_be(:reopened_milestone_event) { create(:event, :reopened, author: user, project: project, target: milestone) }
+ let_it_be(:reopened_task_event) { create(:event, :reopened, author: user, project: project, target: task) }
let_it_be(:reopened_incident_event) { create(:event, :reopened, author: user, project: project, target: incident) }
- let_it_be(:reopened_test_case_event) { create(:event, :reopened, author: user, project: project, target: test_case) }
let_it_be(:reopened_merge_request_event) do
create(:event, :reopened, author: user, project: project, target: merge_request)
end
diff --git a/spec/support/shared_examples/bulk_imports/visibility_level_examples.rb b/spec/support/shared_examples/bulk_imports/visibility_level_examples.rb
index 02eae250e6a..23601134537 100644
--- a/spec/support/shared_examples/bulk_imports/visibility_level_examples.rb
+++ b/spec/support/shared_examples/bulk_imports/visibility_level_examples.rb
@@ -1,87 +1,80 @@
# frozen_string_literal: true
-RSpec.shared_examples 'visibility level settings' do
- context 'when public' do
- let(:data) { { 'visibility' => 'public' } }
-
- context 'when destination is a public group' do
- let(:destination_group) { create(:group, :public) }
-
- it 'sets visibility level to public' do
- expect(transformed_data[:visibility_level]).to eq(Gitlab::VisibilityLevel::PUBLIC)
- end
- end
-
- context 'when destination is a internal group' do
- let(:destination_group) { create(:group, :internal) }
-
- it 'sets visibility level to internal' do
- expect(transformed_data[:visibility_level]).to eq(Gitlab::VisibilityLevel::INTERNAL)
- end
- end
-
- context 'when destination is a private group' do
- let(:destination_group) { create(:group, :private) }
-
- it 'sets visibility level to private' do
- expect(transformed_data[:visibility_level]).to eq(Gitlab::VisibilityLevel::PRIVATE)
- end
- end
+RSpec.shared_examples 'visibility level settings' do |skip_nil_destination_tests|
+ using RSpec::Parameterized::TableSyntax
+
+ let_it_be(:public_group) { create(:group, :public) }
+ let_it_be(:internal_group) { create(:group, :internal) }
+ let_it_be(:private_group) { create(:group, :private) }
+ let(:data) { { 'visibility' => visibility_level } }
+
+ subject(:transformed_data) { described_class.new.transform(context, data) }
+
+ where(
+ :visibility_level,
+ :destination_group,
+ :restricted_level,
+ :expected
+ ) do
+ 'public' | ref(:public_group) | nil | 20
+ 'public' | ref(:public_group) | 20 | 10
+ 'public' | ref(:public_group) | 10 | 20
+ 'public' | ref(:public_group) | 0 | 20
+ 'public' | ref(:internal_group) | nil | 10
+ 'public' | ref(:internal_group) | 20 | 10
+ 'public' | ref(:internal_group) | 10 | 0
+ 'public' | ref(:internal_group) | 0 | 10
+ 'public' | ref(:private_group) | nil | 0
+ 'public' | ref(:private_group) | 20 | 0
+ 'public' | ref(:private_group) | 10 | 0
+ 'public' | ref(:private_group) | 0 | 0
+ 'public' | nil | nil | 20
+ 'public' | nil | 20 | 10
+ 'public' | nil | 10 | 20
+ 'public' | nil | 0 | 20
+ 'internal' | ref(:public_group) | nil | 10
+ 'internal' | ref(:public_group) | 20 | 10
+ 'internal' | ref(:public_group) | 10 | 0
+ 'internal' | ref(:public_group) | 0 | 10
+ 'internal' | ref(:internal_group) | nil | 10
+ 'internal' | ref(:internal_group) | 20 | 10
+ 'internal' | ref(:internal_group) | 10 | 0
+ 'internal' | ref(:internal_group) | 0 | 10
+ 'internal' | ref(:private_group) | nil | 0
+ 'internal' | ref(:private_group) | 20 | 0
+ 'internal' | ref(:private_group) | 10 | 0
+ 'internal' | ref(:private_group) | 0 | 0
+ 'internal' | nil | nil | 10
+ 'internal' | nil | 20 | 10
+ 'internal' | nil | 10 | 0
+ 'internal' | nil | 0 | 10
+ 'private' | ref(:public_group) | nil | 0
+ 'private' | ref(:public_group) | 20 | 0
+ 'private' | ref(:public_group) | 10 | 0
+ 'private' | ref(:public_group) | 0 | 0
+ 'private' | ref(:internal_group) | nil | 0
+ 'private' | ref(:internal_group) | 20 | 0
+ 'private' | ref(:internal_group) | 10 | 0
+ 'private' | ref(:internal_group) | 0 | 0
+ 'private' | ref(:private_group) | nil | 0
+ 'private' | ref(:private_group) | 20 | 0
+ 'private' | ref(:private_group) | 10 | 0
+ 'private' | ref(:private_group) | 0 | 0
+ 'private' | nil | nil | 0
+ 'private' | nil | 20 | 0
+ 'private' | nil | 10 | 0
+ 'private' | nil | 0 | 0
end
- context 'when internal' do
- let(:data) { { 'visibility' => 'internal' } }
-
- context 'when destination is a public group' do
- let(:destination_group) { create(:group, :public) }
-
- it 'sets visibility level to internal' do
- expect(transformed_data[:visibility_level]).to eq(Gitlab::VisibilityLevel::INTERNAL)
- end
- end
-
- context 'when destination is a internal group' do
- let(:destination_group) { create(:group, :internal) }
-
- it 'sets visibility level to internal' do
- expect(transformed_data[:visibility_level]).to eq(Gitlab::VisibilityLevel::INTERNAL)
- end
- end
-
- context 'when destination is a private group' do
- let(:destination_group) { create(:group, :private) }
-
- it 'sets visibility level to private' do
- expect(transformed_data[:visibility_level]).to eq(Gitlab::VisibilityLevel::PRIVATE)
- end
- end
- end
-
- context 'when private' do
- let(:data) { { 'visibility' => 'private' } }
-
- context 'when destination is a public group' do
- let(:destination_group) { create(:group, :public) }
-
- it 'sets visibility level to private' do
- expect(transformed_data[:visibility_level]).to eq(Gitlab::VisibilityLevel::PRIVATE)
- end
- end
-
- context 'when destination is a internal group' do
- let(:destination_group) { create(:group, :internal) }
-
- it 'sets visibility level to private' do
- expect(transformed_data[:visibility_level]).to eq(Gitlab::VisibilityLevel::PRIVATE)
- end
+ with_them do
+ before do
+ stub_application_setting(restricted_visibility_levels: [restricted_level])
end
- context 'when destination is a private group' do
- let(:destination_group) { create(:group, :private) }
+ it 'has the correct visibility level' do
+ next if destination_group.nil? && skip_nil_destination_tests
- it 'sets visibility level to private' do
- expect(transformed_data[:visibility_level]).to eq(Gitlab::VisibilityLevel::PRIVATE)
- end
+ expect(transformed_data[:visibility_level]).to eq(expected)
end
end
end
diff --git a/spec/support/shared_examples/channels/noteable/notes_channel_shared_examples.rb b/spec/support/shared_examples/channels/noteable/notes_channel_shared_examples.rb
new file mode 100644
index 00000000000..cb7001a9faf
--- /dev/null
+++ b/spec/support/shared_examples/channels/noteable/notes_channel_shared_examples.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'handle subscription based on user access' do
+ it 'subscribes to the noteable stream when user has access' do
+ subscribe(subscribe_params)
+
+ expect(subscription).to be_confirmed
+ expect(subscription).to have_stream_for(noteable)
+ end
+
+ it 'rejects the subscription when the user does not have access' do
+ stub_action_cable_connection current_user: nil
+
+ subscribe(subscribe_params)
+
+ expect(subscription).to be_rejected
+ end
+
+ context 'when action_cable_notes is disabled' do
+ before do
+ stub_feature_flags(action_cable_notes: false)
+ end
+
+ it 'rejects the subscription' do
+ subscribe(subscribe_params)
+
+ expect(subscription).to be_rejected
+ end
+ end
+end
diff --git a/spec/support/shared_examples/ci/deployable_policy_shared_examples.rb b/spec/support/shared_examples/ci/deployable_policy_shared_examples.rb
new file mode 100644
index 00000000000..73bdc094237
--- /dev/null
+++ b/spec/support/shared_examples/ci/deployable_policy_shared_examples.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'a deployable job policy' do |factory_type|
+ let_it_be_with_refind(:project) { create(:project, :private) }
+ let_it_be_with_refind(:user) { create(:user) }
+
+ let(:job) { create(factory_type, project: project, user: user, environment: 'production', ref: 'development') }
+ let(:policy) { described_class.new(user, job) }
+
+ context 'when the job triggerer is a project maintainer' do
+ before_all do
+ project.add_maintainer(user)
+ end
+
+ it { expect(policy).to be_allowed :update_build }
+
+ context 'when job is oudated deployment job' do
+ before do
+ allow(job).to receive(:outdated_deployment?).and_return(true)
+ end
+
+ it { expect(policy).not_to be_allowed :update_build }
+ end
+ end
+end
diff --git a/spec/support/shared_examples/ci/deployable_policy_shared_examples_ee.rb b/spec/support/shared_examples/ci/deployable_policy_shared_examples_ee.rb
new file mode 100644
index 00000000000..b1057b3f67a
--- /dev/null
+++ b/spec/support/shared_examples/ci/deployable_policy_shared_examples_ee.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'a deployable job policy in EE' do |factory_type|
+ using RSpec::Parameterized::TableSyntax
+
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, :repository, group: group) }
+
+ let(:user) { create(:user) }
+ let(:pipeline) { create(:ci_empty_pipeline, project: project) }
+ let(:environment) { create(:environment, project: project, name: 'production') }
+
+ let(:job) do
+ create(factory_type, pipeline: pipeline, project: project, environment: 'production', ref: 'development')
+ end
+
+ describe '#update_build?' do
+ subject { user.can?(:update_build, job) }
+
+ it_behaves_like 'protected environments access', direct_access: true
+ end
+
+ describe '#update_commit_status?' do
+ subject { user.can?(:update_commit_status, job) }
+
+ it_behaves_like 'protected environments access', direct_access: true
+ end
+
+ describe '#erase_build?' do
+ subject { user.can?(:erase_build, job) }
+
+ context 'when the job triggerer is a project maintainer' do
+ let_it_be_with_refind(:user) { create(:user).tap { |u| project.add_maintainer(u) } }
+
+ before do
+ stub_licensed_features(protected_environments: true)
+ end
+
+ it 'returns true for ci_build' do
+ # Currently, we allow users to delete normal jobs only.
+ if factory_type == :ci_build
+ is_expected.to eq(true)
+ else
+ is_expected.to eq(false)
+ end
+ end
+
+ context 'when environment is protected' do
+ before do
+ create(:protected_environment, name: environment.name, project: project)
+ end
+
+ it { is_expected.to eq(false) }
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/ci/deployable_shared_examples.rb b/spec/support/shared_examples/ci/deployable_shared_examples.rb
new file mode 100644
index 00000000000..b51a8fa20e2
--- /dev/null
+++ b/spec/support/shared_examples/ci/deployable_shared_examples.rb
@@ -0,0 +1,582 @@
+# frozen_string_literal: true
+
+# rubocop:disable Layout/LineLength
+# rubocop:disable RSpec/ContextWording
+RSpec.shared_examples 'a deployable job' do
+ it { is_expected.to have_one(:deployment) }
+
+ shared_examples 'calling proper BuildFinishedWorker' do
+ it 'calls Ci::BuildFinishedWorker' do
+ skip unless described_class == ::Ci::Build
+
+ expect(Ci::BuildFinishedWorker).to receive(:perform_async)
+
+ subject
+ end
+ end
+
+ describe '#outdated_deployment?' do
+ subject { job.outdated_deployment? }
+
+ let(:job) { create(factory_type, :created, :with_deployment, project: project, pipeline: pipeline, environment: 'production') }
+
+ context 'when job has no environment' do
+ let(:job) { create(factory_type, :created, pipeline: pipeline, environment: nil) }
+
+ it { expect(subject).to be_falsey }
+ end
+
+ context 'when project has forward deployment disabled' do
+ before do
+ project.ci_cd_settings.update!(forward_deployment_enabled: false)
+ end
+
+ it { expect(subject).to be_falsey }
+ end
+
+ context 'when job is not an outdated deployment' do
+ before do
+ allow(job.deployment).to receive(:older_than_last_successful_deployment?).and_return(false)
+ end
+
+ it { expect(subject).to be_falsey }
+ end
+
+ context 'when job is older than the latest deployment and still pending status' do
+ before do
+ allow(job.deployment).to receive(:older_than_last_successful_deployment?).and_return(true)
+ end
+
+ it { expect(subject).to be_truthy} # rubocop: disable Layout/SpaceInsideBlockBraces
+ end
+
+ context 'when job is older than the latest deployment but succeeded once' do
+ let(:job) { create(factory_type, :success, :with_deployment, project: project, pipeline: pipeline, environment: 'production') }
+
+ before do
+ allow(job.deployment).to receive(:older_than_last_successful_deployment?).and_return(true)
+ end
+
+ it 'returns false for allowing rollback' do
+ expect(subject).to be_falsey
+ end
+
+ context 'when forward_deployment_rollback_allowed option is disabled' do
+ before do
+ project.ci_cd_settings.update!(forward_deployment_rollback_allowed: false)
+ end
+
+ it 'returns true for disallowing rollback' do
+ expect(subject).to eq(true)
+ end
+ end
+ end
+ end
+
+ describe 'state transition as a deployable' do
+ subject { job.send(event) }
+
+ let!(:job) { create(factory_type, :with_deployment, :start_review_app, status: :pending, pipeline: pipeline) }
+ let(:deployment) { job.deployment }
+ let(:environment) { deployment.environment }
+
+ before do
+ allow(Deployments::LinkMergeRequestWorker).to receive(:perform_async)
+ allow(Deployments::HooksWorker).to receive(:perform_async)
+ end
+
+ it 'has deployments record with created status' do
+ expect(deployment).to be_created
+ expect(environment.name).to eq('review/master')
+ end
+
+ shared_examples_for 'avoid deadlock' do
+ it 'executes UPDATE in the right order' do
+ recorded = with_cross_database_modification_prevented do
+ ActiveRecord::QueryRecorder.new { subject }
+ end
+
+ index_for_build = recorded.log.index { |l| l.include?("UPDATE #{Ci::Build.quoted_table_name}") }
+ index_for_deployment = recorded.log.index { |l| l.include?("UPDATE \"deployments\"") }
+
+ expect(index_for_build).to be < index_for_deployment
+ end
+ end
+
+ context 'when transits to running' do
+ let(:event) { :run! }
+
+ it_behaves_like 'avoid deadlock'
+
+ it 'transits deployment status to running' do
+ with_cross_database_modification_prevented do
+ subject
+ end
+
+ expect(deployment).to be_running
+ end
+
+ context 'when deployment is already running state' do
+ before do
+ job.deployment.success!
+ end
+
+ it 'does not change deployment status and tracks an error' do
+ expect(Gitlab::ErrorTracking)
+ .to receive(:track_exception).with(
+ instance_of(Deployment::StatusSyncError), deployment_id: deployment.id, job_id: job.id)
+
+ with_cross_database_modification_prevented do
+ expect { subject }.not_to change { deployment.reload.status }
+ end
+ end
+ end
+ end
+
+ context 'when transits to success' do
+ let(:event) { :success! }
+
+ before do
+ allow(Deployments::UpdateEnvironmentWorker).to receive(:perform_async)
+ allow(Deployments::HooksWorker).to receive(:perform_async)
+ end
+
+ it_behaves_like 'avoid deadlock'
+ it_behaves_like 'calling proper BuildFinishedWorker'
+
+ it 'transits deployment status to success' do
+ with_cross_database_modification_prevented do
+ subject
+ end
+
+ expect(deployment).to be_success
+ end
+ end
+
+ context 'when transits to failed' do
+ let(:event) { :drop! }
+
+ it_behaves_like 'avoid deadlock'
+ it_behaves_like 'calling proper BuildFinishedWorker'
+
+ it 'transits deployment status to failed' do
+ with_cross_database_modification_prevented do
+ subject
+ end
+
+ expect(deployment).to be_failed
+ end
+ end
+
+ context 'when transits to skipped' do
+ let(:event) { :skip! }
+
+ it_behaves_like 'avoid deadlock'
+
+ it 'transits deployment status to skipped' do
+ with_cross_database_modification_prevented do
+ subject
+ end
+
+ expect(deployment).to be_skipped
+ end
+ end
+
+ context 'when transits to canceled' do
+ let(:event) { :cancel! }
+
+ it_behaves_like 'avoid deadlock'
+ it_behaves_like 'calling proper BuildFinishedWorker'
+
+ it 'transits deployment status to canceled' do
+ with_cross_database_modification_prevented do
+ subject
+ end
+
+ expect(deployment).to be_canceled
+ end
+ end
+
+ # Mimic playing a manual job that needs another job.
+ # `needs + when:manual` scenario, see: https://gitlab.com/gitlab-org/gitlab/-/issues/347502
+ context 'when transits from skipped to created to running' do
+ before do
+ job.skip!
+ end
+
+ context 'during skipped to created' do
+ let(:event) { :process! }
+
+ it 'transitions to created' do
+ subject
+
+ expect(deployment).to be_created
+ end
+ end
+
+ context 'during created to running' do
+ let(:event) { :run! }
+
+ before do
+ job.process!
+ job.enqueue!
+ end
+
+ it 'transitions to running and calls webhook' do
+ freeze_time do
+ expect(Deployments::HooksWorker)
+ .to receive(:perform_async).with(hash_including({ 'deployment_id' => deployment.id, 'status' => 'running', 'status_changed_at' => Time.current.to_s }))
+
+ subject
+ end
+
+ expect(deployment).to be_running
+ end
+ end
+ end
+ end
+
+ describe '#on_stop' do
+ subject { job.on_stop }
+
+ context 'when a job has a specification that it can be stopped from the other job' do
+ let(:job) { create(factory_type, :start_review_app, pipeline: pipeline) }
+
+ it 'returns the other job name' do
+ is_expected.to eq('stop_review_app')
+ end
+ end
+
+ context 'when a job does not have environment information' do
+ let(:job) { create(factory_type, pipeline: pipeline) }
+
+ it 'returns nil' do
+ is_expected.to be_nil
+ end
+ end
+ end
+
+ describe '#environment_tier_from_options' do
+ subject { job.environment_tier_from_options }
+
+ let(:job) { Ci::Build.new(options: options) }
+ let(:options) { { environment: { deployment_tier: 'production' } } }
+
+ it { is_expected.to eq('production') }
+
+ context 'when options does not include deployment_tier' do
+ let(:options) { { environment: { name: 'production' } } }
+
+ it { is_expected.to be_nil }
+ end
+ end
+
+ describe '#environment_tier' do
+ subject { job.environment_tier }
+
+ let(:options) { { environment: { deployment_tier: 'production' } } }
+ let!(:environment) { create(:environment, name: 'production', tier: 'development', project: project) }
+ let(:job) { Ci::Build.new(options: options, environment: 'production', project: project) }
+
+ it { is_expected.to eq('production') }
+
+ context 'when options does not include deployment_tier' do
+ let(:options) { { environment: { name: 'production' } } }
+
+ it 'uses tier from environment' do
+ is_expected.to eq('development')
+ end
+
+ context 'when persisted environment is absent' do
+ let(:environment) { nil }
+
+ it { is_expected.to be_nil }
+ end
+ end
+ end
+
+ describe 'environment' do
+ describe '#has_environment_keyword?' do
+ subject { job.has_environment_keyword? }
+
+ context 'when environment is defined' do
+ before do
+ job.update!(environment: 'review')
+ end
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when environment is not defined' do
+ before do
+ job.update!(environment: nil)
+ end
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ describe '#expanded_environment_name' do
+ subject { job.expanded_environment_name }
+
+ context 'when environment uses $CI_COMMIT_REF_NAME' do
+ let(:job) do
+ create(
+ factory_type,
+ ref: 'master',
+ environment: 'review/$CI_COMMIT_REF_NAME',
+ pipeline: pipeline
+ )
+ end
+
+ it { is_expected.to eq('review/master') }
+ end
+
+ context 'when environment uses yaml_variables containing symbol keys' do
+ let(:job) do
+ create(
+ factory_type,
+ yaml_variables: [{ key: :APP_HOST, value: 'host' }],
+ environment: 'review/$APP_HOST',
+ pipeline: pipeline
+ )
+ end
+
+ it 'returns an expanded environment name with a list of variables' do
+ is_expected.to eq('review/host')
+ end
+
+ context 'when job metadata has already persisted the expanded environment name' do
+ before do
+ job.metadata.expanded_environment_name = 'review/foo'
+ end
+
+ it 'returns a persisted expanded environment name without a list of variables' do
+ expect(job).not_to receive(:simple_variables)
+
+ is_expected.to eq('review/foo')
+ end
+ end
+ end
+
+ context 'when using persisted variables' do
+ let(:job) do
+ create(factory_type, environment: 'review/x$CI_JOB_ID', pipeline: pipeline)
+ end
+
+ it { is_expected.to eq('review/x') }
+ end
+
+ context 'when environment name uses a nested variable' do
+ let(:yaml_variables) do
+ [
+ { key: 'ENVIRONMENT_NAME', value: '${CI_COMMIT_REF_NAME}' }
+ ]
+ end
+
+ let(:job) do
+ create(
+ factory_type,
+ ref: 'master',
+ yaml_variables: yaml_variables,
+ environment: 'review/$ENVIRONMENT_NAME',
+ pipeline: pipeline
+ )
+ end
+
+ it { is_expected.to eq('review/master') }
+ end
+ end
+
+ describe '#expanded_kubernetes_namespace' do
+ let(:job) { create(factory_type, environment: environment, options: options, pipeline: pipeline) }
+
+ subject { job.expanded_kubernetes_namespace }
+
+ context 'environment and namespace are not set' do
+ let(:environment) { nil }
+ let(:options) { nil }
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'environment is specified' do
+ let(:environment) { 'production' }
+
+ context 'namespace is not set' do
+ let(:options) { nil }
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'namespace is provided' do
+ let(:options) do
+ {
+ environment: {
+ name: environment,
+ kubernetes: {
+ namespace: namespace
+ }
+ }
+ }
+ end
+
+ context 'with a static value' do
+ let(:namespace) { 'production' }
+
+ it { is_expected.to eq namespace }
+ end
+
+ context 'with a dynamic value' do
+ let(:namespace) { 'deploy-$CI_COMMIT_REF_NAME' }
+
+ it { is_expected.to eq 'deploy-master' }
+ end
+ end
+ end
+ end
+
+ describe '#deployment_job?' do
+ subject { job.deployment_job? }
+
+ context 'when environment is defined' do
+ before do
+ job.update!(environment: 'review')
+ end
+
+ context 'no action is defined' do
+ it { is_expected.to be_truthy }
+ end
+
+ context 'and start action is defined' do
+ before do
+ job.update!(options: { environment: { action: 'start' } })
+ end
+
+ it { is_expected.to be_truthy }
+ end
+ end
+
+ context 'when environment is not defined' do
+ before do
+ job.update!(environment: nil)
+ end
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ describe '#stops_environment?' do
+ subject { job.stops_environment? }
+
+ context 'when environment is defined' do
+ before do
+ job.update!(environment: 'review')
+ end
+
+ context 'no action is defined' do
+ it { is_expected.to be_falsey }
+ end
+
+ context 'and stop action is defined' do
+ before do
+ job.update!(options: { environment: { action: 'stop' } })
+ end
+
+ it { is_expected.to be_truthy }
+ end
+ end
+
+ context 'when environment is not defined' do
+ before do
+ job.update!(environment: nil)
+ end
+
+ it { is_expected.to be_falsey }
+ end
+ end
+ end
+
+ describe '#persisted_environment' do
+ let!(:environment) do
+ create(:environment, project: project, name: "foo-#{project.default_branch}")
+ end
+
+ subject { job.persisted_environment }
+
+ context 'when referenced literally' do
+ let(:job) do
+ create(factory_type, pipeline: pipeline, environment: "foo-#{project.default_branch}")
+ end
+
+ it { is_expected.to eq(environment) }
+ end
+
+ context 'when referenced with a variable' do
+ let(:job) do
+ create(factory_type, pipeline: pipeline, environment: "foo-$CI_COMMIT_REF_NAME")
+ end
+
+ it { is_expected.to eq(environment) }
+ end
+
+ context 'when there is no environment' do
+ it { is_expected.to be_nil }
+ end
+
+ context 'when job has a stop environment' do
+ let(:job) { create(factory_type, :stop_review_app, pipeline: pipeline, environment: "foo-#{project.default_branch}") }
+
+ it 'expands environment name' do
+ expect(job).to receive(:expanded_environment_name).and_call_original
+
+ is_expected.to eq(environment)
+ end
+ end
+ end
+
+ describe '#deployment_status' do
+ before do
+ allow_any_instance_of(Ci::Build).to receive(:create_deployment) # rubocop:disable RSpec/AnyInstanceOf
+ end
+
+ context 'when job is a last deployment' do
+ let(:job) { create(factory_type, :success, environment: 'production', pipeline: pipeline) }
+ let(:environment) { create(:environment, name: 'production', project: job.project) }
+ let!(:deployment) { create(:deployment, :success, environment: environment, project: environment.project, deployable: job) }
+
+ it { expect(job.deployment_status).to eq(:last) }
+ end
+
+ context 'when there is a newer job with deployment' do
+ let(:job) { create(factory_type, :success, environment: 'production', pipeline: pipeline) }
+ let(:environment) { create(:environment, name: 'production', project: job.project) }
+ let!(:deployment) { create(:deployment, :success, environment: environment, project: environment.project, deployable: job) }
+ let!(:last_deployment) { create(:deployment, :success, environment: environment, project: environment.project) }
+
+ it { expect(job.deployment_status).to eq(:out_of_date) }
+ end
+
+ context 'when job with deployment has failed' do
+ let(:job) { create(factory_type, :failed, environment: 'production', pipeline: pipeline) }
+ let(:environment) { create(:environment, name: 'production', project: job.project) }
+ let!(:deployment) { create(:deployment, :success, environment: environment, project: environment.project, deployable: job) }
+
+ it { expect(job.deployment_status).to eq(:failed) }
+ end
+
+ context 'when job with deployment is running' do
+ let(:job) { create(factory_type, environment: 'production', pipeline: pipeline) }
+ let(:environment) { create(:environment, name: 'production', project: job.project) }
+ let!(:deployment) { create(:deployment, :success, environment: environment, project: environment.project, deployable: job) }
+
+ it { expect(job.deployment_status).to eq(:creating) }
+ end
+ end
+
+ def factory_type
+ described_class.name.underscore.tr('/', '_')
+ end
+end
+# rubocop:enable Layout/LineLength
+# rubocop:enable RSpec/ContextWording
diff --git a/spec/support/shared_examples/ci/deployable_shared_examples_ee.rb b/spec/support/shared_examples/ci/deployable_shared_examples_ee.rb
new file mode 100644
index 00000000000..803af36eabb
--- /dev/null
+++ b/spec/support/shared_examples/ci/deployable_shared_examples_ee.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'a deployable job in EE' do
+ describe 'when the job is waiting for deployment approval' do
+ let(:job) { create(factory_type, :manual, environment: 'production', pipeline: pipeline) }
+ let!(:deployment) { create(:deployment, :blocked, deployable: job) }
+
+ before do
+ allow(deployment).to receive(:waiting_for_approval?).and_return(true)
+ end
+
+ it 'does not allow the job to be enqueued' do
+ expect { job.enqueue! }.to raise_error(StateMachines::InvalidTransition)
+ end
+ end
+
+ describe '#playable?' do
+ context 'when job is waiting for deployment approval' do
+ subject { build_stubbed(factory_type, :manual, environment: 'production', pipeline: pipeline) }
+
+ let!(:deployment) { create(:deployment, :blocked, deployable: subject) }
+
+ before do
+ allow(deployment).to receive(:waiting_for_approval?).and_return(true)
+ end
+
+ it { is_expected.not_to be_playable }
+ end
+ end
+
+ def factory_type
+ described_class.name.underscore.tr('/', '_')
+ end
+end
diff --git a/spec/support/shared_examples/ci/pipeline_schedules_create_or_update_shared_examples.rb b/spec/support/shared_examples/ci/pipeline_schedules_create_or_update_shared_examples.rb
new file mode 100644
index 00000000000..399225c13b2
--- /dev/null
+++ b/spec/support/shared_examples/ci/pipeline_schedules_create_or_update_shared_examples.rb
@@ -0,0 +1,121 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'pipeline schedules checking variables permission' do
+ let(:params) do
+ {
+ description: 'desc',
+ ref: 'patch-x',
+ active: false,
+ cron: '*/1 * * * *',
+ cron_timezone: 'UTC',
+ variables_attributes: variables_attributes
+ }
+ end
+
+ shared_examples 'success response' do
+ it 'saves values with passed params' do
+ result = service.execute
+
+ expect(result.status).to eq(:success)
+ expect(result.payload).to have_attributes(
+ description: 'desc',
+ ref: 'patch-x',
+ active: false,
+ cron: '*/1 * * * *',
+ cron_timezone: 'UTC'
+ )
+ end
+ end
+
+ shared_examples 'failure response' do
+ it 'does not save' do
+ result = service.execute
+
+ expect(result.status).to eq(:error)
+ expect(result.reason).to eq(:forbidden)
+ expect(result.message).to match_array(
+ ['The current user is not authorized to set pipeline schedule variables']
+ )
+ end
+ end
+
+ context 'when sending variables' do
+ let(:variables_attributes) do
+ [{ key: 'VAR2', secret_value: 'secret 2' }]
+ end
+
+ shared_examples 'success response with variables' do
+ it_behaves_like 'success response'
+
+ it 'saves variables' do
+ result = service.execute
+
+ variables = result.payload.variables.map { |v| [v.key, v.value] }
+
+ expect(variables).to include(
+ ['VAR2', 'secret 2']
+ )
+ end
+ end
+
+ context 'when user is maintainer' do
+ it_behaves_like 'success response with variables'
+ end
+
+ context 'when user is developer' do
+ before_all do
+ project.add_developer(user)
+ end
+
+ it_behaves_like 'success response with variables'
+ end
+
+ context 'when restrict_user_defined_variables is true' do
+ before_all do
+ project.update!(restrict_user_defined_variables: true)
+ end
+
+ it_behaves_like 'success response with variables'
+
+ context 'when user is developer' do
+ before_all do
+ project.add_developer(user)
+ end
+
+ it_behaves_like 'failure response'
+ end
+ end
+ end
+
+ context 'when not sending variables' do
+ let(:variables_attributes) { [] }
+
+ context 'when user is maintainer' do
+ it_behaves_like 'success response'
+ end
+
+ context 'when user is developer' do
+ before_all do
+ project.add_developer(user)
+ end
+
+ it_behaves_like 'success response'
+ end
+
+ context 'when restrict_user_defined_variables is true' do
+ before_all do
+ project.update!(restrict_user_defined_variables: true)
+ end
+
+ it_behaves_like 'success response'
+
+ context 'when user is developer' do
+ before_all do
+ project.add_developer(user)
+ end
+
+ it_behaves_like 'success response'
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/ci/stage_shared_examples.rb b/spec/support/shared_examples/ci/stage_shared_examples.rb
index cdb1058e584..a2849e00d27 100644
--- a/spec/support/shared_examples/ci/stage_shared_examples.rb
+++ b/spec/support/shared_examples/ci/stage_shared_examples.rb
@@ -21,7 +21,7 @@ RSpec.shared_examples 'manual playable stage' do |stage_type|
context 'when is skipped' do
let(:status) { 'skipped' }
- it { is_expected.to be_falsy }
+ it { is_expected.to be_truthy }
end
end
end
diff --git a/spec/support/shared_examples/ci/waiting_for_approval_status_shared_examples.rb b/spec/support/shared_examples/ci/waiting_for_approval_status_shared_examples.rb
new file mode 100644
index 00000000000..44226345fd2
--- /dev/null
+++ b/spec/support/shared_examples/ci/waiting_for_approval_status_shared_examples.rb
@@ -0,0 +1,79 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'a deployment job waiting for approval' do |factory_type|
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:job) { create(factory_type, :manual, environment: 'production', project: project) }
+
+ subject { described_class.new(Gitlab::Ci::Status::Core.new(job, user)) }
+
+ describe '.matches?' do
+ subject { described_class.matches?(job, user) }
+
+ let(:job) { create(factory_type, :manual, environment: 'production', project: project) }
+ let!(:deployment) { create(:deployment, deployment_status, deployable: job, project: project) }
+
+ context 'when job is waiting for approval' do
+ let(:deployment_status) { :blocked }
+
+ before do
+ allow(deployment).to receive(:waiting_for_approval?).and_return(true)
+ end
+
+ it 'is a correct match' do
+ expect(subject).to be_truthy
+ end
+ end
+
+ context 'when job is not waiting for approval' do
+ let(:deployment_status) { :created }
+
+ it 'does not match' do
+ expect(subject).to be_falsey
+ end
+ end
+ end
+
+ describe '#illustration' do
+ before do
+ environment = create(:environment, name: 'production', project: project)
+ create(:deployment, :blocked, project: project, environment: environment, deployable: job)
+ end
+
+ it { expect(subject.illustration).to include(:image, :size) }
+ it { expect(subject.illustration[:title]).to eq('Waiting for approvals') }
+
+ it do
+ expect(subject.illustration[:content]).to include('This job deploys to the protected environment "production"')
+ end
+ end
+
+ describe '#has_action?' do
+ it { expect(subject.has_action?).to be_truthy }
+ end
+
+ describe '#action_icon' do
+ it { expect(subject.action_icon).to be_nil }
+ end
+
+ describe '#action_title' do
+ it { expect(subject.action_title).to be_nil }
+ end
+
+ describe '#action_button_title' do
+ it { expect(subject.action_button_title).to eq('View environment details page') }
+ end
+
+ describe '#action_path' do
+ before do
+ environment = create(:environment, name: 'production', project: project)
+ create(:deployment, :blocked, project: project, environment: environment, deployable: job)
+ end
+
+ it { expect(subject.action_path).to include('environments') }
+ end
+
+ describe '#action_method' do
+ it { expect(subject.action_method).to eq(:get) }
+ end
+end
diff --git a/spec/support/shared_examples/controllers/internal_event_tracking_examples.rb b/spec/support/shared_examples/controllers/internal_event_tracking_examples.rb
index e2a4fb31361..05068cd60af 100644
--- a/spec/support/shared_examples/controllers/internal_event_tracking_examples.rb
+++ b/spec/support/shared_examples/controllers/internal_event_tracking_examples.rb
@@ -10,8 +10,6 @@
RSpec.shared_examples 'internal event tracking' do
let(:fake_tracker) { instance_spy(Gitlab::Tracking::Destinations::Snowplow) }
- let(:namespace) { nil }
- let(:proejct) { nil }
before do
allow(Gitlab::Tracking).to receive(:tracker).and_return(fake_tracker)
@@ -23,18 +21,23 @@ RSpec.shared_examples 'internal event tracking' do
it 'logs to Snowplow', :aggregate_failures do
subject
+ project = try(:project)
+ user = try(:user)
+ namespace = try(:namespace)
+
expect(Gitlab::Tracking::StandardContext)
.to have_received(:new)
.with(
project_id: project&.id,
- user_id: user.id,
+ user_id: user&.id,
namespace_id: namespace&.id,
plan_name: namespace&.actual_plan_name
- )
+ ).at_least(:once)
expect(Gitlab::Tracking::ServicePingContext)
.to have_received(:new)
.with(data_source: :redis_hll, event: action)
+ .at_least(:once)
expect(fake_tracker).to have_received(:event)
.with(
@@ -45,6 +48,5 @@ RSpec.shared_examples 'internal event tracking' do
an_instance_of(SnowplowTracker::SelfDescribingJson)
]
)
- .exactly(:once)
end
end
diff --git a/spec/support/shared_examples/controllers/known_sign_in_shared_examples.rb b/spec/support/shared_examples/controllers/known_sign_in_shared_examples.rb
index 3f147f942ba..77dd67c77a4 100644
--- a/spec/support/shared_examples/controllers/known_sign_in_shared_examples.rb
+++ b/spec/support/shared_examples/controllers/known_sign_in_shared_examples.rb
@@ -9,10 +9,8 @@ RSpec.shared_examples 'known sign in' do
user.update!(current_sign_in_ip: ip)
end
- def stub_cookie(value = user.id)
- cookies.encrypted[KnownSignIn::KNOWN_SIGN_IN_COOKIE] = {
- value: value, expires: KnownSignIn::KNOWN_SIGN_IN_COOKIE_EXPIRY
- }
+ def stub_cookie(value = user.id, expires = KnownSignIn::KNOWN_SIGN_IN_COOKIE_EXPIRY)
+ cookies.encrypted[KnownSignIn::KNOWN_SIGN_IN_COOKIE] = { value: value, expires: expires }
end
context 'when the remote IP and the last sign in IP match' do
@@ -57,15 +55,13 @@ RSpec.shared_examples 'known sign in' do
end
it 'notifies the user when the cookie is expired' do
- stub_cookie
-
- travel_to((KnownSignIn::KNOWN_SIGN_IN_COOKIE_EXPIRY + 1.day).from_now) do
- expect_next_instance_of(NotificationService) do |instance|
- expect(instance).to receive(:unknown_sign_in)
- end
+ stub_cookie(user.id, 1.day.ago)
- post_action
+ expect_next_instance_of(NotificationService) do |instance|
+ expect(instance).to receive(:unknown_sign_in)
end
+
+ post_action
end
context 'when notify_on_unknown_sign_in global setting is false' do
diff --git a/spec/support/shared_examples/controllers/unique_visits_shared_examples.rb b/spec/support/shared_examples/controllers/unique_visits_shared_examples.rb
index ac7680f7ddb..7f33ece854b 100644
--- a/spec/support/shared_examples/controllers/unique_visits_shared_examples.rb
+++ b/spec/support/shared_examples/controllers/unique_visits_shared_examples.rb
@@ -10,7 +10,7 @@ RSpec.shared_examples 'tracking unique visits' do |method|
ids.each do |id|
expect(Gitlab::UsageDataCounters::HLLRedisCounter)
- .to receive(:track_event).with(id, values: kind_of(String))
+ .to receive(:track_event).with(id, values: anything)
end
get method, params: request_params, format: :html
@@ -21,7 +21,7 @@ RSpec.shared_examples 'tracking unique visits' do |method|
ids.each do |id|
expect(Gitlab::UsageDataCounters::HLLRedisCounter)
- .to receive(:track_event).with(id, values: kind_of(String))
+ .to receive(:track_event).with(id, values: anything)
end
stub_do_not_track('0')
diff --git a/spec/support/shared_examples/database_health_status_indicators/prometheus_alert_based_shared_examples.rb b/spec/support/shared_examples/database_health_status_indicators/prometheus_alert_based_shared_examples.rb
new file mode 100644
index 00000000000..109a349a652
--- /dev/null
+++ b/spec/support/shared_examples/database_health_status_indicators/prometheus_alert_based_shared_examples.rb
@@ -0,0 +1,133 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'Prometheus Alert based health indicator' do
+ let(:schema) { :main }
+ let(:connection) { Gitlab::Database.database_base_models[schema].connection }
+
+ around do |example|
+ Gitlab::Database::SharedModel.using_connection(connection) do
+ example.run
+ end
+ end
+
+ describe '#evaluate' do
+ let(:prometheus_url) { 'http://thanos:9090' }
+ let(:prometheus_config) { [prometheus_url, { allow_local_requests: true, verify: true }] }
+
+ let(:prometheus_client) { instance_double(Gitlab::PrometheusClient) }
+
+ let(:context) do
+ Gitlab::Database::HealthStatus::Context.new(
+ described_class,
+ connection,
+ ['users'],
+ gitlab_schema
+ )
+ end
+
+ let(:gitlab_schema) { "gitlab_#{schema}" }
+ let(:client_ready) { true }
+ let(:indicator_name) { described_class.name.demodulize }
+ let(:indicator) { described_class.new(context) }
+
+ subject(:evaluate) { indicator.evaluate }
+
+ before do
+ stub_application_setting(prometheus_alert_db_indicators_settings: prometheus_alert_db_indicators_settings)
+
+ allow(Gitlab::PrometheusClient).to receive(:new).with(*prometheus_config).and_return(prometheus_client)
+ allow(prometheus_client).to receive(:ready?).and_return(client_ready)
+ end
+
+ shared_examples 'Patroni Apdex Evaluator' do |schema|
+ context "with #{schema} schema" do
+ let(:schema) { schema }
+
+ it 'returns NoSignal signal in case the feature flag is disabled' do
+ stub_feature_flags(feature_flag => false)
+
+ expect(evaluate).to be_a(Gitlab::Database::HealthStatus::Signals::NotAvailable)
+ expect(evaluate.reason).to include('indicator disabled')
+ end
+
+ context 'without prometheus_alert_db_indicators_settings' do
+ let(:prometheus_alert_db_indicators_settings) { nil }
+
+ it 'returns Unknown signal' do
+ expect(evaluate).to be_a(Gitlab::Database::HealthStatus::Signals::Unknown)
+ expect(evaluate.reason).to include('Prometheus Settings not configured')
+ end
+ end
+
+ context 'when Prometheus client is not ready' do
+ let(:client_ready) { false }
+
+ it 'returns Unknown signal' do
+ expect(evaluate).to be_a(Gitlab::Database::HealthStatus::Signals::Unknown)
+ expect(evaluate.reason).to include('Prometheus client is not ready')
+ end
+ end
+
+ context 'when apdex SLI query is not configured' do
+ let(:"sli_query_#{schema}") { nil }
+
+ it 'returns Unknown signal' do
+ expect(evaluate).to be_a(Gitlab::Database::HealthStatus::Signals::Unknown)
+ expect(evaluate.reason).to include("#{indicator_name} SLI query is not configured")
+ end
+ end
+
+ context 'when slo is not configured' do
+ let(:"slo_#{schema}") { nil }
+
+ it 'returns Unknown signal' do
+ expect(evaluate).to be_a(Gitlab::Database::HealthStatus::Signals::Unknown)
+ expect(evaluate.reason).to include("#{indicator_name} SLO is not configured")
+ end
+ end
+
+ it 'returns Normal signal when SLI condition is met' do
+ expect(prometheus_client).to receive(:query)
+ .with(send("sli_query_#{schema}"))
+ .and_return([{ "value" => [1662423310.878, sli_with_good_condition[schema]] }])
+ expect(evaluate).to be_a(Gitlab::Database::HealthStatus::Signals::Normal)
+ expect(evaluate.reason).to include("#{indicator_name} SLI condition met")
+ end
+
+ it 'returns Stop signal when SLI condition is not met' do
+ expect(prometheus_client).to receive(:query)
+ .with(send("sli_query_#{schema}"))
+ .and_return([{ "value" => [1662423310.878, sli_with_bad_condition[schema]] }])
+ expect(evaluate).to be_a(Gitlab::Database::HealthStatus::Signals::Stop)
+ expect(evaluate.reason).to include("#{indicator_name} SLI condition not met")
+ end
+
+ context 'when SLI can not be calculated' do
+ where(:result) do
+ [
+ nil,
+ [],
+ [{}],
+ [{ 'value' => 1 }],
+ [{ 'value' => [1] }]
+ ]
+ end
+
+ with_them do
+ it 'returns Unknown signal' do
+ expect(prometheus_client).to receive(:query).and_return(result)
+ expect(evaluate).to be_a(Gitlab::Database::HealthStatus::Signals::Unknown)
+ expect(evaluate.reason).to include("#{indicator_name} can not be calculated")
+ end
+ end
+ end
+ end
+ end
+
+ Gitlab::Database.database_base_models.each do |database_base_model, connection|
+ next unless connection.present?
+
+ it_behaves_like 'Patroni Apdex Evaluator', database_base_model.to_sym
+ end
+ end
+end
diff --git a/spec/support/shared_examples/deployments/create_for_job_shared_examples.rb b/spec/support/shared_examples/deployments/create_for_job_shared_examples.rb
new file mode 100644
index 00000000000..4f6f5b9a91a
--- /dev/null
+++ b/spec/support/shared_examples/deployments/create_for_job_shared_examples.rb
@@ -0,0 +1,153 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'create deployment for job' do
+ describe '#execute' do
+ subject { service.execute(job) }
+
+ context 'with a deployment job' do
+ let!(:job) { create(factory_type, :start_review_app, project: project) }
+ let!(:environment) { create(:environment, project: project, name: job.expanded_environment_name) }
+
+ it 'creates a deployment record' do
+ expect { subject }.to change { Deployment.count }.by(1)
+
+ job.reset
+ expect(job.deployment.project).to eq(job.project)
+ expect(job.deployment.ref).to eq(job.ref)
+ expect(job.deployment.sha).to eq(job.sha)
+ expect(job.deployment.deployable).to eq(job)
+ expect(job.deployment.deployable_type).to eq('CommitStatus')
+ expect(job.deployment.environment).to eq(job.persisted_environment)
+ expect(job.deployment.valid?).to be_truthy
+ end
+
+ context 'when creation failure occures' do
+ before do
+ allow_next_instance_of(Deployment) do |deployment|
+ allow(deployment).to receive(:save!) { raise ActiveRecord::RecordInvalid }
+ end
+ end
+
+ it 'trackes the exception' do
+ expect { subject }.to raise_error(described_class::DeploymentCreationError)
+
+ expect(Deployment.count).to eq(0)
+ end
+ end
+
+ context 'when the corresponding environment does not exist' do
+ let!(:environment) {} # rubocop:disable Lint/EmptyBlock
+
+ it 'does not create a deployment record' do
+ expect { subject }.not_to change { Deployment.count }
+
+ expect(job.deployment).to be_nil
+ end
+ end
+ end
+
+ context 'with a teardown job' do
+ let!(:job) { create(factory_type, :stop_review_app, project: project) }
+ let!(:environment) { create(:environment, name: job.expanded_environment_name) }
+
+ it 'does not create a deployment record' do
+ expect { subject }.not_to change { Deployment.count }
+
+ expect(job.deployment).to be_nil
+ end
+ end
+
+ context 'with a normal job' do
+ let!(:job) { create(factory_type, project: project) }
+
+ it 'does not create a deployment record' do
+ expect { subject }.not_to change { Deployment.count }
+
+ expect(job.deployment).to be_nil
+ end
+ end
+
+ context 'when job has environment attribute' do
+ let!(:job) do
+ create(factory_type, environment: 'production', project: project,
+ options: { environment: { name: 'production', **kubernetes_options } }) # rubocop:disable Layout/ArgumentAlignment
+ end
+
+ let!(:environment) { create(:environment, project: project, name: job.expanded_environment_name) }
+
+ let(:kubernetes_options) { {} }
+
+ it 'returns a deployment object with environment' do
+ expect(subject).to be_a(Deployment)
+ expect(subject.iid).to be_present
+ expect(subject.environment.name).to eq('production')
+ expect(subject.cluster).to be_nil
+ expect(subject.deployment_cluster).to be_nil
+ end
+
+ context 'when environment has deployment platform' do
+ let!(:cluster) { create(:cluster, :provided_by_gcp, projects: [project], managed: managed_cluster) }
+ let(:managed_cluster) { true }
+
+ it 'sets the cluster and deployment_cluster' do
+ expect(subject.cluster).to eq(cluster) # until we stop double writing in 12.9: https://gitlab.com/gitlab-org/gitlab/issues/202628
+ expect(subject.deployment_cluster.cluster).to eq(cluster)
+ end
+
+ context 'when a custom namespace is given' do
+ let(:kubernetes_options) { { kubernetes: { namespace: 'the-custom-namespace' } } }
+
+ context 'when cluster is managed' do
+ it 'does not set the custom namespace' do
+ expect(subject.deployment_cluster.kubernetes_namespace).not_to eq('the-custom-namespace')
+ end
+ end
+
+ context 'when cluster is not managed' do
+ let(:managed_cluster) { false }
+
+ it 'sets the custom namespace' do
+ expect(subject.deployment_cluster.kubernetes_namespace).to eq('the-custom-namespace')
+ end
+ end
+ end
+ end
+
+ context 'when job already has deployment' do
+ let!(:job) { create(factory_type, :with_deployment, project: project, environment: 'production') }
+ let!(:environment) {} # rubocop:disable Lint/EmptyBlock
+
+ it 'returns the persisted deployment' do
+ expect { subject }.not_to change { Deployment.count }
+
+ is_expected.to eq(job.deployment)
+ end
+ end
+ end
+
+ context 'when job does not start environment' do
+ where(:action) do
+ %w[stop prepare verify access]
+ end
+
+ with_them do
+ let!(:job) do
+ create(factory_type, environment: 'production', project: project,
+ options: { environment: { name: 'production', action: action } }) # rubocop:disable Layout/ArgumentAlignment
+ end
+
+ it 'returns nothing' do
+ is_expected.to be_nil
+ end
+ end
+ end
+
+ context 'when job does not have environment attribute' do
+ let!(:job) { create(factory_type, project: project) }
+
+ it 'returns nothing' do
+ is_expected.to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/environments/create_for_job_shared_examples.rb b/spec/support/shared_examples/environments/create_for_job_shared_examples.rb
new file mode 100644
index 00000000000..3acdc8c142f
--- /dev/null
+++ b/spec/support/shared_examples/environments/create_for_job_shared_examples.rb
@@ -0,0 +1,299 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'create environment for job' do
+ let!(:job) { build(factory_type, project: project, pipeline: pipeline, **attributes) }
+ let(:merge_request) {} # rubocop:disable Lint/EmptyBlock
+
+ describe '#execute' do
+ subject { service.execute(job) }
+
+ shared_examples_for 'returning a correct environment' do
+ let(:expected_auto_stop_in_seconds) do
+ ChronicDuration.parse(expected_auto_stop_in).seconds if expected_auto_stop_in
+ end
+
+ it 'returns a persisted environment object' do
+ freeze_time do
+ expect { subject }.to change { Environment.count }.by(1)
+
+ expect(subject).to be_a(Environment)
+ expect(subject).to be_persisted
+ expect(subject.project).to eq(project)
+ expect(subject.name).to eq(expected_environment_name)
+ expect(subject.auto_stop_in).to eq(expected_auto_stop_in_seconds)
+ end
+ end
+
+ context 'when environment has already existed' do
+ let!(:environment) do
+ create(:environment,
+ project: project,
+ name: expected_environment_name
+ ).tap do |env|
+ env.auto_stop_in = expected_auto_stop_in
+ end
+ end
+
+ it 'returns the existing environment object' do
+ expect { subject }.not_to change { Environment.count }
+ expect { subject }.not_to change { environment.auto_stop_at }
+
+ expect(subject).to be_persisted
+ expect(subject).to eq(environment)
+ end
+ end
+ end
+
+ context 'when job has environment name attribute' do
+ let(:environment_name) { 'production' }
+ let(:expected_environment_name) { 'production' }
+ let(:expected_auto_stop_in) { nil }
+
+ let(:attributes) do
+ {
+ environment: environment_name,
+ options: { environment: { name: environment_name } }
+ }
+ end
+
+ it_behaves_like 'returning a correct environment'
+
+ context 'and job environment also has an auto_stop_in attribute' do
+ let(:environment_auto_stop_in) { '5 minutes' }
+ let(:expected_auto_stop_in) { '5 minutes' }
+
+ let(:attributes) do
+ {
+ environment: environment_name,
+ options: {
+ environment: {
+ name: environment_name,
+ auto_stop_in: environment_auto_stop_in
+ }
+ }
+ }
+ end
+
+ it_behaves_like 'returning a correct environment'
+ end
+
+ context 'and job environment has an auto_stop_in variable attribute' do
+ let(:environment_auto_stop_in) { '10 minutes' }
+ let(:expected_auto_stop_in) { '10 minutes' }
+
+ let(:attributes) do
+ {
+ environment: environment_name,
+ options: {
+ environment: {
+ name: environment_name,
+ auto_stop_in: '$TTL'
+ }
+ },
+ yaml_variables: [
+ { key: "TTL", value: environment_auto_stop_in, public: true }
+ ]
+ }
+ end
+
+ it_behaves_like 'returning a correct environment'
+ end
+ end
+
+ context 'when job has deployment tier attribute' do
+ let(:attributes) do
+ {
+ environment: 'customer-portal',
+ options: {
+ environment: {
+ name: 'customer-portal',
+ deployment_tier: deployment_tier
+ }
+ }
+ }
+ end
+
+ let(:deployment_tier) { 'production' }
+
+ context 'when environment has not been created yet' do
+ it 'sets the specified deployment tier' do
+ is_expected.to be_production
+ end
+
+ context 'when deployment tier is staging' do
+ let(:deployment_tier) { 'staging' }
+
+ it 'sets the specified deployment tier' do
+ is_expected.to be_staging
+ end
+ end
+
+ context 'when deployment tier is unknown' do
+ let(:deployment_tier) { 'unknown' }
+
+ it 'raises an error' do
+ expect { subject }.to raise_error(ArgumentError, "'unknown' is not a valid tier")
+ end
+ end
+ end
+
+ context 'when environment has already been created' do
+ before do
+ create(:environment, project: project, name: 'customer-portal', tier: :staging)
+ end
+
+ it 'does not overwrite the specified deployment tier' do
+ # This is to be updated when a deployment succeeded i.e. Deployments::UpdateEnvironmentService.
+ is_expected.to be_staging
+ end
+ end
+ end
+
+ context 'when job starts a review app' do
+ let(:environment_name) { 'review/$CI_COMMIT_REF_NAME' }
+ let(:expected_environment_name) { "review/#{job.ref}" }
+ let(:expected_auto_stop_in) { nil }
+
+ let(:attributes) do
+ {
+ environment: environment_name,
+ options: { environment: { name: environment_name } }
+ }
+ end
+
+ it_behaves_like 'returning a correct environment'
+ end
+
+ context 'when job stops a review app' do
+ let(:environment_name) { 'review/$CI_COMMIT_REF_NAME' }
+ let(:expected_environment_name) { "review/#{job.ref}" }
+ let(:expected_auto_stop_in) { nil }
+
+ let(:attributes) do
+ {
+ environment: environment_name,
+ options: { environment: { name: environment_name, action: 'stop' } }
+ }
+ end
+
+ it_behaves_like 'returning a correct environment'
+ end
+
+ context 'when merge_request is provided' do
+ let(:pipeline) { create(:ci_pipeline, project: project, merge_request: merge_request) }
+ let(:environment_name) { 'development' }
+ let(:attributes) { { environment: environment_name, options: { environment: { name: environment_name } } } }
+ let(:merge_request) { create(:merge_request, source_project: project) }
+ let(:seed) { described_class.new(job) }
+
+ context 'and environment does not exist' do
+ let(:environment_name) { 'review/$CI_COMMIT_REF_NAME' }
+
+ it 'creates an environment associated with the merge request' do
+ expect { subject }.to change { Environment.count }.by(1)
+
+ expect(subject.merge_request).to eq(merge_request)
+ end
+ end
+
+ context 'and environment already exists' do
+ before do
+ create(:environment, project: project, name: environment_name)
+ end
+
+ it 'does not change the merge request associated with the environment' do
+ expect { subject }.not_to change { Environment.count }
+
+ expect(subject.merge_request).to be_nil
+ end
+ end
+ end
+
+ context 'when a pipeline contains a deployment job' do
+ let(:pipeline) { create(:ci_pipeline, project: project, merge_request: merge_request) }
+ let!(:job) { build(factory_type, :start_review_app, project: project, pipeline: pipeline) }
+
+ context 'and the environment does not exist' do
+ it 'creates the environment specified by the job' do
+ expect { subject }.to change { Environment.count }.by(1)
+
+ expect(environment).to be_present
+ expect(job.persisted_environment.name).to eq('review/master')
+ expect(job.metadata.expanded_environment_name).to eq('review/master')
+ end
+
+ context 'and the pipeline is for a merge request' do
+ let(:merge_request) { create(:merge_request, source_project: project) }
+
+ it 'associates the environment with the merge request' do
+ expect { subject }.to change { Environment.count }.by(1)
+
+ expect(environment.merge_request).to eq(merge_request)
+ end
+ end
+ end
+
+ context 'when an environment already exists' do
+ before do
+ create(:environment, project: project, name: 'review/master')
+ end
+
+ it 'ensures environment existence for the job' do
+ expect { subject }.not_to change { Environment.count }
+
+ expect(environment).to be_present
+ expect(job.persisted_environment.name).to eq('review/master')
+ expect(job.metadata.expanded_environment_name).to eq('review/master')
+ end
+
+ context 'and the pipeline is for a merge request' do
+ let(:merge_request) { create(:merge_request, source_project: project) }
+
+ it 'does not associate the environment with the merge request' do
+ expect { subject }.not_to change { Environment.count }
+
+ expect(environment.merge_request).to be_nil
+ end
+ end
+ end
+
+ context 'when an environment name contains an invalid character' do
+ before do
+ job.pipeline = build(:ci_pipeline, ref: '!!!', project: project)
+ end
+
+ it 'sets the failure status' do
+ expect { subject }.not_to change { Environment.count }
+
+ expect(job).to be_failed
+ expect(job).to be_environment_creation_failure
+ expect(job.persisted_environment).to be_nil
+ end
+ end
+ end
+
+ context 'when a pipeline contains a teardown job' do
+ let!(:job) { build(factory_type, :stop_review_app, project: project) }
+
+ it 'ensures environment existence for the job' do
+ expect { subject }.to change { Environment.count }.by(1)
+
+ expect(environment).to be_present
+ expect(job.persisted_environment.name).to eq('review/master')
+ expect(job.metadata.expanded_environment_name).to eq('review/master')
+ end
+ end
+
+ context 'when a pipeline does not contain a deployment job' do
+ let!(:job) { build(factory_type, project: project) }
+
+ it 'does not create any environments' do
+ expect { subject }.not_to change { Environment.count }
+ end
+ end
+
+ def environment
+ project.environments.find_by_name('review/master')
+ end
+ end
+end
diff --git a/spec/support/shared_examples/features/access_tokens_shared_examples.rb b/spec/support/shared_examples/features/access_tokens_shared_examples.rb
index 3c78869ffaa..34e3ba95b0d 100644
--- a/spec/support/shared_examples/features/access_tokens_shared_examples.rb
+++ b/spec/support/shared_examples/features/access_tokens_shared_examples.rb
@@ -15,6 +15,8 @@ RSpec.shared_examples 'resource access tokens creation' do |resource_type|
name = 'My access token'
visit resource_settings_access_tokens_path
+
+ click_button 'Add new token'
fill_in 'Token name', with: name
# Set date to 1st of next month
diff --git a/spec/support/shared_examples/features/content_editor_shared_examples.rb b/spec/support/shared_examples/features/content_editor_shared_examples.rb
index 254bc3c83ac..fff8ef915eb 100644
--- a/spec/support/shared_examples/features/content_editor_shared_examples.rb
+++ b/spec/support/shared_examples/features/content_editor_shared_examples.rb
@@ -27,6 +27,19 @@ RSpec.shared_examples 'edits content using the content editor' do |params = { wi
expect(page).to have_text('Typing text in the content editor')
end
+ it 'autofocuses the rich text editor when switching to rich text' do
+ switch_to_content_editor
+
+ expect(page).to have_css("#{content_editor_testid}:focus")
+ end
+
+ it 'autofocuses the plain text editor when switching back to markdown' do
+ switch_to_content_editor
+ switch_to_markdown_editor
+
+ expect(page).to have_css("textarea:focus")
+ end
+
describe 'creating and editing links' do
before do
switch_to_content_editor
diff --git a/spec/support/shared_examples/features/deploy_token_shared_examples.rb b/spec/support/shared_examples/features/deploy_token_shared_examples.rb
index 80f5f1d805c..b621a0c8cca 100644
--- a/spec/support/shared_examples/features/deploy_token_shared_examples.rb
+++ b/spec/support/shared_examples/features/deploy_token_shared_examples.rb
@@ -6,7 +6,7 @@ RSpec.shared_examples 'a deploy token in settings' do
visit page_path
- within('.deploy-tokens') do
+ within('#js-deploy-tokens') do
expect(page).to have_content(deploy_token.name)
expect(page).to have_content('read_repository')
expect(page).to have_content('read_registry')
@@ -16,6 +16,7 @@ RSpec.shared_examples 'a deploy token in settings' do
it 'add a new deploy token', :js do
visit page_path
+ click_button "Add token"
within('#js-deploy-tokens') do
fill_in _('Name'), with: 'new_deploy_key'
@@ -28,7 +29,7 @@ RSpec.shared_examples 'a deploy token in settings' do
expect(page).to have_content("Your new #{entity_type} deploy token has been created")
- within('.created-deploy-token-container') do
+ within('#new-deploy-token-alert') do
expect(find("input[name='deploy-token-user']").value).to eq("deployer")
expect(find("input[name='deploy-token'][readonly='readonly']")).to be_visible
end
@@ -40,6 +41,7 @@ RSpec.shared_examples 'a deploy token in settings' do
context "with form errors", :js do
before do
visit page_path
+ click_button "Add token"
fill_in _('Name'), with: "new_deploy_key"
fill_in _('Username (optional)'), with: "deployer"
click_button "Create deploy token"
@@ -63,7 +65,7 @@ RSpec.shared_examples 'a deploy token in settings' do
it 'shows absolute times for expires_at' do
visit page_path
- within('.deploy-tokens') do
+ within('#js-deploy-tokens') do
expect(page).to have_content(deploy_token.expires_at.strftime('%b %-d'))
end
end
diff --git a/spec/support/shared_examples/features/discussion_comments_shared_example.rb b/spec/support/shared_examples/features/discussion_comments_shared_example.rb
index 430a8ac39d7..82bddb9f5a4 100644
--- a/spec/support/shared_examples/features/discussion_comments_shared_example.rb
+++ b/spec/support/shared_examples/features/discussion_comments_shared_example.rb
@@ -278,9 +278,7 @@ RSpec.shared_examples 'thread comments for issue, epic and merge request' do |re
expect(page).to have_css('.discussion-notes .note', count: 1)
expect(page).to have_content '1 reply'
end
- end
- if resource_name == 'merge request'
let(:note_id) { find("#{comments_selector} .note:first-child", match: :first)['data-note-id'] }
let(:reply_id) { all("#{comments_selector} [data-note-id]")[1]['data-note-id'] }
diff --git a/spec/support/shared_examples/features/editable_merge_request_shared_examples.rb b/spec/support/shared_examples/features/editable_merge_request_shared_examples.rb
index f802404518b..9f884683f47 100644
--- a/spec/support/shared_examples/features/editable_merge_request_shared_examples.rb
+++ b/spec/support/shared_examples/features/editable_merge_request_shared_examples.rb
@@ -125,11 +125,6 @@ RSpec.shared_examples 'an editable merge request' do
it 'allows to unselect "Remove source branch"', :js do
expect(merge_request.merge_params['force_remove_source_branch']).to be_truthy
- begin
- visit edit_project_merge_request_path(target_project, merge_request)
- rescue Selenium::WebDriver::Error::UnexpectedAlertOpenError
- end
-
uncheck 'Delete source branch when merge request is accepted'
click_button 'Save changes'
diff --git a/spec/support/shared_examples/features/manage_applications_shared_examples.rb b/spec/support/shared_examples/features/manage_applications_shared_examples.rb
index b8fd58e7efa..05b1c991cdb 100644
--- a/spec/support/shared_examples/features/manage_applications_shared_examples.rb
+++ b/spec/support/shared_examples/features/manage_applications_shared_examples.rb
@@ -10,6 +10,8 @@ RSpec.shared_examples 'manage applications' do
expect(page).to have_content 'Add new application'
+ click_button 'Add new application' if page.has_css?('.gl-new-card-header')
+
fill_in :doorkeeper_application_name, with: application_name
fill_in :doorkeeper_application_redirect_uri, with: application_redirect_uri
check :doorkeeper_application_scopes_read_user
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 2d3f1949716..fb882ef8a23 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
@@ -7,6 +7,7 @@ RSpec.shared_examples "protected branches > access control > CE" do
it "allows creating protected branches that #{access_type_name} can push to" do
visit project_protected_branches_path(project)
+ show_add_form
set_protected_branch_name('master')
set_allowed_to('merge', no_one)
set_allowed_to('push', access_type_name)
@@ -19,6 +20,7 @@ RSpec.shared_examples "protected branches > access control > CE" do
it "allows creating protected branches that #{access_type_name} can merge to" do
visit project_protected_branches_path(project)
+ show_add_form
set_protected_branch_name('master')
set_allowed_to('merge', access_type_name)
set_allowed_to('push', no_one)
@@ -31,6 +33,7 @@ RSpec.shared_examples "protected branches > access control > CE" do
it "allows updating protected branches so that #{access_type_name} can push to them" do
visit project_protected_branches_path(project)
+ show_add_form
set_protected_branch_name('master')
set_allowed_to('merge', no_one)
set_allowed_to('push', no_one)
@@ -52,6 +55,7 @@ RSpec.shared_examples "protected branches > access control > CE" do
it "allows updating protected branches so that #{access_type_name} can merge to them" do
visit project_protected_branches_path(project)
+ show_add_form
set_protected_branch_name('master')
set_allowed_to('merge', no_one)
set_allowed_to('push', no_one)
diff --git a/spec/support/shared_examples/features/protected_branches_with_deploy_keys_examples.rb b/spec/support/shared_examples/features/protected_branches_with_deploy_keys_examples.rb
index 90b0e600228..a15ee47de34 100644
--- a/spec/support/shared_examples/features/protected_branches_with_deploy_keys_examples.rb
+++ b/spec/support/shared_examples/features/protected_branches_with_deploy_keys_examples.rb
@@ -20,6 +20,7 @@ RSpec.shared_examples 'Deploy keys with protected branches' do
it "shows all dropdown sections in the 'Allowed to push' main dropdown, with only one deploy key" do
visit project_protected_branches_path(project)
+ click_button 'Add protected branch'
find(".js-allowed-to-push").click
wait_for_requests
@@ -35,6 +36,7 @@ RSpec.shared_examples 'Deploy keys with protected branches' do
it "shows all sections but not deploy keys in the 'Allowed to merge' main dropdown" do
visit project_protected_branches_path(project)
+ click_button 'Add protected branch'
find(".js-allowed-to-merge").click
wait_for_requests
@@ -65,6 +67,7 @@ RSpec.shared_examples 'Deploy keys with protected branches' do
it "just shows all sections but not deploy keys in the 'Allowed to push' dropdown" do
visit project_protected_branches_path(project)
+ click_button 'Add protected branch'
find(".js-allowed-to-push").click
wait_for_requests
diff --git a/spec/support/shared_examples/features/protected_tags_with_deploy_keys_examples.rb b/spec/support/shared_examples/features/protected_tags_with_deploy_keys_examples.rb
index cc0984b6226..703ba5b018a 100644
--- a/spec/support/shared_examples/features/protected_tags_with_deploy_keys_examples.rb
+++ b/spec/support/shared_examples/features/protected_tags_with_deploy_keys_examples.rb
@@ -14,6 +14,7 @@ RSpec.shared_examples 'Deploy keys with protected tags' do
it "shows all dropdown sections in the 'Allowed to create' main dropdown, with only one deploy key" do
visit project_protected_tags_path(project)
+ click_button('Add tag')
find(".js-allowed-to-create").click
wait_for_requests
@@ -31,6 +32,7 @@ RSpec.shared_examples 'Deploy keys with protected tags' do
create(:protected_tag, :no_one_can_create, project: project, name: 'v1.0.0')
visit project_protected_tags_path(project)
+ click_button('Add tag')
within(".js-protected-tag-edit-form") do
find(".js-allowed-to-create").click
@@ -46,6 +48,7 @@ RSpec.shared_examples 'Deploy keys with protected tags' do
context 'when no deploy key can push' do
it "just shows all sections but not deploy keys in the 'Allowed to create' dropdown" do
visit project_protected_tags_path(project)
+ click_button('Add tag')
find(".js-allowed-to-create").click
wait_for_requests
diff --git a/spec/support/shared_examples/features/runners_shared_examples.rb b/spec/support/shared_examples/features/runners_shared_examples.rb
index 54a4db0e81d..0c043f48c5f 100644
--- a/spec/support/shared_examples/features/runners_shared_examples.rb
+++ b/spec/support/shared_examples/features/runners_shared_examples.rb
@@ -57,7 +57,7 @@ RSpec.shared_examples 'shows and resets runner registration token' do
click_on dropdown_text
click_on 'Click to reveal'
- expect(old_registration_token).not_to eq registration_token
+ expect(find_field('token-value').value).not_to eq old_registration_token
end
end
end
diff --git a/spec/support/shared_examples/features/sidebar_shared_examples.rb b/spec/support/shared_examples/features/sidebar_shared_examples.rb
index c2c50e8762f..f402a1bc91a 100644
--- a/spec/support/shared_examples/features/sidebar_shared_examples.rb
+++ b/spec/support/shared_examples/features/sidebar_shared_examples.rb
@@ -100,7 +100,7 @@ RSpec.shared_examples 'issue boards sidebar' do
context 'when notifications have been disabled' do
before do
- project.update_attribute(:emails_disabled, true)
+ project.update_attribute(:emails_enabled, false)
refresh_and_click_first_card
end
diff --git a/spec/support/shared_examples/features/variable_list_drawer_shared_examples.rb b/spec/support/shared_examples/features/variable_list_drawer_shared_examples.rb
new file mode 100644
index 00000000000..9f01c69608d
--- /dev/null
+++ b/spec/support/shared_examples/features/variable_list_drawer_shared_examples.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'variable list drawer' do
+ it 'adds a new CI variable' do
+ click_button('Add variable')
+
+ # For now, we just check that the drawer is displayed
+ expect(page).to have_selector('[data-testid="ci-variable-drawer"]')
+
+ # TODO: Add tests for ADDING a variable via drawer when feature is available
+ end
+
+ it 'edits a variable' do
+ page.within('[data-testid="ci-variable-table"]') do
+ click_button('Edit')
+ end
+
+ # For now, we just check that the drawer is displayed
+ expect(page).to have_selector('[data-testid="ci-variable-drawer"]')
+
+ # TODO: Add tests for EDITING a variable via drawer when feature is available
+ end
+end
diff --git a/spec/support/shared_examples/features/work_items_shared_examples.rb b/spec/support/shared_examples/features/work_items_shared_examples.rb
index 4c15b682458..d3863c9a675 100644
--- a/spec/support/shared_examples/features/work_items_shared_examples.rb
+++ b/spec/support/shared_examples/features/work_items_shared_examples.rb
@@ -15,17 +15,17 @@ RSpec.shared_examples 'work items title' do
end
end
-RSpec.shared_examples 'work items status' do
- let(:state_selector) { '[data-testid="work-item-state-select"]' }
+RSpec.shared_examples 'work items toggle status button' do
+ let(:state_button) { '[data-testid="work-item-state-toggle"]' }
it 'successfully shows and changes the status of the work item' do
- expect(find(state_selector)).to have_content 'Open'
+ expect(find(state_button, match: :first)).to have_content 'Close'
- find(state_selector).select("Closed")
+ find(state_button, match: :first).click
wait_for_requests
- expect(find(state_selector)).to have_content 'Closed'
+ expect(find(state_button, match: :first)).to have_content 'Reopen'
expect(work_item.reload.state).to eq('closed')
end
end
@@ -316,7 +316,7 @@ end
RSpec.shared_examples 'work items notifications' do
let(:actions_dropdown_selector) { '[data-testid="work-item-actions-dropdown"]' }
- let(:notifications_toggle_selector) { '[data-testid="notifications-toggle-action"] > button' }
+ let(:notifications_toggle_selector) { '[data-testid="notifications-toggle-action"] button[role="switch"]' }
it 'displays toast when notification is toggled' do
find(actions_dropdown_selector).click
diff --git a/spec/support/shared_examples/finders/issues_finder_shared_examples.rb b/spec/support/shared_examples/finders/issues_finder_shared_examples.rb
index 30041456d00..19001abcbe2 100644
--- a/spec/support/shared_examples/finders/issues_finder_shared_examples.rb
+++ b/spec/support/shared_examples/finders/issues_finder_shared_examples.rb
@@ -22,6 +22,14 @@ RSpec.shared_examples 'issues or work items finder' do |factory, execute_context
it 'returns no items' do
expect(items).to be_empty
end
+
+ context 'when there are group-level work items' do
+ let!(:group_work_item) { create(:work_item, namespace: create(:group)) }
+
+ it 'returns no items' do
+ expect(items).to be_empty
+ end
+ end
end
context 'when filtering by group id' do
diff --git a/spec/support/shared_examples/graphql/mutations/update_time_estimate_shared_examples.rb b/spec/support/shared_examples/graphql/mutations/update_time_estimate_shared_examples.rb
new file mode 100644
index 00000000000..d6d360bb413
--- /dev/null
+++ b/spec/support/shared_examples/graphql/mutations/update_time_estimate_shared_examples.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'updating time estimate' do
+ context 'when setting time estimate', :aggregate_failures do
+ using RSpec::Parameterized::TableSyntax
+
+ let(:input_params) { input.merge(extra_params).merge({ timeEstimate: time_estimate }) }
+
+ context 'when time estimate is not a valid numerical value' do
+ let(:time_estimate) { '-3.5d' }
+
+ it 'does not update' do
+ expect { post_graphql_mutation(mutation, current_user: current_user) }.not_to change { resource.time_estimate }
+ end
+
+ it 'returns error' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(graphql_errors).to include(a_hash_including('message' => /must be greater than or equal to zero/))
+ end
+ end
+
+ context 'when time estimate is not a number' do
+ let(:time_estimate) { 'nonsense' }
+
+ it 'does not update' do
+ expect { post_graphql_mutation(mutation, current_user: current_user) }.not_to change { resource.time_estimate }
+ end
+
+ it 'returns error' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(graphql_errors).to include(a_hash_including('message' => /must be formatted correctly/))
+ end
+ end
+
+ context 'when time estimate is valid' do
+ let(:time_estimate) { "1h" }
+
+ before do
+ post_graphql_mutation(mutation, current_user: current_user)
+ end
+
+ it_behaves_like 'a working GraphQL mutation'
+
+ where(:time_estimate, :value) do
+ '1h' | 3600
+ '0h' | 0
+ '-0h' | 0
+ end
+
+ with_them do
+ specify do
+ expect(graphql_data_at(mutation_name, resource.class.to_s.underscore, 'timeEstimate')).to eq(value)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/graphql/notes_quick_actions_for_work_items_shared_examples.rb b/spec/support/shared_examples/graphql/notes_quick_actions_for_work_items_shared_examples.rb
index c666b72d492..0577ac329e6 100644
--- a/spec/support/shared_examples/graphql/notes_quick_actions_for_work_items_shared_examples.rb
+++ b/spec/support/shared_examples/graphql/notes_quick_actions_for_work_items_shared_examples.rb
@@ -163,63 +163,77 @@ RSpec.shared_examples 'work item supports type change via quick actions' do
noteable.update!(work_item_type: task_type)
end
- it 'updates type' do
- expect do
- post_graphql_mutation(mutation, current_user: current_user)
- noteable.reload
- end.to change { noteable.work_item_type.base_type }.from('task').to('issue')
-
- expect(response).to have_gitlab_http_status(:success)
- end
-
- context 'when update service returns errors' do
- let_it_be(:issue) { create(:work_item, :issue, project: project) }
-
- before do
- create(:parent_link, work_item: noteable, work_item_parent: issue)
- end
-
- it 'mutation response include the errors' do
+ shared_examples 'a quick command that changes type' do
+ it 'updates type' do
expect do
post_graphql_mutation(mutation, current_user: current_user)
noteable.reload
- end.not_to change { noteable.work_item_type.base_type }
+ end.to change { noteable.work_item_type.base_type }.from('task').to('issue')
expect(response).to have_gitlab_http_status(:success)
- expect(mutation_response['errors'])
- .to include('Validation Work item type cannot be changed to issue when linked to a parent issue.')
end
- end
- context 'when quick command for unsupported widget is present' do
- let(:body) { "\n/type Issue\n/assign @#{assignee.username}" }
+ context 'when update service returns errors' do
+ let_it_be(:issue) { create(:work_item, :issue, project: project) }
- before do
- WorkItems::Type.default_by_type(:issue).widget_definitions
- .find_by_widget_type(:assignees).update!(disabled: true)
+ before do
+ create(:parent_link, work_item: noteable, work_item_parent: issue)
+ end
+
+ it 'mutation response include the errors' do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+ noteable.reload
+ end.not_to change { noteable.work_item_type.base_type }
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_response['errors'])
+ .to include('Validation Work item type cannot be changed to issue when linked to a parent issue.')
+ end
end
- it 'updates only type' do
- expect do
- post_graphql_mutation(mutation, current_user: current_user)
- noteable.reload
- end.to change { noteable.work_item_type.base_type }.from('task').to('issue')
- .and change { noteable.assignees }.to([])
+ context 'when quick command for unsupported widget is present' do
+ let(:body) { "\n/type Issue\n/assign @#{assignee.username}" }
+
+ before do
+ WorkItems::Type.default_by_type(:issue).widget_definitions
+ .find_by_widget_type(:assignees).update!(disabled: true)
+ end
+
+ it 'updates only type' do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+ noteable.reload
+ end.to change { noteable.work_item_type.base_type }.from('task').to('issue')
+ .and change { noteable.assignees }.to([])
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_response['errors'])
+ .to include("Commands only Type changed successfully. Assigned @#{assignee.username}.")
+ end
+ end
- expect(response).to have_gitlab_http_status(:success)
- expect(mutation_response['errors'])
- .to include("Commands only Type changed successfully. Assigned @#{assignee.username}.")
+ context 'when the type name is upper case' do
+ let(:body) { "Updating type.\n/type Issue" }
+
+ it 'changes type to issue' do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+ noteable.reload
+ end.to change { noteable.work_item_type.base_type }.from('task').to('issue')
+ end
end
end
- context 'when the type name is upper case' do
- let(:body) { "Updating type.\n/type Issue" }
+ context 'with /type quick command' do
+ let(:body) { "Updating type.\n/type issue" }
- it 'changes type to issue' do
- expect do
- post_graphql_mutation(mutation, current_user: current_user)
- noteable.reload
- end.to change { noteable.work_item_type.base_type }.from('task').to('issue')
- end
+ it_behaves_like 'a quick command that changes type'
+ end
+
+ context 'with /promote_to quick command' do
+ let(:body) { "Updating type.\n/promote_to issue" }
+
+ it_behaves_like 'a quick command that changes type'
end
end
diff --git a/spec/support/shared_examples/helpers/runners_shared_examples.rb b/spec/support/shared_examples/helpers/runners_shared_examples.rb
new file mode 100644
index 00000000000..e509f7a65a5
--- /dev/null
+++ b/spec/support/shared_examples/helpers/runners_shared_examples.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'admin_runners_data_attributes contains data' do
+ it 'returns data' do
+ expect(subject).to include(
+ runner_install_help_page: 'https://docs.gitlab.com/runner/install/',
+ registration_token: Gitlab::CurrentSettings.runners_registration_token,
+ online_contact_timeout_secs: 7200,
+ stale_timeout_secs: 7889238
+ )
+ end
+end
diff --git a/spec/support/shared_examples/helpers/super_sidebar_shared_examples.rb b/spec/support/shared_examples/helpers/super_sidebar_shared_examples.rb
new file mode 100644
index 00000000000..9da804b3140
--- /dev/null
+++ b/spec/support/shared_examples/helpers/super_sidebar_shared_examples.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'shared super sidebar context' do
+ it 'returns sidebar values for logged-in users and logged-out users', :use_clean_rails_memory_store_caching do
+ expect(subject).to include({
+ current_menu_items: nil,
+ current_context_header: nil,
+ support_path: helper.support_url,
+ display_whats_new: helper.display_whats_new?,
+ whats_new_most_recent_release_items_count: helper.whats_new_most_recent_release_items_count,
+ whats_new_version_digest: helper.whats_new_version_digest,
+ show_version_check: helper.show_version_check?,
+ gitlab_version: Gitlab.version_info,
+ gitlab_version_check: helper.gitlab_version_check,
+ search: {
+ search_path: search_path,
+ issues_path: issues_dashboard_path,
+ mr_path: merge_requests_dashboard_path,
+ autocomplete_path: search_autocomplete_path,
+ search_context: helper.header_search_context
+ },
+ panel_type: panel_type
+ })
+ end
+end
+
+RSpec.shared_examples 'logged-out super-sidebar context' do
+ subject do
+ helper.super_sidebar_context(nil, group: nil, project: nil, panel: panel, panel_type: panel_type)
+ end
+
+ it_behaves_like 'shared super sidebar context'
+
+ it { is_expected.to include({ is_logged_in: false }) }
+
+ it { expect(subject[:context_switcher_links]).to be_an(Array) }
+end
diff --git a/spec/support/shared_examples/lib/gitlab/cache/json_cache_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/cache/json_cache_shared_examples.rb
index 0472bb87e62..f60974beaf8 100644
--- a/spec/support/shared_examples/lib/gitlab/cache/json_cache_shared_examples.rb
+++ b/spec/support/shared_examples/lib/gitlab/cache/json_cache_shared_examples.rb
@@ -18,7 +18,7 @@ RSpec.shared_examples 'Json Cache class' do
it 'parses the cached value' do
allow(backend).to receive(:read).with(expanded_key).and_return(json_value(broadcast_message))
- expect(cache.read(key, BroadcastMessage)).to eq(broadcast_message)
+ expect(cache.read(key, System::BroadcastMessage)).to eq(broadcast_message)
end
it 'returns nil when klass is nil' do
@@ -30,14 +30,14 @@ RSpec.shared_examples 'Json Cache class' do
it 'gracefully handles an empty hash' do
allow(backend).to receive(:read).with(expanded_key).and_return(json_value({}))
- expect(cache.read(key, BroadcastMessage)).to be_a(BroadcastMessage)
+ expect(cache.read(key, System::BroadcastMessage)).to be_a(System::BroadcastMessage)
end
context 'when the cached value is a JSON true value' do
it 'parses the cached value' do
allow(backend).to receive(:read).with(expanded_key).and_return(json_value(true))
- expect(cache.read(key, BroadcastMessage)).to eq(true)
+ expect(cache.read(key, System::BroadcastMessage)).to eq(true)
end
end
@@ -45,7 +45,7 @@ RSpec.shared_examples 'Json Cache class' do
it 'parses the cached value' do
allow(backend).to receive(:read).with(expanded_key).and_return(json_value(false))
- expect(cache.read(key, BroadcastMessage)).to eq(false)
+ expect(cache.read(key, System::BroadcastMessage)).to eq(false)
end
end
@@ -53,23 +53,23 @@ RSpec.shared_examples 'Json Cache class' do
it 'gracefully handles bad cached entry' do
allow(backend).to receive(:read).with(expanded_key).and_return('{')
- expect(cache.read(key, BroadcastMessage)).to be_nil
+ expect(cache.read(key, System::BroadcastMessage)).to be_nil
end
it 'gracefully handles unknown attributes' do
read_value = json_value(broadcast_message.attributes.merge(unknown_attribute: 1))
allow(backend).to receive(:read).with(expanded_key).and_return(read_value)
- expect(cache.read(key, BroadcastMessage)).to be_nil
+ expect(cache.read(key, System::BroadcastMessage)).to be_nil
end
it 'gracefully handles excluded fields from attributes during serialization' do
read_value = json_value(broadcast_message.attributes.except("message_html"))
allow(backend).to receive(:read).with(expanded_key).and_return(read_value)
- result = cache.read(key, BroadcastMessage)
+ result = cache.read(key, System::BroadcastMessage)
- BroadcastMessage.cached_markdown_fields.html_fields.each do |field|
+ System::BroadcastMessage.cached_markdown_fields.html_fields.each do |field|
expect(result.public_send(field)).to be_nil
end
end
@@ -79,7 +79,7 @@ RSpec.shared_examples 'Json Cache class' do
it 'parses the cached value' do
allow(backend).to receive(:read).with(expanded_key).and_return(json_value([broadcast_message]))
- expect(cache.read(key, BroadcastMessage)).to eq([broadcast_message])
+ expect(cache.read(key, System::BroadcastMessage)).to eq([broadcast_message])
end
it 'returns an empty array when klass is nil' do
@@ -91,20 +91,20 @@ RSpec.shared_examples 'Json Cache class' do
it 'gracefully handles bad cached entry' do
allow(backend).to receive(:read).with(expanded_key).and_return('[')
- expect(cache.read(key, BroadcastMessage)).to be_nil
+ expect(cache.read(key, System::BroadcastMessage)).to be_nil
end
it 'gracefully handles an empty array' do
allow(backend).to receive(:read).with(expanded_key).and_return(json_value([]))
- expect(cache.read(key, BroadcastMessage)).to eq([])
+ expect(cache.read(key, System::BroadcastMessage)).to eq([])
end
it 'gracefully handles items with unknown attributes' do
read_value = json_value([{ unknown_attribute: 1 }, broadcast_message.attributes])
allow(backend).to receive(:read).with(expanded_key).and_return(read_value)
- expect(cache.read(key, BroadcastMessage)).to eq([broadcast_message])
+ expect(cache.read(key, System::BroadcastMessage)).to eq([broadcast_message])
end
end
end
@@ -206,20 +206,20 @@ RSpec.shared_examples 'Json Cache class' do
end
it 'parses the cached value' do
- result = cache.fetch(key, as: BroadcastMessage) { 'block result' }
+ result = cache.fetch(key, as: System::BroadcastMessage) { 'block result' }
expect(result).to eq(broadcast_message)
end
it 'decodes enums correctly' do
- result = cache.fetch(key, as: BroadcastMessage) { 'block result' }
+ result = cache.fetch(key, as: System::BroadcastMessage) { 'block result' }
expect(result.broadcast_type).to eq(broadcast_message.broadcast_type)
end
context 'when the cached value is an instance of ActiveRecord::Base' do
it 'returns a persisted record when id is set' do
- result = cache.fetch(key, as: BroadcastMessage) { 'block result' }
+ result = cache.fetch(key, as: System::BroadcastMessage) { 'block result' }
expect(result).to be_persisted
end
@@ -227,7 +227,7 @@ RSpec.shared_examples 'Json Cache class' do
it 'returns a new record when id is nil' do
backend.write(expanded_key, json_value(build(:broadcast_message)))
- result = cache.fetch(key, as: BroadcastMessage) { 'block result' }
+ result = cache.fetch(key, as: System::BroadcastMessage) { 'block result' }
expect(result).to be_new_record
end
@@ -235,7 +235,7 @@ RSpec.shared_examples 'Json Cache class' do
it 'returns a new record when id is missing' do
backend.write(expanded_key, json_value(build(:broadcast_message).attributes.except('id')))
- result = cache.fetch(key, as: BroadcastMessage) { 'block result' }
+ result = cache.fetch(key, as: System::BroadcastMessage) { 'block result' }
expect(result).to be_new_record
end
@@ -243,7 +243,7 @@ RSpec.shared_examples 'Json Cache class' do
it 'gracefully handles bad cached entry' do
allow(backend).to receive(:read).with(expanded_key).and_return('{')
- result = cache.fetch(key, as: BroadcastMessage) { 'block result' }
+ result = cache.fetch(key, as: System::BroadcastMessage) { 'block result' }
expect(result).to eq 'block result'
end
@@ -251,14 +251,14 @@ RSpec.shared_examples 'Json Cache class' do
it 'gracefully handles an empty hash' do
allow(backend).to receive(:read).with(expanded_key).and_return(json_value({}))
- expect(cache.fetch(key, as: BroadcastMessage)).to be_a(BroadcastMessage)
+ expect(cache.fetch(key, as: System::BroadcastMessage)).to be_a(System::BroadcastMessage)
end
it 'gracefully handles unknown attributes' do
read_value = json_value(broadcast_message.attributes.merge(unknown_attribute: 1))
allow(backend).to receive(:read).with(expanded_key).and_return(read_value)
- result = cache.fetch(key, as: BroadcastMessage) { 'block result' }
+ result = cache.fetch(key, as: System::BroadcastMessage) { 'block result' }
expect(result).to eq 'block result'
end
@@ -267,9 +267,9 @@ RSpec.shared_examples 'Json Cache class' do
read_value = json_value(broadcast_message.attributes.except("message_html"))
allow(backend).to receive(:read).with(expanded_key).and_return(read_value)
- result = cache.fetch(key, as: BroadcastMessage) { 'block result' }
+ result = cache.fetch(key, as: System::BroadcastMessage) { 'block result' }
- BroadcastMessage.cached_markdown_fields.html_fields.each do |field|
+ System::BroadcastMessage.cached_markdown_fields.html_fields.each do |field|
expect(result.public_send(field)).to be_nil
end
end
@@ -294,7 +294,7 @@ RSpec.shared_examples 'Json Cache class' do
end
it 'parses the cached value' do
- result = cache.fetch(key, as: BroadcastMessage) { 'block result' }
+ result = cache.fetch(key, as: System::BroadcastMessage) { 'block result' }
expect(result).to eq([broadcast_message])
end
diff --git a/spec/support/shared_examples/lib/gitlab/search_archived_filter_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/search_archived_filter_shared_examples.rb
index 7bcefd07fc4..6b296d0e78a 100644
--- a/spec/support/shared_examples/lib/gitlab/search_archived_filter_shared_examples.rb
+++ b/spec/support/shared_examples/lib/gitlab/search_archived_filter_shared_examples.rb
@@ -1,30 +1,43 @@
# frozen_string_literal: true
-RSpec.shared_examples 'search results filtered by archived' do
+RSpec.shared_examples 'search results filtered by archived' do |feature_flag_name|
context 'when filter not provided (all behavior)' do
let(:filters) { {} }
- it 'returns unarchived results only', :aggregate_failures do
- expect(results.objects('projects')).to include unarchived_project
- expect(results.objects('projects')).not_to include archived_project
+ it 'returns unarchived results only' do
+ expect(results.objects(scope)).to include unarchived_result
+ expect(results.objects(scope)).not_to include archived_result
end
end
context 'when include_archived is true' do
let(:filters) { { include_archived: true } }
- it 'returns archived and unarchived results', :aggregate_failures do
- expect(results.objects('projects')).to include unarchived_project
- expect(results.objects('projects')).to include archived_project
+ it 'returns archived and unarchived results' do
+ expect(results.objects(scope)).to include unarchived_result
+ expect(results.objects(scope)).to include archived_result
end
end
context 'when include_archived filter is false' do
let(:filters) { { include_archived: false } }
- it 'returns unarchived results only', :aggregate_failures do
- expect(results.objects('projects')).to include unarchived_project
- expect(results.objects('projects')).not_to include archived_project
+ it 'returns unarchived results only' do
+ expect(results.objects(scope)).to include unarchived_result
+ expect(results.objects(scope)).not_to include archived_result
+ end
+ end
+
+ context "when the #{feature_flag_name} feature flag is disabled" do
+ let(:filters) { {} }
+
+ before do
+ stub_feature_flags("#{feature_flag_name}": false)
+ end
+
+ it 'returns archived and unarchived results' do
+ expect(results.objects(scope)).to include unarchived_result
+ expect(results.objects(scope)).to include archived_result
end
end
end
diff --git a/spec/support/shared_examples/metrics/active_record_subscriber_shared_examples.rb b/spec/support/shared_examples/metrics/active_record_subscriber_shared_examples.rb
index dc92e56d013..74fba9416e2 100644
--- a/spec/support/shared_examples/metrics/active_record_subscriber_shared_examples.rb
+++ b/spec/support/shared_examples/metrics/active_record_subscriber_shared_examples.rb
@@ -34,7 +34,7 @@ RSpec.shared_examples 'store ActiveRecord info in RequestStore' do |db_role|
it 'prevents db counters from leaking to the next transaction' do
2.times do
- Gitlab::WithRequestStore.with_request_store do
+ Gitlab::SafeRequestStore.ensure_request_store do
subscriber.sql(event)
expected = case db_role
diff --git a/spec/support/shared_examples/migrations/add_work_item_widget_shared_examples.rb b/spec/support/shared_examples/migrations/add_work_item_widget_shared_examples.rb
index 28eac52256f..fdb31fa5d9d 100644
--- a/spec/support/shared_examples/migrations/add_work_item_widget_shared_examples.rb
+++ b/spec/support/shared_examples/migrations/add_work_item_widget_shared_examples.rb
@@ -3,12 +3,13 @@
RSpec.shared_examples 'migration that adds widget to work items definitions' do |widget_name:|
let(:migration) { described_class.new }
let(:work_item_definitions) { table(:work_item_widget_definitions) }
+ let(:work_item_type_count) { 7 }
describe '#up' do
it "creates widget definition in all types" do
work_item_definitions.where(name: widget_name).delete_all
- expect { migrate! }.to change { work_item_definitions.count }.by(7)
+ expect { migrate! }.to change { work_item_definitions.count }.by(work_item_type_count)
expect(work_item_definitions.all.pluck(:name)).to include(widget_name)
end
@@ -26,7 +27,7 @@ RSpec.shared_examples 'migration that adds widget to work items definitions' do
it "removes definitions for widget" do
migrate!
- expect { migration.down }.to change { work_item_definitions.count }.by(-7)
+ expect { migration.down }.to change { work_item_definitions.count }.by(-work_item_type_count)
expect(work_item_definitions.all.pluck(:name)).not_to include(widget_name)
end
end
diff --git a/spec/support/shared_examples/models/concerns/auto_disabling_hooks_shared_examples.rb b/spec/support/shared_examples/models/concerns/auto_disabling_hooks_shared_examples.rb
index a196b63585c..33b62564e5f 100644
--- a/spec/support/shared_examples/models/concerns/auto_disabling_hooks_shared_examples.rb
+++ b/spec/support/shared_examples/models/concerns/auto_disabling_hooks_shared_examples.rb
@@ -1,6 +1,13 @@
# frozen_string_literal: true
RSpec.shared_examples 'a hook that gets automatically disabled on failure' do
+ let(:logger) { instance_double('Gitlab::WebHooks::Logger') }
+
+ before do
+ allow(hook).to receive(:logger).and_return(logger)
+ allow(logger).to receive(:info)
+ end
+
shared_examples 'is tolerant of invalid records' do
specify do
hook.url = nil
@@ -83,6 +90,20 @@ RSpec.shared_examples 'a hook that gets automatically disabled on failure' do
expect(find_hooks.disabled).to be_empty
end
end
+
+ context 'when silent mode is enabled' do
+ before do
+ stub_application_setting(silent_mode_enabled: true)
+ end
+
+ it 'causes no hooks to be considered executable' do
+ expect(find_hooks.executable).to be_empty
+ end
+
+ it 'causes all hooks to be considered disabled' do
+ expect(find_hooks.disabled.count).to eq(16)
+ end
+ end
end
describe '#executable?', :freeze_time do
@@ -157,6 +178,23 @@ RSpec.shared_examples 'a hook that gets automatically disabled on failure' do
expect { hook.enable! }.to change { hook.executable? }.from(false).to(true)
end
+ it 'logs relevant information' do
+ hook.recent_failures = 1000
+ hook.disabled_until = 1.hour.from_now
+
+ expect(logger)
+ .to receive(:info)
+ .with(a_hash_including(
+ hook_id: hook.id,
+ action: 'enable',
+ recent_failures: 0,
+ disabled_until: nil,
+ backoff_count: 0
+ ))
+
+ hook.enable!
+ end
+
it 'does not update hooks unless necessary' do
hook
@@ -174,11 +212,25 @@ RSpec.shared_examples 'a hook that gets automatically disabled on failure' do
end
end
- describe '#backoff!' do
+ describe '#backoff!', :freeze_time do
context 'when we have not backed off before' do
it 'does not disable the hook' do
expect { hook.backoff! }.not_to change { hook.executable? }.from(true)
end
+
+ it 'increments recent_failures' do
+ expect { hook.backoff! }.to change { hook.recent_failures }.from(0).to(1)
+ end
+
+ it 'logs relevant information' do
+ expect(logger)
+ .to receive(:info)
+ .with(a_hash_including(
+ hook_id: hook.id, action: 'backoff', recent_failures: 1
+ ))
+
+ hook.backoff!
+ end
end
context 'when we have exhausted the grace period' do
@@ -186,6 +238,32 @@ RSpec.shared_examples 'a hook that gets automatically disabled on failure' do
hook.update!(recent_failures: WebHooks::AutoDisabling::FAILURE_THRESHOLD)
end
+ it 'disables the hook' do
+ expect { hook.backoff! }.to change { hook.executable? }.from(true).to(false)
+ end
+
+ it 'increments backoff_count' do
+ expect { hook.backoff! }.to change { hook.backoff_count }.from(0).to(1)
+ end
+
+ it 'sets disabled_until' do
+ expect { hook.backoff! }.to change { hook.disabled_until }.from(nil).to(1.minute.from_now)
+ end
+
+ it 'logs relevant information' do
+ expect(logger)
+ .to receive(:info)
+ .with(a_hash_including(
+ hook_id: hook.id,
+ action: 'backoff',
+ recent_failures: WebHooks::AutoDisabling::FAILURE_THRESHOLD + 1,
+ disabled_until: 1.minute.from_now,
+ backoff_count: 1
+ ))
+
+ hook.backoff!
+ end
+
context 'when the hook is permanently disabled' do
before do
allow(hook).to receive(:permanently_disabled?).and_return(true)
@@ -204,15 +282,15 @@ RSpec.shared_examples 'a hook that gets automatically disabled on failure' do
def run_expectation
expect { hook.backoff! }.to change { hook.backoff_count }.by(1)
end
+ end
- context 'when the flag is disabled' do
- before do
- stub_feature_flags(auto_disabling_web_hooks: false)
- end
+ context 'when the flag is disabled' do
+ before do
+ stub_feature_flags(auto_disabling_web_hooks: false)
+ end
- it 'does not increment backoff count' do
- expect { hook.failed! }.not_to change { hook.backoff_count }
- end
+ it 'does not increment backoff count' do
+ expect { hook.failed! }.not_to change { hook.backoff_count }
end
end
end
@@ -236,36 +314,6 @@ RSpec.shared_examples 'a hook that gets automatically disabled on failure' do
end
end
- describe '#disable!' do
- it 'disables a hook' do
- expect { hook.disable! }.to change { hook.executable? }.from(true).to(false)
- end
-
- context 'when the flag is disabled' do
- before do
- stub_feature_flags(auto_disabling_web_hooks: false)
- end
-
- it 'does not disable the hook' do
- expect { hook.disable! }.not_to change { hook.executable? }
- end
- end
-
- it 'does nothing if the hook is already disabled' do
- allow(hook).to receive(:permanently_disabled?).and_return(true)
-
- sql_count = ActiveRecord::QueryRecorder.new { hook.disable! }.count
-
- expect(sql_count).to eq(0)
- end
-
- include_examples 'is tolerant of invalid records' do
- def run_expectation
- expect { hook.disable! }.to change { hook.executable? }.from(true).to(false)
- end
- end
- end
-
describe '#temporarily_disabled?' do
it 'is false when not temporarily disabled' do
expect(hook).not_to be_temporarily_disabled
@@ -310,7 +358,7 @@ RSpec.shared_examples 'a hook that gets automatically disabled on failure' do
context 'when hook has been disabled' do
before do
- hook.disable!
+ hook.update!(recent_failures: WebHooks::AutoDisabling::EXCEEDED_FAILURE_THRESHOLD)
end
it 'is true' do
@@ -336,7 +384,7 @@ RSpec.shared_examples 'a hook that gets automatically disabled on failure' do
context 'when hook has been disabled' do
before do
- hook.disable!
+ hook.update!(recent_failures: WebHooks::AutoDisabling::EXCEEDED_FAILURE_THRESHOLD)
end
it { is_expected.to eq :disabled }
@@ -352,7 +400,7 @@ RSpec.shared_examples 'a hook that gets automatically disabled on failure' do
context 'when hook has been backed off' do
before do
- hook.update!(recent_failures: WebHooks::AutoDisabling::FAILURE_THRESHOLD + 1)
+ hook.update!(recent_failures: WebHooks::AutoDisabling::EXCEEDED_FAILURE_THRESHOLD)
hook.disabled_until = 1.hour.from_now
end
diff --git a/spec/support/shared_examples/models/concerns/has_repository_shared_examples.rb b/spec/support/shared_examples/models/concerns/has_repository_shared_examples.rb
index 0a07c9d677b..187c0b3ab43 100644
--- a/spec/support/shared_examples/models/concerns/has_repository_shared_examples.rb
+++ b/spec/support/shared_examples/models/concerns/has_repository_shared_examples.rb
@@ -162,10 +162,24 @@ RSpec.shared_examples 'model with repository' do
end
describe '#after_repository_change_head' do
+ let(:event) { instance_double('Repositories::DefaultBranchChangedEvent') }
+ let(:event_data) { { container_id: stubbed_container.id, container_type: stubbed_container.class.name } }
+
it 'calls #reload_default_branch' do
expect(stubbed_container).to receive(:reload_default_branch)
stubbed_container.after_repository_change_head
end
+
+ it 'publishes an Repositories::DefaultBranchChangedEvent event' do
+ allow(Repositories::DefaultBranchChangedEvent)
+ .to receive(:new)
+ .with(data: event_data)
+ .and_return(event)
+
+ expect(Gitlab::EventStore).to receive(:publish).with(event).once
+
+ stubbed_container.after_repository_change_head
+ end
end
end
diff --git a/spec/support/shared_examples/models/concerns/integrations/slack_mattermost_notifier_shared_examples.rb b/spec/support/shared_examples/models/concerns/integrations/slack_mattermost_notifier_shared_examples.rb
index 28d2d4f1597..2985763426f 100644
--- a/spec/support/shared_examples/models/concerns/integrations/slack_mattermost_notifier_shared_examples.rb
+++ b/spec/support/shared_examples/models/concerns/integrations/slack_mattermost_notifier_shared_examples.rb
@@ -327,7 +327,7 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |integration_name
end
context 'on a protected branch' do
- before(:all) do
+ before_all do
create(:protected_branch, :create_branch_on_repository, project: project, name: 'a-protected-branch')
end
@@ -369,7 +369,7 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |integration_name
end
context 'on a protected branch with protected branches defined using wildcards' do
- before(:all) do
+ before_all do
create(:protected_branch, :create_branch_on_repository, repository_branch_name: '1-stable', project: project, name: '*-stable')
end
@@ -578,7 +578,7 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |integration_name
end
context 'on a protected branch' do
- before(:all) do
+ before_all do
create(:protected_branch, :create_branch_on_repository, project: project, name: 'a-protected-branch')
end
@@ -606,7 +606,7 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |integration_name
end
context 'on a protected branch with protected branches defined usin wildcards' do
- before(:all) do
+ before_all do
create(:protected_branch, :create_branch_on_repository, repository_branch_name: '1-stable', project: project, name: '*-stable')
end
@@ -682,7 +682,7 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |integration_name
it_behaves_like "triggered #{integration_name} integration", event_type: "deployment"
context 'on a protected branch' do
- before(:all) do
+ before_all do
create(:protected_branch, :create_branch_on_repository, project: project, name: 'a-protected-branch')
end
diff --git a/spec/support/shared_examples/models/concerns/linkable_items_shared_examples.rb b/spec/support/shared_examples/models/concerns/linkable_items_shared_examples.rb
new file mode 100644
index 00000000000..efd27a051fe
--- /dev/null
+++ b/spec/support/shared_examples/models/concerns/linkable_items_shared_examples.rb
@@ -0,0 +1,82 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'includes LinkableItem concern' do
+ describe 'validation' do
+ let_it_be(:task) { create(:work_item, :task, project: project) }
+ let_it_be(:issue) { create(:work_item, :issue, project: project) }
+
+ subject(:link) { build(link_factory, source_id: source.id, target_id: target.id) }
+
+ describe '#check_existing_parent_link' do
+ shared_examples 'invalid due to existing link' do
+ it do
+ is_expected.to be_invalid
+ expect(link.errors.messages[:source]).to include("is a parent or child of this #{item_type}")
+ end
+ end
+
+ context 'without existing link parent' do
+ let(:source) { issue }
+ let(:target) { task }
+
+ it 'is valid' do
+ is_expected.to be_valid
+ expect(link.errors).to be_empty
+ end
+ end
+
+ context 'with existing link parent' do
+ let_it_be(:relationship) { create(:parent_link, work_item_parent: issue, work_item: task) }
+
+ it_behaves_like 'invalid due to existing link' do
+ let(:source) { issue }
+ let(:target) { task }
+ end
+
+ it_behaves_like 'invalid due to existing link' do
+ let(:source) { task }
+ let(:target) { issue }
+ end
+ end
+ end
+ end
+
+ describe 'Scopes' do
+ describe '.for_source' do
+ it 'includes linked items for source' do
+ source = item
+ link_1 = create(link_factory, source: source, target: item1)
+ link_2 = create(link_factory, source: source, target: item2)
+
+ result = described_class.for_source(source)
+
+ expect(result).to contain_exactly(link_1, link_2)
+ end
+ end
+
+ describe '.for_target' do
+ it 'includes linked items for target' do
+ target = item
+ link_1 = create(link_factory, source: item1, target: target)
+ link_2 = create(link_factory, source: item2, target: target)
+
+ result = described_class.for_target(target)
+
+ expect(result).to contain_exactly(link_1, link_2)
+ end
+ end
+
+ describe '.for_items' do
+ let_it_be(:source_link) { create(link_factory, source: item, target: item1) }
+ let_it_be(:target_link) { create(link_factory, source: item2, target: item) }
+
+ it 'includes links when item is source' do
+ expect(described_class.for_items(item, item1)).to contain_exactly(source_link)
+ end
+
+ it 'includes links when item is target' do
+ expect(described_class.for_items(item, item2)).to contain_exactly(target_link)
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/models/concerns/unstoppable_hooks_shared_examples.rb b/spec/support/shared_examples/models/concerns/unstoppable_hooks_shared_examples.rb
index f98528ffedc..32e36c74a73 100644
--- a/spec/support/shared_examples/models/concerns/unstoppable_hooks_shared_examples.rb
+++ b/spec/support/shared_examples/models/concerns/unstoppable_hooks_shared_examples.rb
@@ -2,7 +2,7 @@
RSpec.shared_examples 'a hook that does not get automatically disabled on failure' do
describe '.executable/.disabled', :freeze_time do
- let!(:executables) do
+ let!(:webhooks) do
[
[0, Time.current],
[0, 1.minute.from_now],
@@ -29,9 +29,23 @@ RSpec.shared_examples 'a hook that does not get automatically disabled on failur
it 'finds the correct set of project hooks' do
expect(find_hooks).to all(be_executable)
- expect(find_hooks.executable).to match_array executables
+ expect(find_hooks.executable).to match_array(webhooks)
expect(find_hooks.disabled).to be_empty
end
+
+ context 'when silent mode is enabled' do
+ before do
+ stub_application_setting(silent_mode_enabled: true)
+ end
+
+ it 'causes no hooks to be considered executable' do
+ expect(find_hooks.executable).to be_empty
+ end
+
+ it 'causes all hooks to be considered disabled' do
+ expect(find_hooks.disabled).to match_array(webhooks)
+ end
+ end
end
describe '#executable?', :freeze_time do
@@ -123,12 +137,6 @@ RSpec.shared_examples 'a hook that does not get automatically disabled on failur
end
end
- describe '#disable!' do
- it 'does not disable a group hook' do
- expect { hook.disable! }.not_to change { hook.executable? }.from(true)
- end
- end
-
describe '#temporarily_disabled?' do
it 'is false' do
# Initially
@@ -150,7 +158,7 @@ RSpec.shared_examples 'a hook that does not get automatically disabled on failur
# Initially
expect(hook).not_to be_permanently_disabled
- hook.disable!
+ hook.update!(recent_failures: WebHooks::AutoDisabling::EXCEEDED_FAILURE_THRESHOLD)
expect(hook).not_to be_permanently_disabled
end
@@ -163,7 +171,7 @@ RSpec.shared_examples 'a hook that does not get automatically disabled on failur
context 'when hook has been disabled' do
before do
- hook.disable!
+ hook.update!(recent_failures: WebHooks::AutoDisabling::EXCEEDED_FAILURE_THRESHOLD)
end
it { is_expected.to eq :executable }
@@ -171,7 +179,7 @@ RSpec.shared_examples 'a hook that does not get automatically disabled on failur
context 'when hook has been backed off' do
before do
- hook.update!(recent_failures: WebHooks::AutoDisabling::FAILURE_THRESHOLD + 1)
+ hook.update!(recent_failures: WebHooks::AutoDisabling::EXCEEDED_FAILURE_THRESHOLD)
hook.disabled_until = 1.hour.from_now
end
diff --git a/spec/support/shared_examples/models/issuable_link_shared_examples.rb b/spec/support/shared_examples/models/issuable_link_shared_examples.rb
index 42c7be5ddc3..af96b77edaf 100644
--- a/spec/support/shared_examples/models/issuable_link_shared_examples.rb
+++ b/spec/support/shared_examples/models/issuable_link_shared_examples.rb
@@ -7,8 +7,8 @@
# issuable_link_factory
RSpec.shared_examples 'issuable link' do
describe 'Associations' do
- it { is_expected.to belong_to(:source).class_name(issuable.class.name) }
- it { is_expected.to belong_to(:target).class_name(issuable.class.name) }
+ it { is_expected.to belong_to(:source).class_name(issuable_class) }
+ it { is_expected.to belong_to(:target).class_name(issuable_class) }
end
describe 'Validation' do
@@ -27,7 +27,8 @@ RSpec.shared_examples 'issuable link' do
issuable_link = create_issuable_link(subject.target, subject.source)
expect(issuable_link).to be_invalid
- expect(issuable_link.errors[:source]).to include("is already related to this #{issuable.class.name.downcase}")
+ expect(issuable_link.errors[:source])
+ .to include("is already related to this #{issuable.issuable_type.humanize(capitalize: false)}")
end
context 'when it relates to itself' do
diff --git a/spec/support/shared_examples/protected_tags/access_control_ce_shared_examples.rb b/spec/support/shared_examples/protected_tags/access_control_ce_shared_examples.rb
index f308b4ad372..371f33f2b29 100644
--- a/spec/support/shared_examples/protected_tags/access_control_ce_shared_examples.rb
+++ b/spec/support/shared_examples/protected_tags/access_control_ce_shared_examples.rb
@@ -4,6 +4,7 @@ RSpec.shared_examples "protected tags > access control > CE" do
ProtectedRef::AccessLevel.human_access_levels.each do |(access_type_id, access_type_name)|
it "allows creating protected tags that #{access_type_name} can create" do
visit project_protected_tags_path(project)
+ click_button('Add tag')
set_protected_tag_name('master')
set_allowed_to('create', access_type_name)
@@ -15,6 +16,7 @@ RSpec.shared_examples "protected tags > access control > CE" do
it "allows updating protected tags so that #{access_type_name} can create them" do
visit project_protected_tags_path(project)
+ click_button('Add tag')
set_protected_tag_name('master')
set_allowed_to('create', 'No one')
diff --git a/spec/support/shared_examples/quick_actions/issuable/time_tracking_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/time_tracking_quick_action_shared_examples.rb
index 56a1cee44c8..344f827dbb2 100644
--- a/spec/support/shared_examples/quick_actions/issuable/time_tracking_quick_action_shared_examples.rb
+++ b/spec/support/shared_examples/quick_actions/issuable/time_tracking_quick_action_shared_examples.rb
@@ -90,6 +90,15 @@ RSpec.shared_examples 'issuable time tracker' do |issuable_type|
end
end
+ it 'shows the set time estimate form when add button is clicked' do
+ click_button _('Set estimate')
+
+ page.within '[data-testid="set-time-estimate-modal"]' do
+ expect(page).to have_content 'Set time estimate'
+ expect(page).to have_content 'Estimate'
+ end
+ end
+
it 'shows the time tracking report when link is clicked' do
submit_time('/estimate 1w')
submit_time('/spend 1d')
diff --git a/spec/support/shared_examples/quick_actions/merge_request/merge_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/merge_request/merge_quick_action_shared_examples.rb
index cf5a67f6096..f9bcfb1f304 100644
--- a/spec/support/shared_examples/quick_actions/merge_request/merge_quick_action_shared_examples.rb
+++ b/spec/support/shared_examples/quick_actions/merge_request/merge_quick_action_shared_examples.rb
@@ -36,6 +36,8 @@ RSpec.shared_examples 'merge quick action' do
create(:ci_pipeline, :detached_merge_request_pipeline,
project: project, merge_request: merge_request)
merge_request.update_head_pipeline
+
+ stub_licensed_features(merge_request_approvers: true) if Gitlab.ee?
end
it 'schedules to merge the MR' do
diff --git a/spec/support/shared_examples/quick_actions/work_item/type_change_quick_actions_shared_examples.rb b/spec/support/shared_examples/quick_actions/work_item/type_change_quick_actions_shared_examples.rb
index 0fc914d71d5..67e0b2c4b65 100644
--- a/spec/support/shared_examples/quick_actions/work_item/type_change_quick_actions_shared_examples.rb
+++ b/spec/support/shared_examples/quick_actions/work_item/type_change_quick_actions_shared_examples.rb
@@ -78,16 +78,6 @@ RSpec.shared_examples 'quick actions that change work item type' do
end
it_behaves_like 'action with validation errors'
-
- context 'when task has a parent' do
- let_it_be(:parent) { create(:work_item, :issue, project: project) }
-
- before do
- create(:parent_link, work_item: task, work_item_parent: parent)
- end
-
- it_behaves_like 'quick command error', 'A task cannot be promoted when a parent issue is present', 'promote'
- end
end
end
end
diff --git a/spec/support/shared_examples/requests/api/draft_notes_shared_examples.rb b/spec/support/shared_examples/requests/api/draft_notes_shared_examples.rb
new file mode 100644
index 00000000000..40825cdd5ed
--- /dev/null
+++ b/spec/support/shared_examples/requests/api/draft_notes_shared_examples.rb
@@ -0,0 +1,91 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'diff draft notes API' do |id_name|
+ describe "post /projects/:id/merge_requests/:merge_request_id/draft_notes" do
+ it "creates a new diff draft note" do
+ line_range = {
+ "start" => {
+ "line_code" => Gitlab::Git.diff_line_code(draft_note.position.file_path, 1, 1),
+ "type" => draft_note.position.type
+ },
+ "end" => {
+ "line_code" => Gitlab::Git.diff_line_code(draft_note.position.file_path, 2, 2),
+ "type" => draft_note.position.type
+ }
+ }
+
+ position = draft_note.position.to_h.merge({ line_range: line_range }).except(:ignore_whitespace_change)
+
+ post api("/projects/#{project.id}/merge_requests/#{merge_request[id_name]}/draft_notes", user),
+ params: { note: 'hi!', position: position }
+
+ expect(response).to have_gitlab_http_status(:created)
+ expect(json_response['note']).to eq('hi!')
+ expect(json_response['position']).to eq(position.stringify_keys)
+ end
+
+ context "when position is invalid" do
+ it "returns a 400 bad request error when position is not plausible" do
+ position = draft_note.position.to_h.merge(new_line: '100000')
+
+ post api("/projects/#{project.id}/merge_requests/#{merge_request[id_name]}/draft_notes", user),
+ params: { body: 'hi!', position: position }
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+
+ it "returns a 400 bad request error when the position is not valid for this discussion" do
+ position = draft_note.position.to_h.merge(new_line: '588440f66559714280628a4f9799f0c4eb880a4a')
+
+ post api("/projects/#{project.id}/merge_requests/#{merge_request[id_name]}/draft_notes", user),
+ params: { body: 'hi!', position: position }
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+ end
+ end
+
+ describe "put /projects/:id/merge_requests/:merge_request_id/draft_notes/:draft_note_id" do
+ it "modifies a draft note" do
+ line_range = {
+ "start" => {
+ "line_code" => Gitlab::Git.diff_line_code(draft_note.position.file_path, 3, 3),
+ "type" => draft_note.position.type
+ },
+ "end" => {
+ "line_code" => Gitlab::Git.diff_line_code(draft_note.position.file_path, 4, 4),
+ "type" => draft_note.position.type
+ }
+ }
+
+ position = draft_note.position.to_h.merge({ line_range: line_range }).except(:ignore_whitespace_change)
+
+ put api("/projects/#{project.id}/merge_requests/#{merge_request[id_name]}/draft_notes/#{draft_note.id}", user),
+ params: { note: 'hola!', position: position }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['note']).to eq('hola!')
+ expect(json_response['position']).to eq(position.stringify_keys)
+ end
+
+ it "returns bad request for an empty note" do
+ line_range = {
+ "start" => {
+ "line_code" => Gitlab::Git.diff_line_code(draft_note.position.file_path, 3, 3),
+ "type" => draft_note.position.type
+ },
+ "end" => {
+ "line_code" => Gitlab::Git.diff_line_code(draft_note.position.file_path, 4, 4),
+ "type" => draft_note.position.type
+ }
+ }
+
+ position = draft_note.position.to_h.merge({ line_range: line_range }).except(:ignore_whitespace_change)
+
+ put api("/projects/#{project.id}/merge_requests/#{merge_request[id_name]}/draft_notes/#{draft_note.id}", user),
+ params: { note: '', position: position }
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+ end
+end
diff --git a/spec/support/shared_examples/requests/api/graphql/packages/group_and_project_packages_list_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/packages/group_and_project_packages_list_shared_examples.rb
index 5e9dfc826d4..36832113b30 100644
--- a/spec/support/shared_examples/requests/api/graphql/packages/group_and_project_packages_list_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/graphql/packages/group_and_project_packages_list_shared_examples.rb
@@ -44,7 +44,7 @@ RSpec.shared_examples 'group and project packages query' do
post_graphql(query, current_user: current_user)
end
- it_behaves_like 'a working graphql query'
+ it_behaves_like 'a working graphql query that returns data'
it 'returns packages successfully' do
expect(package_names).to contain_exactly(
@@ -84,11 +84,7 @@ RSpec.shared_examples 'group and project packages query' do
post_graphql(query, current_user: current_user)
end
- it_behaves_like 'a working graphql query'
-
- it 'returns nil' do
- expect(packages).to be_nil
- end
+ it_behaves_like 'a working graphql query that returns no data'
end
context 'when the user is not authenticated' do
@@ -96,11 +92,7 @@ RSpec.shared_examples 'group and project packages query' do
post_graphql(query)
end
- it_behaves_like 'a working graphql query'
-
- it 'returns nil' do
- expect(packages).to be_nil
- end
+ it_behaves_like 'a working graphql query that returns no data'
end
describe 'sorting and pagination' do
diff --git a/spec/support/shared_examples/requests/api/graphql/remote_development_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/remote_development_shared_examples.rb
index 7c32c7bf2a9..83e22945361 100644
--- a/spec/support/shared_examples/requests/api/graphql/remote_development_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/graphql/remote_development_shared_examples.rb
@@ -10,9 +10,7 @@ RSpec.shared_examples 'workspaces query in licensed environment and with feature
it_behaves_like 'a working graphql query'
- # noinspection RubyResolve
it { is_expected.to match_array(a_hash_including('name' => workspace.name)) }
- # noinspection RubyResolve
context 'when user is not authorized' do
let(:current_user) { create(:user) }
diff --git a/spec/support/shared_examples/requests/api/hooks_shared_examples.rb b/spec/support/shared_examples/requests/api/hooks_shared_examples.rb
index a2c34aa6a54..7489dc7c1d6 100644
--- a/spec/support/shared_examples/requests/api/hooks_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/hooks_shared_examples.rb
@@ -121,7 +121,7 @@ RSpec.shared_examples 'web-hook API endpoints' do |prefix|
context 'the hook is disabled' do
before do
- hook.disable!
+ hook.update!(recent_failures: hook.class::EXCEEDED_FAILURE_THRESHOLD)
end
it "has the correct alert status", :aggregate_failures do
diff --git a/spec/support/shared_examples/requests/api/nuget_endpoints_shared_examples.rb b/spec/support/shared_examples/requests/api/nuget_endpoints_shared_examples.rb
index 432e67ee21e..150e9a4e004 100644
--- a/spec/support/shared_examples/requests/api/nuget_endpoints_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/nuget_endpoints_shared_examples.rb
@@ -1,8 +1,10 @@
# frozen_string_literal: true
-RSpec.shared_examples 'handling nuget service requests' do
+RSpec.shared_examples 'handling nuget service requests' do |v2: false|
subject { get api(url) }
+ it { is_expected.to have_request_urgency(v2 ? :low : :default) }
+
context 'with valid target' do
using RSpec::Parameterized::TableSyntax
@@ -20,15 +22,17 @@ RSpec.shared_examples 'handling nuget service requests' do
end
with_them do
- let(:snowplow_gitlab_standard_context) { snowplow_context(user_role: :anonymous) }
-
- subject { get api(url) }
+ let(:snowplow_gitlab_standard_context) do
+ snowplow_context(user_role: :anonymous).tap do |ctx|
+ ctx[:feed] = 'v2' if v2
+ end
+ end
before do
update_visibility_to(Gitlab::VisibilityLevel.const_get(visibility_level, false))
end
- it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member]
+ it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member], v2
end
end
diff --git a/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb
index d6a0055700d..2e66bae26ba 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
@@ -18,7 +18,7 @@ RSpec.shared_examples 'rejects nuget packages access' do |user_type, status, add
end
end
-RSpec.shared_examples 'process nuget service index request' do |user_type, status, add_member = true|
+RSpec.shared_examples 'process nuget service index request' do |user_type, status, add_member = true, v2 = false|
context "for user type #{user_type}" do
before do
target.send("add_#{user_type}", user) if add_member && user_type != :anonymous
@@ -28,15 +28,22 @@ RSpec.shared_examples 'process nuget service index request' do |user_type, statu
it_behaves_like 'a package tracking event', 'API::NugetPackages', 'cli_metadata'
- it 'returns a valid json response' do
+ it 'returns a valid json or xml response' do
subject
- expect(response.media_type).to eq('application/json')
- expect(json_response).to match_schema('public_api/v4/packages/nuget/service_index')
- expect(json_response).to be_a(Hash)
+ if v2
+ expect(response.media_type).to eq('application/xml')
+ expect(body).to have_xpath('//service')
+ .and have_xpath('//service/workspace')
+ .and have_xpath('//service/workspace/collection[@href]')
+ else
+ expect(response.media_type).to eq('application/json')
+ expect(json_response).to match_schema('public_api/v4/packages/nuget/service_index')
+ expect(json_response).to be_a(Hash)
+ end
end
- context 'with invalid format' do
+ context 'with invalid format', unless: v2 do
let(:url) { "/#{target_type}/#{target.id}/packages/nuget/index.xls" }
it_behaves_like 'rejects nuget packages access', :anonymous, :not_found
@@ -44,6 +51,34 @@ RSpec.shared_examples 'process nuget service index request' do |user_type, statu
end
end
+RSpec.shared_examples 'process nuget v2 $metadata service request' do |user_type, status, add_member = true|
+ context "for user type #{user_type}" do
+ before do
+ target.send("add_#{user_type}", user) if add_member && user_type != :anonymous
+ end
+
+ it_behaves_like 'returning response status', status
+
+ it 'returns a valid xml response' do
+ api_request
+
+ doc = Nokogiri::XML(body)
+
+ expect(response.media_type).to eq('application/xml')
+ expect(doc.at_xpath('//edmx:Edmx')).to be_present
+ expect(doc.at_xpath('//edmx:Edmx/edmx:DataServices')).to be_present
+ expect(doc.css('*').map(&:name)).to include(
+ 'Schema', 'EntityType', 'Key', 'PropertyRef', 'EntityContainer', 'EntitySet', 'FunctionImport', 'Parameter'
+ )
+ expect(doc.css('*').select { |el| el.name == 'Property' }.map { |el| el.attribute_nodes.first.value })
+ .to match_array(%w[Id Version Authors Dependencies Description DownloadCount IconUrl Published ProjectUrl
+ Tags Title LicenseUrl]
+ )
+ expect(doc.css('*').detect { |el| el.name == 'FunctionImport' }.attr('Name')).to eq('FindPackagesById')
+ end
+ end
+end
+
RSpec.shared_examples 'returning nuget metadata json response with json schema' do |json_schema|
it 'returns a valid json response' do
subject
@@ -320,6 +355,33 @@ RSpec.shared_examples 'process nuget download content request' do |user_type, st
expect(response.media_type).to eq('application/octet-stream')
end
end
+
+ context 'with normalized package version' do
+ let(:normalized_version) { '0.1.0' }
+ let(:url) { "/projects/#{target.id}/packages/nuget/download/#{package.name}/#{normalized_version}/#{package.name}.#{package.version}.#{format}" }
+
+ before do
+ package.nuget_metadatum.update_column(:normalized_version, normalized_version)
+ end
+
+ it_behaves_like 'returning response status', status
+
+ it 'returns a valid package archive' do
+ subject
+
+ expect(response.media_type).to eq('application/octet-stream')
+ end
+
+ it_behaves_like 'bumping the package last downloaded at field'
+
+ context 'when nuget_normalized_version feature flag is disabled' do
+ before do
+ stub_feature_flags(nuget_normalized_version: false)
+ end
+
+ it_behaves_like 'returning response status', :not_found
+ end
+ end
end
end
@@ -439,6 +501,13 @@ end
RSpec.shared_examples 'nuget authorize upload endpoint' do
using RSpec::Parameterized::TableSyntax
+ include_context 'workhorse headers'
+
+ let(:headers) { {} }
+
+ subject { put api(url), headers: headers }
+
+ it { is_expected.to have_request_urgency(:low) }
context 'with valid project' do
where(:visibility_level, :user_role, :member, :user_token, :sent_through, :shared_examples_name, :expected_status) do
@@ -517,6 +586,26 @@ end
RSpec.shared_examples 'nuget upload endpoint' do |symbol_package: false|
using RSpec::Parameterized::TableSyntax
+ include_context 'workhorse headers'
+
+ let(:headers) { {} }
+ let(:file_name) { symbol_package ? 'package.snupkg' : 'package.nupkg' }
+ let(:params) { { package: temp_file(file_name) } }
+ let(:file_key) { :package }
+ let(:send_rewritten_field) { true }
+
+ subject do
+ workhorse_finalize(
+ api(url),
+ method: :put,
+ file_key: file_key,
+ params: params,
+ headers: headers,
+ send_rewritten_field: send_rewritten_field
+ )
+ end
+
+ it { is_expected.to have_request_urgency(:low) }
context 'with valid project' do
where(:visibility_level, :user_role, :member, :user_token, :sent_through, :shared_examples_name, :expected_status) do
@@ -573,7 +662,12 @@ RSpec.shared_examples 'nuget upload endpoint' do |symbol_package: false|
end
let(:headers) { user_headers.merge(workhorse_headers) }
- let(:snowplow_gitlab_standard_context) { { project: project, user: user, namespace: project.namespace, property: 'i_package_nuget_user' } }
+
+ let(:snowplow_gitlab_standard_context) do
+ { project: project, user: user, namespace: project.namespace, property: 'i_package_nuget_user' }.tap do |ctx|
+ ctx[:feed] = 'v2' if url.include?('nuget/v2')
+ end
+ end
before do
update_visibility_to(Gitlab::VisibilityLevel.const_get(visibility_level, false))
@@ -604,4 +698,16 @@ RSpec.shared_examples 'nuget upload endpoint' do |symbol_package: false|
it_behaves_like 'returning response status', :bad_request
end
+
+ context 'when ObjectStorage::RemoteStoreError is raised' do
+ let(:headers) { basic_auth_header(deploy_token.username, deploy_token.token).merge(workhorse_headers) }
+
+ before do
+ allow_next_instance_of(::Packages::CreatePackageFileService) do |instance|
+ allow(instance).to receive(:execute).and_raise(ObjectStorage::RemoteStoreError)
+ end
+ end
+
+ it_behaves_like 'returning response status', :forbidden
+ end
end
diff --git a/spec/support/shared_examples/requests/api/time_tracking_shared_examples.rb b/spec/support/shared_examples/requests/api/time_tracking_shared_examples.rb
index 398421c7a79..dec15cb68b3 100644
--- a/spec/support/shared_examples/requests/api/time_tracking_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/time_tracking_shared_examples.rb
@@ -20,40 +20,49 @@ RSpec.shared_examples 'time tracking endpoints' do |issuable_name|
issuable_collection_name = issuable_name.pluralize
describe "POST /projects/:id/#{issuable_collection_name}/:#{issuable_name}_id/time_estimate" do
+ subject(:set_time_estimate) do
+ post(api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/time_estimate", user), params: { duration: duration })
+ end
+
+ let(:duration) { '2h' }
+
context 'with an unauthorized user' do
- subject { post(api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/time_estimate", non_member), params: { duration: '1w' }) }
+ let(:user) { non_member }
it_behaves_like 'an unauthorized API user'
it_behaves_like 'API user with insufficient permissions'
end
- it "sets the time estimate for #{issuable_name}" do
- post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/time_estimate", user), params: { duration: '1w' }
+ context 'with an authorized user' do
+ it "sets the time estimate for #{issuable_name}" do
+ set_time_estimate
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response['human_time_estimate']).to eq('1w')
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['time_estimate']).to eq(7200)
+ end
end
describe 'updating the current estimate' do
before do
- post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/time_estimate", user), params: { duration: '1w' }
+ post(api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/time_estimate", user), params: { duration: '2h' })
end
- context 'when duration has a bad format' do
- it 'does not modify the original estimate' do
- post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/time_estimate", user), params: { duration: 'foo' }
+ using RSpec::Parameterized::TableSyntax
- expect(response).to have_gitlab_http_status(:bad_request)
- expect(issuable.reload.human_time_estimate).to eq('1w')
- end
+ where(:updated_duration, :expected_http_status, :expected_time_estimate) do
+ 'foo' | :bad_request | 7200
+ '-1' | :bad_request | 7200
+ '1h' | :ok | 3600
+ '0' | :ok | 0
end
- context 'with a valid duration' do
- it 'updates the estimate' do
- post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/time_estimate", user), params: { duration: '3w1h' }
+ with_them do
+ let(:duration) { updated_duration }
+ it 'returns expected HTTP status and time estimate' do
+ set_time_estimate
- expect(response).to have_gitlab_http_status(:ok)
- expect(issuable.reload.human_time_estimate).to eq('3w 1h')
+ expect(response).to have_gitlab_http_status(expected_http_status)
+ expect(issuable.reload.time_estimate).to eq(expected_time_estimate)
end
end
end
diff --git a/spec/support/shared_examples/requests/graphql_shared_examples.rb b/spec/support/shared_examples/requests/graphql_shared_examples.rb
index 2c08f946468..69933bafbea 100644
--- a/spec/support/shared_examples/requests/graphql_shared_examples.rb
+++ b/spec/support/shared_examples/requests/graphql_shared_examples.rb
@@ -5,11 +5,31 @@ RSpec.shared_examples 'a working graphql query' do
it 'returns a successful response', :aggregate_failures do
expect(response).to have_gitlab_http_status(:success)
- expect(graphql_errors).to be_nil
+ expect_graphql_errors_to_be_empty
expect(json_response.keys).to include('data')
end
end
+RSpec.shared_examples 'a working graphql query that returns no data' do
+ include GraphqlHelpers
+
+ it_behaves_like 'a working graphql query'
+
+ it 'contains no data' do
+ expect(graphql_data.compact).to be_empty
+ end
+end
+
+RSpec.shared_examples 'a working graphql query that returns data' do
+ include GraphqlHelpers
+
+ it_behaves_like 'a working graphql query'
+
+ it 'contains data' do
+ expect(graphql_data.compact).not_to be_empty
+ end
+end
+
RSpec.shared_examples 'a working GraphQL mutation' do
include GraphqlHelpers
@@ -20,11 +40,7 @@ RSpec.shared_examples 'a working GraphQL mutation' do
shared_examples 'allows access to the mutation' do
let(:scopes) { ['api'] }
- it_behaves_like 'a working graphql query' do
- it 'returns data' do
- expect(graphql_data.compact).not_to be_empty
- end
- end
+ it_behaves_like 'a working graphql query that returns data'
end
shared_examples 'prevents access to the mutation' do
diff --git a/spec/support/shared_examples/services/container_registry_auth_service_shared_examples.rb b/spec/support/shared_examples/services/container_registry_auth_service_shared_examples.rb
index 493a96b8dae..34188a8d18a 100644
--- a/spec/support/shared_examples/services/container_registry_auth_service_shared_examples.rb
+++ b/spec/support/shared_examples/services/container_registry_auth_service_shared_examples.rb
@@ -58,6 +58,12 @@ RSpec.shared_examples 'with auth_type' do
let(:current_params) { super().merge(auth_type: :foo) }
it { expect(payload['auth_type']).to eq('foo') }
+
+ it "contains the auth_type as part of the encoded user information in the payload" do
+ user_info = decode_user_info_from_payload(payload)
+
+ expect(user_info["token_type"]).to eq("foo")
+ end
end
RSpec.shared_examples 'a browsable' do
@@ -971,7 +977,16 @@ RSpec.shared_examples 'a container registry auth service' do
let(:authentication_abilities) { [:read_container_image] }
it_behaves_like 'an authenticated'
+
it { expect(payload['auth_type']).to eq('deploy_token') }
+
+ it "has encoded user information in the payload" do
+ user_info = decode_user_info_from_payload(payload)
+
+ expect(user_info["token_type"]).to eq('deploy_token')
+ expect(user_info["username"]).to eq(deploy_token.username)
+ expect(user_info["deploy_token_id"]).to eq(deploy_token.id)
+ end
end
end
@@ -1198,6 +1213,15 @@ RSpec.shared_examples 'a container registry auth service' do
it_behaves_like 'a pushable'
it_behaves_like 'container repository factory'
end
+
+ it "has encoded user information in the payload" do
+ user_info = decode_user_info_from_payload(payload)
+
+ expect(user_info["username"]).to eq(current_user.username)
+ expect(user_info["user_id"]).to eq(current_user.id)
+ end
+
+ it_behaves_like 'with auth_type'
end
end
@@ -1293,4 +1317,8 @@ RSpec.shared_examples 'a container registry auth service' do
end
end
end
+
+ def decode_user_info_from_payload(payload)
+ JWT.decode(payload["user"], nil, false)[0]["user_info"]
+ end
end
diff --git a/spec/support/shared_examples/services/import_csv_service_shared_examples.rb b/spec/support/shared_examples/services/import_csv_service_shared_examples.rb
index 1555497ae48..b09d087f518 100644
--- a/spec/support/shared_examples/services/import_csv_service_shared_examples.rb
+++ b/spec/support/shared_examples/services/import_csv_service_shared_examples.rb
@@ -36,3 +36,15 @@ RSpec.shared_examples 'correctly handles invalid files' do
it_behaves_like 'invalid file'
end
end
+
+RSpec.shared_examples 'performs a spam check' do |perform_check|
+ it 'initializes issue create service with expected spam check parameter' do
+ expect(Issues::CreateService)
+ .to receive(:new)
+ .at_least(:once)
+ .with(hash_including(perform_spam_check: perform_check))
+ .and_call_original
+
+ subject
+ end
+end
diff --git a/spec/support/shared_examples/services/issuable/issuable_update_service_shared_examples.rb b/spec/support/shared_examples/services/issuable/issuable_update_service_shared_examples.rb
index 85a05bbe56d..3f95d6060ea 100644
--- a/spec/support/shared_examples/services/issuable/issuable_update_service_shared_examples.rb
+++ b/spec/support/shared_examples/services/issuable/issuable_update_service_shared_examples.rb
@@ -64,6 +64,87 @@ RSpec.shared_examples 'issuable update service' do
end
end
+RSpec.shared_examples 'updating issuable labels' do
+ context 'when add_label_ids and label_ids are passed' do
+ let(:params) { { label_ids: [label_a.id], add_label_ids: [label_c.id] } }
+
+ it 'replaces the labels with the ones in label_ids and adds those in add_label_ids' do
+ issuable.update!(labels: [label_b])
+ update_issuable(params)
+
+ expect(issuable.label_ids).to contain_exactly(label_a.id, label_c.id)
+ end
+ end
+
+ context 'when remove_label_ids and label_ids are passed' do
+ let(:params) { { label_ids: [label_a.id, label_b.id, label_c.id], remove_label_ids: [label_a.id] } }
+
+ it 'replaces the labels with the ones in label_ids and removes those in remove_label_ids' do
+ issuable.update!(labels: [label_a, label_c])
+ update_issuable(params)
+
+ expect(issuable.label_ids).to contain_exactly(label_b.id, label_c.id)
+ end
+ end
+
+ context 'when add_label_ids and remove_label_ids are passed' do
+ let(:params) { { add_label_ids: [label_c.id], remove_label_ids: [label_a.id] } }
+
+ before do
+ issuable.update!(labels: [label_a])
+ update_issuable(params)
+ end
+
+ it 'adds the passed labels' do
+ expect(issuable.label_ids).to include(label_c.id)
+ end
+
+ it 'removes the passed labels' do
+ expect(issuable.label_ids).not_to include(label_a.id)
+ end
+ end
+
+ context 'when same id is passed as add_label_ids and remove_label_ids' do
+ let(:params) { { add_label_ids: [label_a.id], remove_label_ids: [label_a.id] } }
+
+ context 'for a label assigned to an issue' do
+ it 'removes the label' do
+ issuable.update!(labels: [label_a])
+ update_issuable(params)
+
+ expect(issuable.label_ids).to be_empty
+ end
+ end
+
+ context 'for a label not assigned to an issue' do
+ it 'does not add the label' do
+ expect(issuable.label_ids).to be_empty
+ end
+ end
+ end
+
+ context 'when duplicate label titles are given' do
+ let(:params) { { labels: [label_c.title, label_c.title] } }
+
+ it 'assigns the label once' do
+ update_issuable(params)
+
+ expect(issuable.labels).to contain_exactly(label_c)
+ end
+ end
+
+ context 'when remove_label_ids contains a locked label' do
+ let(:params) { { remove_label_ids: [label_locked.id] } }
+
+ it 'removes locked labels for non-merged issuables' do
+ issuable.update!(labels: [label_a, label_locked])
+ update_issuable(params)
+
+ expect(issuable.label_ids).to contain_exactly(label_a.id)
+ end
+ end
+end
+
RSpec.shared_examples 'keeps issuable labels sorted after update' do
before do
update_issuable(label_ids: [label_b.id])
diff --git a/spec/support/shared_examples/services/issuable_links/create_links_shared_examples.rb b/spec/support/shared_examples/services/issuable_links/create_links_shared_examples.rb
index 0bf8bc4ff04..83a2f3136b4 100644
--- a/spec/support/shared_examples/services/issuable_links/create_links_shared_examples.rb
+++ b/spec/support/shared_examples/services/issuable_links/create_links_shared_examples.rb
@@ -1,16 +1,30 @@
# frozen_string_literal: true
-RSpec.shared_examples 'issuable link creation' do
+RSpec.shared_examples 'issuable link creation' do |use_references: true|
+ let(:items_param) { use_references ? :issuable_references : :target_issuable }
+ let(:response_keys) { [:status, :created_references] }
+ let(:already_assigned_error_msg) { "#{issuable_type.capitalize}(s) already assigned" }
+ let(:permission_error_status) { issuable_type == :issue ? 403 : 404 }
+ let(:permission_error_msg) do
+ if issuable_type == :issue
+ "Couldn't link issue. You must have at least the Reporter role in both projects."
+ else
+ no_found_error_msg
+ end
+ end
+
+ let(:no_found_error_msg) do
+ "No matching #{issuable_type} found. Make sure that you are adding a valid #{issuable_type} URL."
+ end
+
describe '#execute' do
subject { described_class.new(issuable, user, params).execute }
- context 'when the reference list is empty' do
- let(:params) do
- { issuable_references: [] }
- end
+ context 'when the items list is empty' do
+ let(:params) { set_params([]) }
it 'returns error' do
- is_expected.to eq(message: "No matching #{issuable_type} found. Make sure that you are adding a valid #{issuable_type} URL.", status: :error, http_status: 404)
+ is_expected.to eq(message: no_found_error_msg, status: :error, http_status: 404)
end
end
@@ -20,7 +34,7 @@ RSpec.shared_examples 'issuable link creation' do
end
it 'returns error' do
- is_expected.to eq(message: "No matching #{issuable_type} found. Make sure that you are adding a valid #{issuable_type} URL.", status: :error, http_status: 404)
+ is_expected.to eq(message: no_found_error_msg, status: :error, http_status: 404)
end
it 'no relationship is created' do
@@ -29,16 +43,10 @@ RSpec.shared_examples 'issuable link creation' do
end
context 'when user has no permission to target issuable' do
- let(:params) do
- { issuable_references: [restricted_issuable.to_reference(issuable_parent)] }
- end
+ let(:params) { set_params([restricted_issuable]) }
it 'returns error' do
- if issuable_type == :issue
- is_expected.to eq(message: "Couldn't link #{issuable_type}. You must have at least the Reporter role in both projects.", status: :error, http_status: 403)
- else
- is_expected.to eq(message: "No matching #{issuable_type} found. Make sure that you are adding a valid #{issuable_type} URL.", status: :error, http_status: 404)
- end
+ is_expected.to eq(message: permission_error_msg, status: :error, http_status: permission_error_status)
end
it 'no relationship is created' do
@@ -47,9 +55,7 @@ RSpec.shared_examples 'issuable link creation' do
end
context 'source and target are the same issuable' do
- let(:params) do
- { issuable_references: [issuable.to_reference] }
- end
+ let(:params) { set_params([issuable]) }
it 'does not create notes' do
expect(SystemNoteService).not_to receive(:relate_issuable)
@@ -63,9 +69,7 @@ RSpec.shared_examples 'issuable link creation' do
end
context 'when there is an issuable to relate' do
- let(:params) do
- { issuable_references: [issuable2.to_reference, issuable3.to_reference(issuable_parent)] }
- end
+ let(:params) { set_params([issuable2, issuable3]) }
it 'creates relationships' do
expect { subject }.to change { issuable_link_class.count }.by(2)
@@ -75,7 +79,7 @@ RSpec.shared_examples 'issuable link creation' do
end
it 'returns success status and created links', :aggregate_failures do
- expect(subject.keys).to match_array([:status, :created_references])
+ expect(subject.keys).to match_array(response_keys)
expect(subject[:status]).to eq(:success)
expect(subject[:created_references].map(&:target_id)).to match_array([issuable2.id, issuable3.id])
end
@@ -98,15 +102,7 @@ RSpec.shared_examples 'issuable link creation' do
end
context 'when reference of any already related issue is present' do
- let(:params) do
- {
- issuable_references: [
- issuable_a.to_reference,
- issuable_b.to_reference
- ],
- link_type: IssueLink::TYPE_RELATES_TO
- }
- end
+ let(:params) { set_params([issuable_a, issuable_b]) }
it 'creates notes only for new relations' do
expect(SystemNoteService).to receive(:relate_issuable).with(issuable, issuable_a, anything)
@@ -118,22 +114,18 @@ RSpec.shared_examples 'issuable link creation' do
end
end
- context 'when there are invalid references' do
- let(:params) do
- { issuable_references: [issuable.to_reference, issuable_a.to_reference] }
- end
-
- it 'creates links only for valid references' do
- expect { subject }.to change { issuable_link_class.count }.by(1)
- end
+ context 'when reference of all related issue are present' do
+ let(:params) { set_params([issuable_b]) }
it 'returns error status' do
- expect(subject).to eq(
- status: :error,
- http_status: 422,
- message: "#{issuable.to_reference} cannot be added: cannot be related to itself"
- )
+ expect(subject).to eq(status: :error, http_status: 409, message: already_assigned_error_msg)
end
end
end
+
+ def set_params(items)
+ items_list = items_param == :issuable_references ? items.map { |item| item.to_reference(issuable_parent) } : items
+
+ { items_param => items_list }
+ 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 bdb01b12607..9b2e038a331 100644
--- a/spec/support/shared_examples/services/metrics/dashboard_shared_examples.rb
+++ b/spec/support/shared_examples/services/metrics/dashboard_shared_examples.rb
@@ -124,27 +124,6 @@ RSpec.shared_examples 'valid dashboard cloning process' do |dashboard_template,
end
end
-RSpec.shared_examples 'valid dashboard update process' do
- let(:dashboard_attrs) do
- {
- commit_message: commit_message,
- branch_name: branch,
- start_branch: project.default_branch,
- encoding: 'text',
- file_path: ".gitlab/dashboards/#{file_name}",
- file_content: ::PerformanceMonitoring::PrometheusDashboard.from_json(file_content_hash).to_yaml
- }
- end
-
- it 'delegates commit creation to Files::UpdateService', :aggregate_failures do
- service_instance = instance_double(::Files::UpdateService)
- expect(::Files::UpdateService).to receive(:new).with(project, user, dashboard_attrs).and_return(service_instance)
- expect(service_instance).to receive(:execute).and_return(status: :success)
-
- service_call
- end
-end
-
RSpec.shared_examples 'misconfigured dashboard service response with stepable' do |status_code, message = nil|
it 'returns an appropriate message and status code', :aggregate_failures do
result = service_call
diff --git a/spec/support/shared_examples/services/namespace_package_settings_shared_examples.rb b/spec/support/shared_examples/services/namespace_package_settings_shared_examples.rb
index 11a786fdefb..6f0fd1aa4ed 100644
--- a/spec/support/shared_examples/services/namespace_package_settings_shared_examples.rb
+++ b/spec/support/shared_examples/services/namespace_package_settings_shared_examples.rb
@@ -9,6 +9,8 @@ RSpec.shared_examples 'updating the namespace package setting attributes' do |to
.and change { namespace.package_settings.reload.maven_duplicate_exception_regex }.from(from[:maven_duplicate_exception_regex]).to(to[:maven_duplicate_exception_regex])
.and change { namespace.package_settings.reload.generic_duplicates_allowed }.from(from[:generic_duplicates_allowed]).to(to[:generic_duplicates_allowed])
.and change { namespace.package_settings.reload.generic_duplicate_exception_regex }.from(from[:generic_duplicate_exception_regex]).to(to[:generic_duplicate_exception_regex])
+ .and change { namespace.package_settings.reload.nuget_duplicates_allowed }.from(from[:nuget_duplicates_allowed]).to(to[:nuget_duplicates_allowed])
+ .and change { namespace.package_settings.reload.nuget_duplicate_exception_regex }.from(from[:nuget_duplicate_exception_regex]).to(to[:nuget_duplicate_exception_regex])
end
end
@@ -30,6 +32,8 @@ RSpec.shared_examples 'creating the namespace package setting' do
expect(namespace.package_setting_relation.maven_duplicate_exception_regex).to eq(package_settings[:maven_duplicate_exception_regex])
expect(namespace.package_setting_relation.generic_duplicates_allowed).to eq(package_settings[:generic_duplicates_allowed])
expect(namespace.package_setting_relation.generic_duplicate_exception_regex).to eq(package_settings[:generic_duplicate_exception_regex])
+ expect(namespace.package_setting_relation.nuget_duplicates_allowed).to eq(package_settings[:nuget_duplicates_allowed])
+ expect(namespace.package_setting_relation.nuget_duplicate_exception_regex).to eq(package_settings[:nuget_duplicate_exception_regex])
end
it_behaves_like 'returning a success'
diff --git a/spec/support/shared_examples/services/notification_service_shared_examples.rb b/spec/support/shared_examples/services/notification_service_shared_examples.rb
index cfd674e3c43..df1ae67a590 100644
--- a/spec/support/shared_examples/services/notification_service_shared_examples.rb
+++ b/spec/support/shared_examples/services/notification_service_shared_examples.rb
@@ -8,16 +8,16 @@ RSpec.shared_examples 'project emails are disabled' do |check_delivery_jobs_queu
before do
reset_delivered_emails!
- target_project.clear_memoization(:emails_disabled)
+ target_project.project_setting.clear_memoization(:emails_enabled?)
end
it 'sends no emails with project emails disabled' do
- target_project.update_attribute(:emails_disabled, true)
+ target_project.project_setting.update_attribute(:emails_enabled, false)
notification_trigger
if check_delivery_jobs_queue
- # Only check enqueud jobs, not delivered emails
+ # Only check enqueued jobs, not delivered emails
expect_no_delivery_jobs
else
# Deprecated: Check actual delivered emails
@@ -26,12 +26,12 @@ RSpec.shared_examples 'project emails are disabled' do |check_delivery_jobs_queu
end
it 'sends emails to someone' do
- target_project.update_attribute(:emails_disabled, false)
+ target_project.project_setting.update_attribute(:emails_enabled, true)
notification_trigger
if check_delivery_jobs_queue
- # Only check enqueud jobs, not delivered emails
+ # Only check enqueued jobs, not delivered emails
expect_any_delivery_jobs
else
# Deprecated: Check actual delivered emails
diff --git a/spec/support/shared_examples/services/security/ci_configuration/create_service_shared_examples.rb b/spec/support/shared_examples/services/security/ci_configuration/create_service_shared_examples.rb
index 21dc3c2bf70..fd2c7455c5f 100644
--- a/spec/support/shared_examples/services/security/ci_configuration/create_service_shared_examples.rb
+++ b/spec/support/shared_examples/services/security/ci_configuration/create_service_shared_examples.rb
@@ -111,11 +111,14 @@ RSpec.shared_examples_for 'services security ci configuration create service' do
YAML
end
- it 'fails with error' do
+ it 'returns a ServiceResponse error' do
expect(project).to receive(:ci_config_for).and_return(unsupported_yaml)
- expect { result }.to raise_error(Gitlab::Graphql::Errors::MutationError, Gitlab::Utils::ErrorMessage.to_user_facing(
- _(".gitlab-ci.yml with aliases/anchors is not supported. Please change the CI configuration manually.")))
+ expect(result).to be_kind_of(ServiceResponse)
+ expect(result.status).to eq(:error)
+ expect(result.message).to eq(
+ _(".gitlab-ci.yml with aliases/anchors is not supported. Please change the CI configuration manually.")
+ )
end
end
@@ -133,11 +136,13 @@ RSpec.shared_examples_for 'services security ci configuration create service' do
YAML
end
- it 'fails with error' do
+ it 'returns a ServiceResponse error' do
expect(project).to receive(:ci_config_for).and_return(invalid_yaml)
expect(YAML).to receive(:safe_load).and_raise(Psych::Exception)
- expect { result }.to raise_error(Gitlab::Graphql::Errors::MutationError, /merge request creation mutation failed/)
+ expect(result).to be_kind_of(ServiceResponse)
+ expect(result.status).to eq(:error)
+ expect(result.message).to match(/merge request creation failed/)
end
end
@@ -166,14 +171,13 @@ RSpec.shared_examples_for 'services security ci configuration create service' do
let(:params) { nil }
let_it_be(:project) { create(:project_empty_repo) }
- it 'returns an error' do
- expect { result }.to raise_error { |error|
- expect(error).to be_a(Gitlab::Graphql::Errors::MutationError)
- expect(error.message).to eq('UF You must <a target="_blank" rel="noopener noreferrer" ' \
- 'href="http://localhost/help/user/project/repository/index.md' \
- '#add-files-to-a-repository">add at least one file to the repository' \
- '</a> before using Security features.')
- }
+ it 'returns a ServiceResponse error' do
+ expect(result).to be_kind_of(ServiceResponse)
+ expect(result.status).to eq(:error)
+ expect(result.message).to eq('You must <a target="_blank" rel="noopener noreferrer" ' \
+ 'href="http://localhost/help/user/project/repository/index.md' \
+ '#add-files-to-a-repository">add at least one file to the repository' \
+ '</a> before using Security features.')
end
end
end
diff --git a/spec/support/shared_examples/usage_data_counters/work_item_activity_unique_counter_shared_examples.rb b/spec/support/shared_examples/usage_data_counters/work_item_activity_unique_counter_shared_examples.rb
index 4655585a092..83119046377 100644
--- a/spec/support/shared_examples/usage_data_counters/work_item_activity_unique_counter_shared_examples.rb
+++ b/spec/support/shared_examples/usage_data_counters/work_item_activity_unique_counter_shared_examples.rb
@@ -1,41 +1,27 @@
# frozen_string_literal: true
-RSpec.shared_examples 'counter that does not track the event' do
- it 'does not track the event' do
- expect { 3.times { track_event } }.to not_change {
+RSpec.shared_examples 'work item unique counter' do
+ it 'tracks a unique event only once' do
+ expect { 3.times { track_event } }.to change {
Gitlab::UsageDataCounters::HLLRedisCounter.unique_events(
event_names: event_name,
start_date: 2.weeks.ago,
end_date: 2.weeks.from_now
)
- }
+ }.by(1)
end
-end
-RSpec.shared_examples 'work item unique counter' do
- context 'when track_work_items_activity FF is enabled' do
- it 'tracks a unique event only once' do
- expect { 3.times { track_event } }.to change {
+ context 'when author is nil' do
+ let(:user) { nil }
+
+ it 'does not track the event' do
+ expect { 3.times { track_event } }.to not_change {
Gitlab::UsageDataCounters::HLLRedisCounter.unique_events(
event_names: event_name,
start_date: 2.weeks.ago,
end_date: 2.weeks.from_now
)
- }.by(1)
+ }
end
-
- context 'when author is nil' do
- let(:user) { nil }
-
- it_behaves_like 'counter that does not track the event'
- end
- end
-
- context 'when track_work_items_activity FF is disabled' do
- before do
- stub_feature_flags(track_work_items_activity: false)
- end
-
- it_behaves_like 'counter that does not track the event'
end
end
diff --git a/spec/support/shared_examples/work_item_hierarchy_restrictions_importer.rb b/spec/support/shared_examples/work_item_hierarchy_restrictions_importer.rb
index b75aa27b2b7..d61458db3b3 100644
--- a/spec/support/shared_examples/work_item_hierarchy_restrictions_importer.rb
+++ b/spec/support/shared_examples/work_item_hierarchy_restrictions_importer.rb
@@ -3,7 +3,7 @@
RSpec.shared_examples 'work item hierarchy restrictions importer' do
shared_examples_for 'adds restrictions' do
it "adds all restrictions if they don't exist" do
- expect { subject }.to change { WorkItems::HierarchyRestriction.count }.from(0).to(4)
+ expect { subject }.to change { WorkItems::HierarchyRestriction.count }.from(0).to(7)
end
end
@@ -53,7 +53,7 @@ RSpec.shared_examples 'work item hierarchy restrictions importer' do
expect { subject }.to make_queries_matching(/INSERT/, 1).and(
change { WorkItems::HierarchyRestriction.count }.by(1)
)
- expect(WorkItems::HierarchyRestriction.count).to eq(4)
+ expect(WorkItems::HierarchyRestriction.count).to eq(7)
end
end
end