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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-06-20 13:43:29 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-06-20 13:43:29 +0300
commit3b1af5cc7ed2666ff18b718ce5d30fa5a2756674 (patch)
tree3bc4a40e0ee51ec27eabf917c537033c0c5b14d4 /spec/support/shared_examples
parent9bba14be3f2c211bf79e15769cd9b77bc73a13bc (diff)
Add latest changes from gitlab-org/gitlab@16-1-stable-eev16.1.0-rc42
Diffstat (limited to 'spec/support/shared_examples')
-rw-r--r--spec/support/shared_examples/analytics/cycle_analytics/flow_metrics_examples.rb388
-rw-r--r--spec/support/shared_examples/banzai/filters/inline_embeds_shared_examples.rb64
-rw-r--r--spec/support/shared_examples/banzai/filters/inline_metrics_redactor_shared_examples.rb30
-rw-r--r--spec/support/shared_examples/ci/runner_migrations_backoff_shared_examples.rb33
-rw-r--r--spec/support/shared_examples/config/metrics/every_metric_definition_shared_examples.rb161
-rw-r--r--spec/support/shared_examples/controllers/concerns/integrations/integrations_actions_shared_examples.rb22
-rw-r--r--spec/support/shared_examples/controllers/hotlink_interceptor_shared_examples.rb3
-rw-r--r--spec/support/shared_examples/controllers/metrics/dashboard/prometheus_api_proxy_shared_examples.rb148
-rw-r--r--spec/support/shared_examples/controllers/metrics_dashboard_shared_examples.rb17
-rw-r--r--spec/support/shared_examples/features/content_editor_shared_examples.rb39
-rw-r--r--spec/support/shared_examples/features/milestone_showing_shared_examples.rb54
-rw-r--r--spec/support/shared_examples/features/runners_shared_examples.rb8
-rw-r--r--spec/support/shared_examples/features/sidebar/sidebar_labels_shared_examples.rb3
-rw-r--r--spec/support/shared_examples/features/variable_list_shared_examples.rb18
-rw-r--r--spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb7
-rw-r--r--spec/support/shared_examples/features/work_items_shared_examples.rb87
-rw-r--r--spec/support/shared_examples/graphql/mutations/can_mutate_spammable_examples.rb19
-rw-r--r--spec/support/shared_examples/graphql/notes_on_noteables_shared_examples.rb29
-rw-r--r--spec/support/shared_examples/graphql/notes_quick_actions_for_work_items_shared_examples.rb19
-rw-r--r--spec/support/shared_examples/graphql/resolvers/releases_resolvers_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/graphql/types/merge_request_interactions_type_shared_examples.rb9
-rw-r--r--spec/support/shared_examples/integrations/integration_settings_form.rb7
-rw-r--r--spec/support/shared_examples/lib/api/ai_workhorse_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/lib/gitlab/cache/json_cache_shared_examples.rb379
-rw-r--r--spec/support/shared_examples/lib/gitlab/database/foreign_key_validators_shared_examples.rb48
-rw-r--r--spec/support/shared_examples/lib/gitlab/position_formatters_shared_examples.rb3
-rw-r--r--spec/support/shared_examples/lib/gitlab/search_archived_filter_shared_examples.rb30
-rw-r--r--spec/support/shared_examples/lib/gitlab/search_labels_filter_shared_examples.rb19
-rw-r--r--spec/support/shared_examples/lib/sentry/client_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/metrics/active_record_subscriber_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/models/chat_integration_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/models/ci/token_format_shared_examples.rb6
-rw-r--r--spec/support/shared_examples/models/concerns/participable_shared_examples.rb11
-rw-r--r--spec/support/shared_examples/models/mentionable_shared_examples.rb195
-rw-r--r--spec/support/shared_examples/models/wiki_shared_examples.rb5
-rw-r--r--spec/support/shared_examples/namespaces/traversal_scope_examples.rb50
-rw-r--r--spec/support/shared_examples/quick_actions/work_item/type_change_quick_actions_shared_examples.rb93
-rw-r--r--spec/support/shared_examples/requests/api/debian_packages_shared_examples.rb12
-rw-r--r--spec/support/shared_examples/requests/api/diff_discussions_shared_examples.rb6
-rw-r--r--spec/support/shared_examples/requests/api/graphql/releases_and_group_releases_shared_examples.rb15
-rw-r--r--spec/support/shared_examples/requests/api/ml/mlflow/mlflow_shared_examples.rb9
-rw-r--r--spec/support/shared_examples/requests/api/ml_model_packages_shared_examples.rb108
-rw-r--r--spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb18
-rw-r--r--spec/support/shared_examples/requests/api/nuget_endpoints_shared_examples.rb100
-rw-r--r--spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb146
-rw-r--r--spec/support/shared_examples/requests/api/packages_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/services/packages_shared_examples.rb5
-rw-r--r--spec/support/shared_examples/services/rate_limited_service_shared_examples.rb6
-rw-r--r--spec/support/shared_examples/services/service_ping/complete_service_ping_payload_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/services/snippets_shared_examples.rb1
-rw-r--r--spec/support/shared_examples/uploaders/gitlab_uploader_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/work_items/update_service_shared_examples.rb17
-rw-r--r--spec/support/shared_examples/workers/batched_background_migration_execution_worker_shared_example.rb15
-rw-r--r--spec/support/shared_examples/workers/batched_background_migration_worker_shared_examples.rb280
54 files changed, 1648 insertions, 1118 deletions
diff --git a/spec/support/shared_examples/analytics/cycle_analytics/flow_metrics_examples.rb b/spec/support/shared_examples/analytics/cycle_analytics/flow_metrics_examples.rb
index 9c096c5a158..b436fa18a9a 100644
--- a/spec/support/shared_examples/analytics/cycle_analytics/flow_metrics_examples.rb
+++ b/spec/support/shared_examples/analytics/cycle_analytics/flow_metrics_examples.rb
@@ -239,391 +239,3 @@ RSpec.shared_examples 'value stream analytics flow metrics deploymentCount examp
it_behaves_like 'validation on Time arguments'
end
-
-RSpec.shared_examples 'value stream analytics flow metrics leadTime examples' do
- let_it_be(:milestone) { create(:milestone, group: group) }
- let_it_be(:label) { create(:group_label, group: group) }
-
- let_it_be(:author) { create(:user) }
- let_it_be(:assignee) { create(:user) }
-
- let_it_be(:issue1) do
- create(:issue, project: project1, author: author, created_at: 17.days.ago, closed_at: 12.days.ago)
- end
-
- let_it_be(:issue2) do
- create(:issue, project: project2, author: author, created_at: 16.days.ago, closed_at: 13.days.ago)
- end
-
- let_it_be(:issue3) do
- create(:labeled_issue,
- project: project1,
- labels: [label],
- author: author,
- milestone: milestone,
- assignees: [assignee],
- created_at: 14.days.ago,
- closed_at: 11.days.ago)
- end
-
- let_it_be(:issue4) do
- create(:labeled_issue,
- project: project2,
- labels: [label],
- assignees: [assignee],
- created_at: 20.days.ago,
- closed_at: 15.days.ago)
- end
-
- before do
- Analytics::CycleAnalytics::DataLoaderService.new(group: group, model: Issue).execute
- end
-
- let(:query) do
- <<~QUERY
- query($path: ID!, $assigneeUsernames: [String!], $authorUsername: String, $milestoneTitle: String, $labelNames: [String!], $from: Time!, $to: Time!) {
- #{context}(fullPath: $path) {
- flowMetrics {
- leadTime(assigneeUsernames: $assigneeUsernames, authorUsername: $authorUsername, milestoneTitle: $milestoneTitle, labelNames: $labelNames, from: $from, to: $to) {
- value
- unit
- identifier
- title
- links {
- label
- url
- }
- }
- }
- }
- }
- QUERY
- end
-
- let(:variables) do
- {
- path: full_path,
- from: 21.days.ago.iso8601,
- to: 10.days.ago.iso8601
- }
- end
-
- subject(:result) do
- post_graphql(query, current_user: current_user, variables: variables)
-
- graphql_data.dig(context.to_s, 'flowMetrics', 'leadTime')
- end
-
- it 'returns the correct value' do
- expect(result).to match(a_hash_including({
- 'identifier' => 'lead_time',
- 'unit' => n_('day', 'days', 4),
- 'value' => 4,
- 'title' => _('Lead Time'),
- 'links' => [
- { 'label' => s_('ValueStreamAnalytics|Dashboard'), 'url' => match(/issues_analytics/) },
- { 'label' => s_('ValueStreamAnalytics|Go to docs'), 'url' => match(/definitions/) }
- ]
- }))
- end
-
- context 'when the user is not authorized' do
- let(:current_user) { create(:user) }
-
- it 'returns nil' do
- expect(result).to eq(nil)
- end
- end
-
- context 'when outside of the date range' do
- let(:variables) do
- {
- path: full_path,
- from: 30.days.ago.iso8601,
- to: 25.days.ago.iso8601
- }
- end
-
- it 'returns 0 count' do
- expect(result).to match(a_hash_including({ 'value' => nil }))
- end
- end
-
- context 'with all filters' do
- let(:variables) do
- {
- path: full_path,
- assigneeUsernames: [assignee.username],
- labelNames: [label.title],
- authorUsername: author.username,
- milestoneTitle: milestone.title,
- from: 20.days.ago.iso8601,
- to: 10.days.ago.iso8601
- }
- end
-
- it 'returns filtered count' do
- expect(result).to match(a_hash_including({ 'value' => 3 }))
- end
- end
-end
-
-RSpec.shared_examples 'value stream analytics flow metrics cycleTime examples' do
- let_it_be(:milestone) { create(:milestone, group: group) }
- let_it_be(:label) { create(:group_label, group: group) }
-
- let_it_be(:author) { create(:user) }
- let_it_be(:assignee) { create(:user) }
-
- let_it_be(:issue1) do
- create(:issue, project: project1, author: author, closed_at: 12.days.ago).tap do |issue|
- issue.metrics.update!(first_mentioned_in_commit_at: 17.days.ago)
- end
- end
-
- let_it_be(:issue2) do
- create(:issue, project: project2, author: author, closed_at: 13.days.ago).tap do |issue|
- issue.metrics.update!(first_mentioned_in_commit_at: 16.days.ago)
- end
- end
-
- let_it_be(:issue3) do
- create(:labeled_issue,
- project: project1,
- labels: [label],
- author: author,
- milestone: milestone,
- assignees: [assignee],
- closed_at: 11.days.ago).tap do |issue|
- issue.metrics.update!(first_mentioned_in_commit_at: 14.days.ago)
- end
- end
-
- let_it_be(:issue4) do
- create(:labeled_issue,
- project: project2,
- labels: [label],
- assignees: [assignee],
- closed_at: 15.days.ago).tap do |issue|
- issue.metrics.update!(first_mentioned_in_commit_at: 20.days.ago)
- end
- end
-
- before do
- Analytics::CycleAnalytics::DataLoaderService.new(group: group, model: Issue).execute
- end
-
- let(:query) do
- <<~QUERY
- query($path: ID!, $assigneeUsernames: [String!], $authorUsername: String, $milestoneTitle: String, $labelNames: [String!], $from: Time!, $to: Time!) {
- #{context}(fullPath: $path) {
- flowMetrics {
- cycleTime(assigneeUsernames: $assigneeUsernames, authorUsername: $authorUsername, milestoneTitle: $milestoneTitle, labelNames: $labelNames, from: $from, to: $to) {
- value
- unit
- identifier
- title
- links {
- label
- url
- }
- }
- }
- }
- }
- QUERY
- end
-
- let(:variables) do
- {
- path: full_path,
- from: 21.days.ago.iso8601,
- to: 10.days.ago.iso8601
- }
- end
-
- subject(:result) do
- post_graphql(query, current_user: current_user, variables: variables)
-
- graphql_data.dig(context.to_s, 'flowMetrics', 'cycleTime')
- end
-
- it 'returns the correct value' do
- expect(result).to eq({
- 'identifier' => 'cycle_time',
- 'unit' => n_('day', 'days', 4),
- 'value' => 4,
- 'title' => _('Cycle Time'),
- 'links' => []
- })
- end
-
- context 'when the user is not authorized' do
- let(:current_user) { create(:user) }
-
- it 'returns nil' do
- expect(result).to eq(nil)
- end
- end
-
- context 'when outside of the date range' do
- let(:variables) do
- {
- path: full_path,
- from: 30.days.ago.iso8601,
- to: 25.days.ago.iso8601
- }
- end
-
- it 'returns 0 count' do
- expect(result).to match(a_hash_including({ 'value' => nil }))
- end
- end
-
- context 'with all filters' do
- let(:variables) do
- {
- path: full_path,
- assigneeUsernames: [assignee.username],
- labelNames: [label.title],
- authorUsername: author.username,
- milestoneTitle: milestone.title,
- from: 20.days.ago.iso8601,
- to: 10.days.ago.iso8601
- }
- end
-
- it 'returns filtered count' do
- expect(result).to match(a_hash_including({ 'value' => 3 }))
- end
- end
-end
-
-RSpec.shared_examples 'value stream analytics flow metrics issuesCompleted examples' do
- let_it_be(:milestone) { create(:milestone, group: group) }
- let_it_be(:label) { create(:group_label, group: group) }
-
- let_it_be(:author) { create(:user) }
- let_it_be(:assignee) { create(:user) }
-
- # we don't care about opened date, only closed date.
- let_it_be(:issue1) do
- create(:issue, project: project1, author: author, created_at: 17.days.ago, closed_at: 12.days.ago)
- end
-
- let_it_be(:issue2) do
- create(:issue, project: project2, author: author, created_at: 16.days.ago, closed_at: 13.days.ago)
- end
-
- let_it_be(:issue3) do
- create(:labeled_issue,
- project: project1,
- labels: [label],
- author: author,
- milestone: milestone,
- assignees: [assignee],
- created_at: 14.days.ago,
- closed_at: 11.days.ago)
- end
-
- let_it_be(:issue4) do
- create(:labeled_issue,
- project: project2,
- labels: [label],
- assignees: [assignee],
- created_at: 20.days.ago,
- closed_at: 15.days.ago)
- end
-
- before do
- Analytics::CycleAnalytics::DataLoaderService.new(group: group, model: Issue).execute
- end
-
- let(:query) do
- <<~QUERY
- query($path: ID!, $assigneeUsernames: [String!], $authorUsername: String, $milestoneTitle: String, $labelNames: [String!], $from: Time!, $to: Time!) {
- #{context}(fullPath: $path) {
- flowMetrics {
- issuesCompletedCount(assigneeUsernames: $assigneeUsernames, authorUsername: $authorUsername, milestoneTitle: $milestoneTitle, labelNames: $labelNames, from: $from, to: $to) {
- value
- unit
- identifier
- title
- links {
- label
- url
- }
- }
- }
- }
- }
- QUERY
- end
-
- let(:variables) do
- {
- path: full_path,
- from: 21.days.ago.iso8601,
- to: 10.days.ago.iso8601
- }
- end
-
- subject(:result) do
- post_graphql(query, current_user: current_user, variables: variables)
-
- graphql_data.dig(context.to_s, 'flowMetrics', 'issuesCompletedCount')
- end
-
- it 'returns the correct value' do
- expect(result).to match(a_hash_including({
- 'identifier' => 'issues_completed',
- 'unit' => n_('issue', 'issues', 4),
- 'value' => 4,
- 'title' => _('Issues Completed'),
- 'links' => [
- { 'label' => s_('ValueStreamAnalytics|Dashboard'), 'url' => match(/issues_analytics/) },
- { 'label' => s_('ValueStreamAnalytics|Go to docs'), 'url' => match(/definitions/) }
- ]
- }))
- end
-
- context 'when the user is not authorized' do
- let(:current_user) { create(:user) }
-
- it 'returns nil' do
- expect(result).to eq(nil)
- end
- end
-
- context 'when outside of the date range' do
- let(:variables) do
- {
- path: full_path,
- from: 30.days.ago.iso8601,
- to: 25.days.ago.iso8601
- }
- end
-
- it 'returns 0 count' do
- expect(result).to match(a_hash_including({ 'value' => 0.0 }))
- end
- end
-
- context 'with all filters' do
- let(:variables) do
- {
- path: full_path,
- assigneeUsernames: [assignee.username],
- labelNames: [label.title],
- authorUsername: author.username,
- milestoneTitle: milestone.title,
- from: 20.days.ago.iso8601,
- to: 10.days.ago.iso8601
- }
- end
-
- it 'returns filtered count' do
- expect(result).to match(a_hash_including({ 'value' => 1.0 }))
- end
- end
-end
diff --git a/spec/support/shared_examples/banzai/filters/inline_embeds_shared_examples.rb b/spec/support/shared_examples/banzai/filters/inline_embeds_shared_examples.rb
deleted file mode 100644
index 8f2f3f89914..00000000000
--- a/spec/support/shared_examples/banzai/filters/inline_embeds_shared_examples.rb
+++ /dev/null
@@ -1,64 +0,0 @@
-# frozen_string_literal: true
-
-# Expects 2 attributes to be defined:
-# trigger_url - Url expected to trigger the insertion of a placeholder.
-# dashboard_url - Url expected to be present in the placeholder.
-RSpec.shared_examples 'a metrics embed filter' do
- let(:input) { %(<a href="#{url}">example</a>) }
- let(:doc) { filter(input) }
-
- before do
- stub_feature_flags(remove_monitor_metrics: false)
- end
-
- context 'when the document has an external link' do
- let(:url) { 'https://foo.com' }
-
- it 'leaves regular non-metrics links unchanged' do
- expect(doc.to_s).to eq(input)
- end
- end
-
- context 'when the document contains an embeddable link' do
- let(:url) { trigger_url }
-
- it 'leaves the original link unchanged' do
- expect(unescape(doc.at_css('a').to_s)).to eq(input)
- end
-
- it 'appends a metrics charts placeholder' do
- node = doc.at_css('.js-render-metrics')
- expect(node).to be_present
-
- expect(node.attribute('data-dashboard-url').to_s).to eq(dashboard_url)
- end
-
- context 'in a paragraph' do
- let(:paragraph) { %(This is an <a href="#{url}">example</a> of metrics.) }
- let(:input) { %(<p>#{paragraph}</p>) }
-
- it 'appends a metrics charts placeholder after the enclosing paragraph' do
- expect(unescape(doc.at_css('p').to_s)).to include(paragraph)
- expect(doc.at_css('.js-render-metrics')).to be_present
- end
- end
-
- context 'when metrics dashboard feature is unavailable' do
- before do
- stub_feature_flags(remove_monitor_metrics: true)
- end
-
- it 'does not append a metrics chart placeholder' do
- node = doc.at_css('.js-render-metrics')
-
- expect(node).not_to be_present
- end
- end
- end
-
- # Nokogiri escapes the URLs, but we don't care about that
- # distinction for the purposes of these filters
- def unescape(html)
- CGI.unescapeHTML(html)
- end
-end
diff --git a/spec/support/shared_examples/banzai/filters/inline_metrics_redactor_shared_examples.rb b/spec/support/shared_examples/banzai/filters/inline_metrics_redactor_shared_examples.rb
deleted file mode 100644
index 07abb86ceb5..00000000000
--- a/spec/support/shared_examples/banzai/filters/inline_metrics_redactor_shared_examples.rb
+++ /dev/null
@@ -1,30 +0,0 @@
-# frozen_string_literal: true
-
-RSpec.shared_examples 'redacts the embed placeholder' do
- context 'no user is logged in' do
- it 'redacts the placeholder' do
- expect(doc.to_s).to be_empty
- end
- end
-
- context 'the user does not have permission do see charts' do
- let(:doc) { filter(input, current_user: build(:user)) }
-
- it 'redacts the placeholder' do
- expect(doc.to_s).to be_empty
- end
- end
-end
-
-RSpec.shared_examples 'retains the embed placeholder when applicable' do
- context 'the user has requisite permissions' do
- let(:user) { create(:user) }
- let(:doc) { filter(input, current_user: user) }
-
- it 'leaves the placeholder' do
- project.add_maintainer(user)
-
- expect(CGI.unescapeHTML(doc.to_s)).to eq(input)
- end
- end
-end
diff --git a/spec/support/shared_examples/ci/runner_migrations_backoff_shared_examples.rb b/spec/support/shared_examples/ci/runner_migrations_backoff_shared_examples.rb
new file mode 100644
index 00000000000..06a8e8811b7
--- /dev/null
+++ b/spec/support/shared_examples/ci/runner_migrations_backoff_shared_examples.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'runner migrations backoff' do
+ context 'when executing locking database migrations' do
+ it 'returns 429 error', :aggregate_failures do
+ expect(Gitlab::Database::Migrations::RunnerBackoff::Communicator)
+ .to receive(:backoff_runner?)
+ .and_return(true)
+
+ request
+
+ expect(response).to have_gitlab_http_status(:too_many_requests)
+ expect(response.headers['Retry-After']).to eq(60)
+ expect(json_response).to match({ "message" => "Executing database migrations. Please retry later." })
+ end
+
+ context 'with runner_migrations_backoff disabled' do
+ before do
+ stub_feature_flags(runner_migrations_backoff: false)
+ end
+
+ it 'does not return 429' do
+ expect(Gitlab::ExclusiveLease).not_to receive(:new)
+ .with(Gitlab::Database::Migrations::RunnerBackoff::Communicator::KEY,
+ timeout: Gitlab::Database::Migrations::RunnerBackoff::Communicator::EXPIRY)
+
+ request
+
+ expect(response).not_to have_gitlab_http_status(:too_many_requests)
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/config/metrics/every_metric_definition_shared_examples.rb b/spec/support/shared_examples/config/metrics/every_metric_definition_shared_examples.rb
new file mode 100644
index 00000000000..c8eaef764af
--- /dev/null
+++ b/spec/support/shared_examples/config/metrics/every_metric_definition_shared_examples.rb
@@ -0,0 +1,161 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'every metric definition' do
+ include UsageDataHelpers
+
+ let(:usage_ping) { Gitlab::Usage::ServicePingReport.for(output: :all_metrics_values, cached: false) }
+ let(:ignored_usage_ping_key_patterns) do
+ %w[
+ testing_total_unique_counts
+ user_auth_by_provider
+ ].freeze
+ end
+
+ let(:usage_ping_key_paths) do
+ parse_usage_ping_keys(usage_ping)
+ .flatten
+ .grep_v(Regexp.union(ignored_usage_ping_key_patterns))
+ .sort
+ end
+
+ let(:ignored_metric_files_key_patterns) do
+ %w[
+ ci_runners_online
+ mock_ci
+ mock_monitoring
+ user_auth_by_provider
+ p_ci_templates_5_min_production_app
+ p_ci_templates_aws_cf_deploy_ec2
+ p_ci_templates_auto_devops_build
+ p_ci_templates_auto_devops_deploy
+ p_ci_templates_auto_devops_deploy_latest
+ p_ci_templates_implicit_auto_devops_build
+ p_ci_templates_implicit_auto_devops_deploy_latest
+ p_ci_templates_implicit_auto_devops_deploy
+ ].freeze
+ end
+
+ let(:metric_files_key_paths) do
+ Gitlab::Usage::MetricDefinition
+ .definitions
+ .reject { |_, v| v.status == 'removed' || v.key_path =~ Regexp.union(ignored_metric_files_key_patterns) }
+ .keys
+ .sort
+ end
+
+ let(:metric_files_with_schema) do
+ Gitlab::Usage::MetricDefinition
+ .definitions
+ .select { |_, v| v.respond_to?(:value_json_schema) }
+ end
+
+ let(:expected_metric_files_key_paths) { metric_files_key_paths }
+
+ # Recursively traverse nested Hash of a generated Usage Ping to return an Array of key paths
+ # in the dotted format used in metric definition YAML files, e.g.: 'count.category.metric_name'
+ def parse_usage_ping_keys(object, key_path = [])
+ if object.is_a?(Hash) && !object_with_schema?(key_path.join('.'))
+ object.each_with_object([]) do |(key, value), result|
+ result.append parse_usage_ping_keys(value, key_path + [key])
+ end
+ else
+ key_path.join('.')
+ end
+ end
+
+ def object_with_schema?(key_path)
+ metric_files_with_schema.key?(key_path)
+ end
+
+ before do
+ allow(Gitlab::UsageData).to receive_messages(count: -1, distinct_count: -1, estimate_batch_distinct_count: -1,
+ sum: -1)
+ allow(Gitlab::UsageData).to receive(:alt_usage_data).and_wrap_original do |_m, *_args, **kwargs|
+ kwargs[:fallback] || Gitlab::Utils::UsageData::FALLBACK
+ end
+ stub_licensed_features(requirements: true)
+ stub_prometheus_queries
+ stub_usage_data_connections
+ end
+
+ it 'is included in the Usage Ping hash structure' do
+ msg = "see https://docs.gitlab.com/ee/development/service_ping/metrics_dictionary.html#metrics-added-dynamic-to-service-ping-payload"
+ expect(expected_metric_files_key_paths).to match_array(usage_ping_key_paths), msg
+ end
+
+ it 'only uses .yml and .json formats from metric related files in (ee/)config/metrics directory' do
+ metric_definition_format = '.yml'
+ object_schema_format = '.json'
+ allowed_formats = [metric_definition_format, object_schema_format]
+ glob_paths = Gitlab::Usage::MetricDefinition.paths.map do |glob_path|
+ File.join(File.dirname(glob_path), '*.*')
+ end
+
+ files_with_wrong_extensions = glob_paths.each_with_object([]) do |glob_path, array|
+ Dir.glob(glob_path).each do |path|
+ array << path unless allowed_formats.include? File.extname(path)
+ end
+ end
+
+ msg = <<~MSG
+ The only supported file extensions are: #{allowed_formats.join(', ')}.
+ The following files has the wrong extension: #{files_with_wrong_extensions}"
+ MSG
+
+ expect(files_with_wrong_extensions).to be_empty, msg
+ end
+
+ describe 'metrics classes' do
+ let(:parent_metric_classes) do
+ [
+ Gitlab::Usage::Metrics::Instrumentations::BaseMetric,
+ Gitlab::Usage::Metrics::Instrumentations::GenericMetric,
+ Gitlab::Usage::Metrics::Instrumentations::DatabaseMetric,
+ Gitlab::Usage::Metrics::Instrumentations::RedisMetric,
+ Gitlab::Usage::Metrics::Instrumentations::RedisHLLMetric,
+ Gitlab::Usage::Metrics::Instrumentations::NumbersMetric
+ ]
+ end
+
+ let(:ignored_classes) do
+ [
+ Gitlab::Usage::Metrics::Instrumentations::IssuesWithAlertManagementAlertsMetric,
+ Gitlab::Usage::Metrics::Instrumentations::IssuesWithPrometheusAlertEvents,
+ Gitlab::Usage::Metrics::Instrumentations::IssuesWithSelfManagedPrometheusAlertEvents
+ ].freeze
+ end
+
+ def assert_uses_all_nested_classes(parent_module)
+ parent_module.constants(false).each do |const_name|
+ constant = parent_module.const_get(const_name, false)
+ next if parent_metric_classes.include?(constant) || ignored_classes.include?(constant)
+
+ case constant
+ when Class
+ metric_class_instance = instance_double(constant)
+ expect(constant).to receive(:new).at_least(:once).and_return(metric_class_instance)
+ allow(metric_class_instance).to receive(:available?).and_return(true)
+ allow(metric_class_instance).to receive(:value).and_return(-1)
+ expect(metric_class_instance).to receive(:value).at_least(:once)
+ when Module
+ assert_uses_all_nested_classes(constant)
+ end
+ end
+ end
+
+ it 'uses all metrics classes' do
+ assert_uses_all_nested_classes(Gitlab::Usage::Metrics::Instrumentations)
+ usage_ping
+ end
+ end
+
+ context 'with value json schema' do
+ it 'has a valid structure', :aggregate_failures do
+ metric_files_with_schema.each do |key_path, metric|
+ structure = usage_ping.dig(*key_path.split('.').map(&:to_sym))
+
+ expect(structure).to match_metric_definition_schema(metric.value_json_schema)
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/controllers/concerns/integrations/integrations_actions_shared_examples.rb b/spec/support/shared_examples/controllers/concerns/integrations/integrations_actions_shared_examples.rb
index a8aed0c1f0b..106260e644f 100644
--- a/spec/support/shared_examples/controllers/concerns/integrations/integrations_actions_shared_examples.rb
+++ b/spec/support/shared_examples/controllers/concerns/integrations/integrations_actions_shared_examples.rb
@@ -10,6 +10,16 @@ RSpec.shared_examples Integrations::Actions do
)
end
+ shared_examples 'unknown integration' do
+ let(:routing_params) do
+ super().merge(id: 'unknown_integration')
+ end
+
+ it 'returns 404 Not Found' do
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
describe 'GET #edit' do
before do
get :edit, params: routing_params
@@ -19,6 +29,8 @@ RSpec.shared_examples Integrations::Actions do
expect(response).to have_gitlab_http_status(:ok)
expect(assigns(:integration)).to eq(integration)
end
+
+ it_behaves_like 'unknown integration'
end
describe 'PUT #update' do
@@ -55,5 +67,15 @@ RSpec.shared_examples Integrations::Actions do
expect(integration.reload).to have_attributes(params.merge(api_key: 'secret'))
end
end
+
+ it_behaves_like 'unknown integration'
+ end
+
+ describe 'PUT #test' do
+ before do
+ put :test, params: routing_params
+ end
+
+ it_behaves_like 'unknown integration'
end
end
diff --git a/spec/support/shared_examples/controllers/hotlink_interceptor_shared_examples.rb b/spec/support/shared_examples/controllers/hotlink_interceptor_shared_examples.rb
index 93a394387a3..59bdc4da174 100644
--- a/spec/support/shared_examples/controllers/hotlink_interceptor_shared_examples.rb
+++ b/spec/support/shared_examples/controllers/hotlink_interceptor_shared_examples.rb
@@ -35,6 +35,9 @@ RSpec.shared_examples "hotlink interceptor" do
:not_acceptable | "text/css,*/*;q=0.1"
:not_acceptable | "text/css"
:not_acceptable | "text/css,*/*;q=0.1"
+
+ # Invalid MIME definition
+ :not_acceptable | "text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2"
end
with_them do
diff --git a/spec/support/shared_examples/controllers/metrics/dashboard/prometheus_api_proxy_shared_examples.rb b/spec/support/shared_examples/controllers/metrics/dashboard/prometheus_api_proxy_shared_examples.rb
deleted file mode 100644
index 19b1cee44ee..00000000000
--- a/spec/support/shared_examples/controllers/metrics/dashboard/prometheus_api_proxy_shared_examples.rb
+++ /dev/null
@@ -1,148 +0,0 @@
-# frozen_string_literal: true
-
-RSpec.shared_examples_for 'metrics dashboard prometheus api proxy' do
- let(:service_params) { [proxyable, 'GET', 'query', expected_params] }
- let(:service_result) { { status: :success, body: prometheus_body } }
- let(:prometheus_proxy_service) { instance_double(Prometheus::ProxyService) }
- let(:proxyable_params) do
- {
- id: proxyable.id.to_s
- }
- end
-
- let(:expected_params) do
- ActionController::Parameters.new(
- prometheus_proxy_params(
- proxy_path: 'query',
- controller: described_class.controller_path,
- action: 'prometheus_proxy'
- )
- ).permit!
- end
-
- before do
- allow_next_instance_of(Prometheus::ProxyService, *service_params) do |proxy_service|
- allow(proxy_service).to receive(:execute).and_return(service_result)
- end
- end
-
- context 'with valid requests' do
- context 'with success result' do
- let(:prometheus_body) { '{"status":"success"}' }
- let(:prometheus_json_body) { Gitlab::Json.parse(prometheus_body) }
-
- it 'returns prometheus response' do
- get :prometheus_proxy, params: prometheus_proxy_params
-
- expect(Prometheus::ProxyService).to have_received(:new).with(*service_params)
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response).to eq(prometheus_json_body)
- end
-
- context 'with nil query' do
- let(:params_without_query) do
- prometheus_proxy_params.except(:query)
- end
-
- before do
- expected_params.delete(:query)
- end
-
- it 'does not raise error' do
- get :prometheus_proxy, params: params_without_query
-
- expect(Prometheus::ProxyService).to have_received(:new).with(*service_params)
- end
- end
- end
-
- context 'with nil result' do
- let(:service_result) { nil }
-
- it 'returns 204 no_content' do
- get :prometheus_proxy, params: prometheus_proxy_params
-
- expect(json_response['status']).to eq(_('processing'))
- expect(json_response['message']).to eq(_('Not ready yet. Try again later.'))
- expect(response).to have_gitlab_http_status(:no_content)
- end
- end
-
- context 'with 404 result' do
- let(:service_result) { { http_status: 404, status: :success, body: '{"body": "value"}' } }
-
- it 'returns body' do
- get :prometheus_proxy, params: prometheus_proxy_params
-
- expect(response).to have_gitlab_http_status(:not_found)
- expect(json_response['body']).to eq('value')
- end
- end
-
- context 'with error result' do
- context 'with http_status' do
- let(:service_result) do
- { http_status: :service_unavailable, status: :error, message: 'error message' }
- end
-
- it 'sets the http response status code' do
- get :prometheus_proxy, params: prometheus_proxy_params
-
- expect(response).to have_gitlab_http_status(:service_unavailable)
- expect(json_response['status']).to eq('error')
- expect(json_response['message']).to eq('error message')
- end
- end
-
- context 'without http_status' do
- let(:service_result) { { status: :error, message: 'error message' } }
-
- it 'returns bad_request' do
- get :prometheus_proxy, params: prometheus_proxy_params
-
- expect(response).to have_gitlab_http_status(:bad_request)
- expect(json_response['status']).to eq('error')
- expect(json_response['message']).to eq('error message')
- end
- end
- end
- end
-
- context 'with inappropriate requests' do
- let(:prometheus_body) { nil }
-
- context 'without correct permissions' do
- let(:user2) { create(:user) }
-
- before do
- sign_out(user)
- sign_in(user2)
- end
-
- it 'returns 404' do
- get :prometheus_proxy, params: prometheus_proxy_params
-
- expect(response).to have_gitlab_http_status(:not_found)
- end
- end
- end
-
- context 'with invalid proxyable id' do
- let(:prometheus_body) { nil }
-
- it 'returns 404' do
- get :prometheus_proxy, params: prometheus_proxy_params(id: proxyable.id + 1)
-
- expect(response).to have_gitlab_http_status(:not_found)
- end
- end
-
- private
-
- def prometheus_proxy_params(params = {})
- {
- proxy_path: 'query',
- query: '1'
- }.merge(proxyable_params).merge(params)
- end
-end
diff --git a/spec/support/shared_examples/controllers/metrics_dashboard_shared_examples.rb b/spec/support/shared_examples/controllers/metrics_dashboard_shared_examples.rb
index cb8f6721d66..5b63ef10c85 100644
--- a/spec/support/shared_examples/controllers/metrics_dashboard_shared_examples.rb
+++ b/spec/support/shared_examples/controllers/metrics_dashboard_shared_examples.rb
@@ -17,6 +17,10 @@ RSpec.shared_examples_for 'GET #metrics_dashboard for dashboard' do |dashboard_n
let(:expected_keys) { %w(dashboard status metrics_data) }
let(:status_code) { :ok }
+ before do
+ stub_feature_flags(remove_monitor_metrics: false)
+ end
+
it_behaves_like 'GET #metrics_dashboard correctly formatted response'
it 'returns correct dashboard' do
@@ -24,4 +28,17 @@ RSpec.shared_examples_for 'GET #metrics_dashboard for dashboard' do |dashboard_n
expect(json_response['dashboard']['dashboard']).to eq(dashboard_name)
end
+
+ context 'when metrics dashboard feature is unavailable' do
+ before do
+ stub_feature_flags(remove_monitor_metrics: true)
+ end
+
+ it 'returns 404 not found' do
+ get :metrics_dashboard, params: metrics_dashboard_req_params, format: :json
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect(response.body).to be_empty
+ end
+ end
end
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 41114197ff5..f70288168d7 100644
--- a/spec/support/shared_examples/features/content_editor_shared_examples.rb
+++ b/spec/support/shared_examples/features/content_editor_shared_examples.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.shared_examples 'edits content using the content editor' do
+RSpec.shared_examples 'edits content using the content editor' do |params = { with_expanded_references: true }|
include ContentEditorHelpers
let(:content_editor_testid) { '[data-testid="content-editor"] [contenteditable].ProseMirror' }
@@ -413,6 +413,21 @@ RSpec.shared_examples 'edits content using the content editor' do
end
end
+ describe 'rendering with initial content' do
+ it 'renders correctly with table as initial content' do
+ textarea = find 'textarea'
+ textarea.send_keys "\n\n"
+ textarea.send_keys "| First Header | Second Header |\n"
+ textarea.send_keys "|--------------|---------------|\n"
+ textarea.send_keys "| Content from cell 1 | Content from cell 2 |\n\n"
+ textarea.send_keys "Content below table"
+
+ switch_to_content_editor
+
+ expect(page).not_to have_text('An error occurred')
+ end
+ end
+
describe 'pasting text' do
before do
switch_to_content_editor
@@ -493,6 +508,28 @@ RSpec.shared_examples 'edits content using the content editor' do
type_in_content_editor :enter
end
+ if params[:with_expanded_references]
+ describe 'when expanding an issue reference' do
+ it 'displays full reference name' do
+ new_issue = create(:issue, project: project, title: 'Brand New Issue')
+
+ type_in_content_editor "##{new_issue.iid}+s "
+
+ expect(page).to have_text('Brand New Issue')
+ end
+ end
+
+ describe 'when expanding an MR reference' do
+ it 'displays full reference name' do
+ new_mr = create(:merge_request, source_project: project, source_branch: 'branch-2', title: 'Brand New MR')
+
+ type_in_content_editor "!#{new_mr.iid}+s "
+
+ expect(page).to have_text('Brand New')
+ end
+ end
+ end
+
it 'shows suggestions for members with descriptions' do
type_in_content_editor '@a'
diff --git a/spec/support/shared_examples/features/milestone_showing_shared_examples.rb b/spec/support/shared_examples/features/milestone_showing_shared_examples.rb
new file mode 100644
index 00000000000..7bcaf1fe64a
--- /dev/null
+++ b/spec/support/shared_examples/features/milestone_showing_shared_examples.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'milestone with interactive markdown task list items in description' do
+ let(:markdown) do
+ <<-MARKDOWN.strip_heredoc
+ This is a task list:
+
+ - [ ] Incomplete task list item 1
+ - [x] Complete task list item 1
+ - [ ] Incomplete task list item 2
+ - [x] Complete task list item 2
+ - [ ] Incomplete task list item 3
+ - [ ] Incomplete task list item 4
+ MARKDOWN
+ end
+
+ before do
+ milestone.update!(description: markdown)
+ end
+
+ it 'renders task list in description' do
+ visit milestone_path
+
+ wait_for_requests
+
+ within('ul.task-list') do
+ expect(page).to have_selector('li.task-list-item', count: 6)
+ expect(page).to have_selector('li.task-list-item input.task-list-item-checkbox[checked]', count: 2)
+ end
+ end
+
+ it 'allows interaction with task list item checkboxes' do
+ visit milestone_path
+
+ wait_for_requests
+
+ within('ul.task-list') do
+ within('li.task-list-item', text: 'Incomplete task list item 1') do
+ find('input.task-list-item-checkbox').click
+ wait_for_requests
+ end
+
+ expect(page).to have_selector('li.task-list-item', count: 6)
+ page.all('li.task-list-item input.task-list-item-checkbox') { |element| expect(element).to be_checked }
+
+ # After page reload, the task list items should still be checked
+ visit milestone_path
+
+ wait_for_requests
+
+ expect(page).to have_selector('ul input[type="checkbox"][checked]', count: 3)
+ end
+ end
+end
diff --git a/spec/support/shared_examples/features/runners_shared_examples.rb b/spec/support/shared_examples/features/runners_shared_examples.rb
index 7edf306183e..54a4db0e81d 100644
--- a/spec/support/shared_examples/features/runners_shared_examples.rb
+++ b/spec/support/shared_examples/features/runners_shared_examples.rb
@@ -127,7 +127,7 @@ RSpec.shared_examples 'pauses, resumes and deletes a runner' do
it 'deletes a runner' do
within_modal do
- click_on 'Delete runner'
+ click_on 'Permanently delete runner'
end
expect(page.find('.gl-toast')).to have_text(/Runner .+ deleted/)
@@ -201,13 +201,13 @@ RSpec.shared_examples 'submits edit runner form' do
describe 'runner header', :js do
it 'contains the runner id' do
- expect(page).to have_content("Runner ##{runner.id} created")
+ expect(page).to have_content("##{runner.id} (#{runner.short_sha})")
end
end
context 'when a runner is updated', :js do
before do
- find('[data-testid="runner-field-description"] input').set('new-runner-description')
+ fill_in s_('Runners|Runner description'), with: 'new-runner-description'
click_on _('Save changes')
wait_for_requests
@@ -232,7 +232,7 @@ RSpec.shared_examples 'creates runner and shows register page' do
before do
fill_in s_('Runners|Runner description'), with: 'runner-foo'
fill_in s_('Runners|Tags'), with: 'tag1'
- click_on _('Submit')
+ click_on s_('Runners|Create runner')
wait_for_requests
end
diff --git a/spec/support/shared_examples/features/sidebar/sidebar_labels_shared_examples.rb b/spec/support/shared_examples/features/sidebar/sidebar_labels_shared_examples.rb
index a332fdec963..8ebec19a884 100644
--- a/spec/support/shared_examples/features/sidebar/sidebar_labels_shared_examples.rb
+++ b/spec/support/shared_examples/features/sidebar/sidebar_labels_shared_examples.rb
@@ -47,7 +47,8 @@ RSpec.shared_examples 'labels sidebar widget' do
end
end
- it 'adds first label by pressing enter when search' do
+ it 'adds first label by pressing enter when search',
+ quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/414877' do
within(labels_widget) do
page.within('[data-testid="value-wrapper"]') do
expect(page).not_to have_content(development.name)
diff --git a/spec/support/shared_examples/features/variable_list_shared_examples.rb b/spec/support/shared_examples/features/variable_list_shared_examples.rb
index 1211c9d19e6..3a91b798bbd 100644
--- a/spec/support/shared_examples/features/variable_list_shared_examples.rb
+++ b/spec/support/shared_examples/features/variable_list_shared_examples.rb
@@ -32,7 +32,7 @@ RSpec.shared_examples 'variable list' do
page.within('[data-testid="ci-variable-table"]') do
expect(find(".js-ci-variable-row:nth-child(1) td[data-label='#{s_('CiVariables|Key')}']").text).to eq('key')
- expect(find(".js-ci-variable-row:nth-child(1) td[data-label='#{s_('CiVariables|Options')}']")).to have_content(s_('CiVariables|Protected'))
+ expect(find(".js-ci-variable-row:nth-child(1) td[data-label='#{s_('CiVariables|Attributes')}']")).to have_content(s_('CiVariables|Protected'))
end
end
@@ -47,7 +47,7 @@ RSpec.shared_examples 'variable list' do
page.within('[data-testid="ci-variable-table"]') do
expect(find(".js-ci-variable-row:nth-child(1) td[data-label='#{s_('CiVariables|Key')}']").text).to eq('key')
- expect(find(".js-ci-variable-row:nth-child(1) td[data-label='#{s_('CiVariables|Options')}']")).not_to have_content(s_('CiVariables|Masked'))
+ expect(find(".js-ci-variable-row:nth-child(1) td[data-label='#{s_('CiVariables|Attributes')}']")).not_to have_content(s_('CiVariables|Masked'))
end
end
@@ -116,8 +116,8 @@ RSpec.shared_examples 'variable list' do
wait_for_requests
page.within('[data-testid="ci-variable-table"]') do
- expect(find(".js-ci-variable-row:nth-child(1) td[data-label='#{s_('CiVariables|Options')}']")).to have_content(s_('CiVariables|Protected'))
- expect(find(".js-ci-variable-row:nth-child(1) td[data-label='#{s_('CiVariables|Options')}']")).not_to have_content(s_('CiVariables|Masked'))
+ expect(find(".js-ci-variable-row:nth-child(1) td[data-label='#{s_('CiVariables|Attributes')}']")).to have_content(s_('CiVariables|Protected'))
+ expect(find(".js-ci-variable-row:nth-child(1) td[data-label='#{s_('CiVariables|Attributes')}']")).not_to have_content(s_('CiVariables|Masked'))
end
end
@@ -145,7 +145,7 @@ RSpec.shared_examples 'variable list' do
end
page.within('[data-testid="ci-variable-table"]') do
- expect(find(".js-ci-variable-row:nth-child(1) td[data-label='#{s_('CiVariables|Options')}']")).to have_content(s_('CiVariables|Masked'))
+ expect(find(".js-ci-variable-row:nth-child(1) td[data-label='#{s_('CiVariables|Attributes')}']")).to have_content(s_('CiVariables|Masked'))
end
end
@@ -170,15 +170,13 @@ RSpec.shared_examples 'variable list' do
expect(find('[data-testid="alert-danger"]').text).to have_content('(key) has already been taken')
end
- it 'prevents a variable to be added if no values are provided when a variable is set to masked' do
+ it 'allows variable to be added even if no value is provided' do
click_button('Add variable')
page.within('#add-ci-variable') do
find('[data-testid="pipeline-form-ci-variable-key"] input').set('empty_mask_key')
- find('[data-testid="ci-variable-protected-checkbox"]').click
- find('[data-testid="ci-variable-masked-checkbox"]').click
- expect(find_button('Add variable', disabled: true)).to be_present
+ expect(find_button('Add variable', disabled: false)).to be_present
end
end
@@ -186,7 +184,7 @@ RSpec.shared_examples 'variable list' do
click_button('Add variable')
fill_variable('empty_mask_key', '???', protected: true, masked: true) do
- expect(page).to have_content('This variable can not be masked')
+ expect(page).to have_content('This variable value does not meet the masking requirements.')
expect(find_button('Add variable', disabled: true)).to be_present
end
end
diff --git a/spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb
index c1e4185e058..91cacaf9209 100644
--- a/spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb
+++ b/spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb
@@ -6,9 +6,12 @@
RSpec.shared_examples 'User updates wiki page' do
include WikiHelpers
+ let(:diagramsnet_url) { 'https://embed.diagrams.net' }
before do
sign_in(user)
+ allow(Gitlab::CurrentSettings).to receive(:diagramsnet_enabled).and_return(true)
+ allow(Gitlab::CurrentSettings).to receive(:diagramsnet_url).and_return(diagramsnet_url)
end
context 'when wiki is empty', :js do
@@ -149,7 +152,7 @@ RSpec.shared_examples 'User updates wiki page' do
end
end
- it_behaves_like 'edits content using the content editor'
+ it_behaves_like 'edits content using the content editor', { with_expanded_references: false }
it_behaves_like 'inserts diagrams.net diagram using the content editor'
it_behaves_like 'autocompletes items'
end
@@ -245,7 +248,7 @@ RSpec.shared_examples 'User updates wiki page' do
click_on 'Save changes'
expect(page).to have_content('The form contains the following error:')
- expect(page).to have_content('Content is too long (11 Bytes). The maximum size is 10 Bytes.')
+ expect(page).to have_content('Content is too long (11 B). The maximum size is 10 B.')
end
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 526a56e7dab..128bd28410c 100644
--- a/spec/support/shared_examples/features/work_items_shared_examples.rb
+++ b/spec/support/shared_examples/features/work_items_shared_examples.rb
@@ -32,6 +32,7 @@ end
RSpec.shared_examples 'work items comments' do |type|
let(:form_selector) { '[data-testid="work-item-add-comment"]' }
+ let(:edit_button) { '[data-testid="edit-work-item-note"]' }
let(:textarea_selector) { '[data-testid="work-item-add-comment"] #work-item-add-or-edit-comment' }
let(:is_mac) { page.evaluate_script('navigator.platform').include?('Mac') }
let(:modifier_key) { is_mac ? :command : :control }
@@ -53,21 +54,48 @@ RSpec.shared_examples 'work items comments' do |type|
end
end
+ it 'successfully updates existing comments' do
+ set_comment
+ click_button "Comment"
+ wait_for_all_requests
+
+ find(edit_button).click
+ send_keys(" updated")
+ click_button "Save comment"
+
+ wait_for_all_requests
+
+ page.within(".main-notes-list") do
+ expect(page).to have_content "Test comment updated"
+ end
+ end
+
context 'for work item note actions signed in user with developer role' do
+ let_it_be(:owner) { create(:user) }
+
+ before do
+ project.add_owner(owner)
+ end
+
it 'shows work item note actions' do
set_comment
- click_button "Comment"
-
+ send_keys([modifier_key, :enter])
wait_for_requests
page.within(".main-notes-list") do
+ expect(page).to have_content comment
+ end
+
+ page.within('.timeline-entry.note.note-wrapper.note-comment:last-child') do
expect(page).to have_selector('[data-testid="work-item-note-actions"]')
- find('[data-testid="work-item-note-actions"]', match: :first).click
+ find('[data-testid="work-item-note-actions"]').click
expect(page).to have_selector('[data-testid="copy-link-action"]')
- expect(page).not_to have_selector('[data-testid="assign-note-action"]')
+ expect(page).to have_selector('[data-testid="assign-note-action"]')
+ expect(page).to have_selector('[data-testid="delete-note-action"]')
+ expect(page).to have_selector('[data-testid="edit-work-item-note"]')
end
end
end
@@ -148,7 +176,7 @@ RSpec.shared_examples 'work items assignees' do
find("body").click
wait_for_requests
- expect(work_item.assignees).to include(user)
+ expect(work_item.reload.assignees).to include(user)
end
end
@@ -278,7 +306,6 @@ RSpec.shared_examples 'work items comment actions for guest users' do
expect(page).to have_selector('[data-testid="work-item-note-actions"]')
find('[data-testid="work-item-note-actions"]', match: :first).click
-
expect(page).to have_selector('[data-testid="copy-link-action"]')
expect(page).not_to have_selector('[data-testid="assign-note-action"]')
end
@@ -344,42 +371,56 @@ RSpec.shared_examples 'work items todos' do
end
RSpec.shared_examples 'work items award emoji' do
- let(:award_section_selector) { '[data-testid="work-item-award-list"]' }
- let(:award_action_selector) { '[data-testid="award-button"]' }
- let(:selected_award_action_selector) { '[data-testid="award-button"].selected' }
- let(:emoji_picker_action_selector) { '[data-testid="emoji-picker"]' }
+ let(:award_section_selector) { '.awards' }
+ let(:award_button_selector) { '[data-testid="award-button"]' }
+ let(:selected_award_button_selector) { '[data-testid="award-button"].selected' }
+ let(:emoji_picker_button_selector) { '[data-testid="emoji-picker"]' }
let(:basketball_emoji_selector) { 'gl-emoji[data-name="basketball"]' }
+ let(:tooltip_selector) { '.gl-tooltip' }
def select_emoji
- first(award_action_selector).click
+ page.within(award_section_selector) do
+ page.first(award_button_selector).click
+ end
wait_for_requests
end
- it 'adds award to the work item' do
+ before do
+ emoji_upvote
+ end
+
+ it 'adds award to the work item for current user' do
+ select_emoji
+
within(award_section_selector) do
- select_emoji
+ expect(page).to have_selector(selected_award_button_selector)
- expect(page).to have_selector(selected_award_action_selector)
- expect(first(award_action_selector)).to have_content '1'
+ # As the user2 has already awarded the `:thumbsup:` emoji, the emoji count will be 2
+ expect(first(award_button_selector)).to have_content '2'
end
+ expect(page.find(tooltip_selector)).to have_content("You and John reacted with :thumbsup:")
end
- it 'removes award from work item' do
- within(award_section_selector) do
- select_emoji
+ it 'removes award from work item for current user' do
+ select_emoji
- expect(first(award_action_selector)).to have_content '1'
+ page.within(award_section_selector) do
+ # As the user2 has already awarded the `:thumbsup:` emoji, the emoji count will be 2
+ expect(first(award_button_selector)).to have_content '2'
+ end
- select_emoji
+ select_emoji
- expect(first(award_action_selector)).to have_content '0'
+ page.within(award_section_selector) do
+ # The emoji count will be back to 1
+ expect(first(award_button_selector)).to have_content '1'
end
end
- it 'add custom award to the work item' do
+ it 'add custom award to the work item for current user' do
within(award_section_selector) do
- find(emoji_picker_action_selector).click
+ find(emoji_picker_button_selector).click
find(basketball_emoji_selector).click
expect(page).to have_selector(basketball_emoji_selector)
diff --git a/spec/support/shared_examples/graphql/mutations/can_mutate_spammable_examples.rb b/spec/support/shared_examples/graphql/mutations/can_mutate_spammable_examples.rb
deleted file mode 100644
index b17e59f0797..00000000000
--- a/spec/support/shared_examples/graphql/mutations/can_mutate_spammable_examples.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.shared_examples 'a mutation which can mutate a spammable' do
- describe "#spam_params" do
- it 'passes spam params to the service constructor' do
- args = [
- project: anything,
- current_user: anything,
- params: anything,
- spam_params: instance_of(::Spam::SpamParams)
- ]
- expect(service).to receive(:new).with(*args).and_call_original
-
- subject
- end
- end
-end
diff --git a/spec/support/shared_examples/graphql/notes_on_noteables_shared_examples.rb b/spec/support/shared_examples/graphql/notes_on_noteables_shared_examples.rb
index 99d122e8254..64f811771ec 100644
--- a/spec/support/shared_examples/graphql/notes_on_noteables_shared_examples.rb
+++ b/spec/support/shared_examples/graphql/notes_on_noteables_shared_examples.rb
@@ -20,6 +20,14 @@ RSpec.shared_context 'exposing regular notes on a noteable in GraphQL' do
edges {
node {
#{all_graphql_fields_for('Note', max_depth: 1)}
+ awardEmoji {
+ nodes {
+ name
+ user {
+ name
+ }
+ }
+ }
}
}
}
@@ -40,6 +48,27 @@ RSpec.shared_context 'exposing regular notes on a noteable in GraphQL' do
expect(noteable_data['notes']['edges'].first['node']['body'])
.to eq(note.note)
end
+
+ it 'avoids N+1 queries' do
+ create(:award_emoji, awardable: note, name: 'star', user: user)
+ another_user = create(:user).tap { |u| note.resource_parent.add_developer(u) }
+ create(:note, project: note.project, noteable: noteable, author: another_user)
+
+ post_graphql(query, current_user: user)
+
+ control = ActiveRecord::QueryRecorder.new { post_graphql(query, current_user: user) }
+
+ expect_graphql_errors_to_be_empty
+
+ another_note = create(:note, project: note.project, noteable: noteable, author: user)
+ create(:award_emoji, awardable: another_note, name: 'star', user: user)
+ another_user = create(:user).tap { |u| note.resource_parent.add_developer(u) }
+ note_with_different_user = create(:note, project: note.project, noteable: noteable, author: another_user)
+ create(:award_emoji, awardable: note_with_different_user, name: 'star', user: user)
+
+ expect { post_graphql(query, current_user: user) }.not_to exceed_query_limit(control)
+ expect_graphql_errors_to_be_empty
+ end
end
context "for discussions" do
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 52908c5b6df..30212e44c6a 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
@@ -172,6 +172,25 @@ RSpec.shared_examples 'work item supports type change via quick actions' do
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
+ 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
+
context 'when quick command for unsupported widget is present' do
let(:body) { "\n/type Issue\n/assign @#{assignee.username}" }
diff --git a/spec/support/shared_examples/graphql/resolvers/releases_resolvers_shared_examples.rb b/spec/support/shared_examples/graphql/resolvers/releases_resolvers_shared_examples.rb
index 0e09a9d9e66..a1fa263c524 100644
--- a/spec/support/shared_examples/graphql/resolvers/releases_resolvers_shared_examples.rb
+++ b/spec/support/shared_examples/graphql/resolvers/releases_resolvers_shared_examples.rb
@@ -4,8 +4,8 @@ RSpec.shared_examples 'releases and group releases resolver' do
context 'when the user does not have access to the project' do
let(:current_user) { public_user }
- it 'returns an empty array' do
- expect(resolve_releases).to be_empty
+ it 'returns an empty response' do
+ expect(resolve_releases).to be_blank
end
end
diff --git a/spec/support/shared_examples/graphql/types/merge_request_interactions_type_shared_examples.rb b/spec/support/shared_examples/graphql/types/merge_request_interactions_type_shared_examples.rb
index 3dffc2066ae..d8cc6f697d7 100644
--- a/spec/support/shared_examples/graphql/types/merge_request_interactions_type_shared_examples.rb
+++ b/spec/support/shared_examples/graphql/types/merge_request_interactions_type_shared_examples.rb
@@ -42,7 +42,14 @@ RSpec.shared_examples "a user type with merge request interaction type" do
profileEnableGitpodPath
savedReplies
savedReply
- user_achievements
+ userAchievements
+ bio
+ linkedin
+ twitter
+ discord
+ organization
+ jobTitle
+ createdAt
]
# TODO: 'workspaces' needs to be included, but only when this spec is run in EE context, to account for the
diff --git a/spec/support/shared_examples/integrations/integration_settings_form.rb b/spec/support/shared_examples/integrations/integration_settings_form.rb
index c43bdfa53ff..1d7f74837f2 100644
--- a/spec/support/shared_examples/integrations/integration_settings_form.rb
+++ b/spec/support/shared_examples/integrations/integration_settings_form.rb
@@ -20,6 +20,8 @@ RSpec.shared_examples 'integration settings form' do
fields = parse_json(fields_for_integration(integration))
fields.each do |field|
+ next if exclude_field?(integration, field)
+
field_name = field[:name]
expect(page).to have_field(field[:title], wait: 0),
"#{integration.title} field #{field_name} not present"
@@ -54,6 +56,11 @@ RSpec.shared_examples 'integration settings form' do
Gitlab::Json.parse(json, symbolize_names: true)
end
+ # Fields that have specific handling on the frontend
+ def exclude_field?(integration, field)
+ integration.is_a?(Integrations::Jira) && field[:name] == 'jira_auth_type'
+ end
+
def trigger_event_title(name)
# Should match `integrationTriggerEventTitles` in app/assets/javascripts/integrations/constants.js
event_titles = {
diff --git a/spec/support/shared_examples/lib/api/ai_workhorse_shared_examples.rb b/spec/support/shared_examples/lib/api/ai_workhorse_shared_examples.rb
index 7ace223723c..d4fe45a91a0 100644
--- a/spec/support/shared_examples/lib/api/ai_workhorse_shared_examples.rb
+++ b/spec/support/shared_examples/lib/api/ai_workhorse_shared_examples.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-RSpec.shared_examples 'delegates AI request to Workhorse' do |provider_flag|
+RSpec.shared_examples 'behind AI related feature flags' do |provider_flag|
context "when #{provider_flag} is disabled" do
before do
stub_feature_flags(provider_flag => false)
@@ -24,7 +24,9 @@ RSpec.shared_examples 'delegates AI request to Workhorse' do |provider_flag|
expect(response).to have_gitlab_http_status(:not_found)
end
end
+end
+RSpec.shared_examples 'delegates AI request to Workhorse' do
it 'responds with Workhorse send-url headers' do
post api(url, current_user), params: input_params
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
new file mode 100644
index 00000000000..0472bb87e62
--- /dev/null
+++ b/spec/support/shared_examples/lib/gitlab/cache/json_cache_shared_examples.rb
@@ -0,0 +1,379 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'Json Cache class' do
+ describe '#read' do
+ it 'returns the cached value when there is data in the cache with the given key' do
+ allow(backend).to receive(:read).with(expanded_key).and_return(json_value(true))
+
+ expect(cache.read(key)).to eq(true)
+ end
+
+ it 'returns nil when there is no data in the cache with the given key' do
+ allow(backend).to receive(:read).with(expanded_key).and_return(nil)
+
+ expect(Gitlab::Json).not_to receive(:parse)
+ expect(cache.read(key)).to be_nil
+ end
+
+ 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)
+ end
+
+ it 'returns nil when klass is nil' do
+ allow(backend).to receive(:read).with(expanded_key).and_return(json_value(broadcast_message))
+
+ expect(cache.read(key)).to be_nil
+ end
+
+ 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)
+ 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)
+ end
+ end
+
+ context 'when the cached value is a JSON false value' 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)
+ end
+ end
+
+ context 'when the cached value is a hash' 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
+ 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
+ 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)
+
+ BroadcastMessage.cached_markdown_fields.html_fields.each do |field|
+ expect(result.public_send(field)).to be_nil
+ end
+ end
+ end
+
+ context 'when the cached value is an array' 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])
+ end
+
+ it 'returns an empty array when klass is nil' do
+ allow(backend).to receive(:read).with(expanded_key).and_return(json_value([broadcast_message]))
+
+ expect(cache.read(key)).to eq([])
+ end
+
+ 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
+ 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([])
+ 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])
+ end
+ end
+ end
+
+ describe '#write' do
+ it 'writes value to the cache with the given key' do
+ cache.write(key, true)
+
+ expect(backend).to have_received(:write).with(expanded_key, json_value(true), nil)
+ end
+
+ it 'writes a string containing a JSON representation of the value to the cache' do
+ cache.write(key, broadcast_message)
+
+ expect(backend).to have_received(:write).with(expanded_key, json_value(broadcast_message), nil)
+ end
+
+ it 'passes options the underlying cache implementation' do
+ cache.write(key, true, expires_in: 15.seconds)
+
+ expect(backend).to have_received(:write).with(expanded_key, json_value(true), expires_in: 15.seconds)
+ end
+
+ it 'passes options the underlying cache implementation when options is empty' do
+ cache.write(key, true, {})
+
+ expect(backend).to have_received(:write).with(expanded_key, json_value(true), {})
+ end
+
+ it 'passes options the underlying cache implementation when options is nil' do
+ cache.write(key, true, nil)
+
+ expect(backend).to have_received(:write).with(expanded_key, json_value(true), nil)
+ end
+ end
+
+ # rubocop:disable Style/RedundantFetchBlock
+ describe '#fetch', :use_clean_rails_memory_store_caching do
+ let(:backend) { Rails.cache }
+
+ it 'requires a block' do
+ expect { cache.fetch(key) }.to raise_error(LocalJumpError)
+ end
+
+ it 'passes options the underlying cache implementation' do
+ expect(backend).to receive(:write).with(expanded_key, json_value(true), { expires_in: 15.seconds })
+
+ cache.fetch(key, { expires_in: 15.seconds }) { true }
+ end
+
+ context 'when the given key does not exist in the cache' do
+ context 'when the result of the block is truthy' do
+ it 'returns the result of the block' do
+ result = cache.fetch(key) { true }
+
+ expect(result).to eq(true)
+ end
+
+ it 'caches the value' do
+ expect(backend).to receive(:write).with(expanded_key, json_value(true), {})
+
+ cache.fetch(key) { true }
+ end
+ end
+
+ context 'when the result of the block is false' do
+ it 'returns the result of the block' do
+ result = cache.fetch(key) { false }
+
+ expect(result).to eq(false)
+ end
+
+ it 'caches the value' do
+ expect(backend).to receive(:write).with(expanded_key, json_value(false), {})
+
+ cache.fetch(key) { false }
+ end
+ end
+
+ context 'when the result of the block is nil' do
+ it 'returns the result of the block' do
+ result = cache.fetch(key) { nil }
+
+ expect(result).to eq(nil)
+ end
+
+ it 'caches the value' do
+ expect(backend).to receive(:write).with(expanded_key, json_value(nil), {})
+
+ cache.fetch(key) { nil }
+ end
+ end
+ end
+
+ context 'when the given key exists in the cache' do
+ context 'when the cached value is a hash' do
+ before do
+ backend.write(expanded_key, json_value(broadcast_message))
+ end
+
+ it 'parses the cached value' do
+ result = cache.fetch(key, as: BroadcastMessage) { 'block result' }
+
+ expect(result).to eq(broadcast_message)
+ end
+
+ it 'decodes enums correctly' do
+ result = cache.fetch(key, as: 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' }
+
+ expect(result).to be_persisted
+ end
+
+ 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' }
+
+ expect(result).to be_new_record
+ end
+
+ 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' }
+
+ expect(result).to be_new_record
+ end
+
+ 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' }
+
+ expect(result).to eq 'block result'
+ end
+
+ 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)
+ 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' }
+
+ expect(result).to eq 'block result'
+ 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.fetch(key, as: BroadcastMessage) { 'block result' }
+
+ BroadcastMessage.cached_markdown_fields.html_fields.each do |field|
+ expect(result.public_send(field)).to be_nil
+ end
+ end
+ end
+
+ it 'returns the result of the block when `as` option is nil' do
+ result = cache.fetch(key, as: nil) { 'block result' }
+
+ expect(result).to eq('block result')
+ end
+
+ it 'returns the result of the block when `as` option is missing' do
+ result = cache.fetch(key) { 'block result' }
+
+ expect(result).to eq('block result')
+ end
+ end
+
+ context 'when the cached value is a array' do
+ before do
+ backend.write(expanded_key, json_value([broadcast_message]))
+ end
+
+ it 'parses the cached value' do
+ result = cache.fetch(key, as: BroadcastMessage) { 'block result' }
+
+ expect(result).to eq([broadcast_message])
+ end
+
+ it 'returns an empty array when `as` option is nil' do
+ result = cache.fetch(key, as: nil) { 'block result' }
+
+ expect(result).to eq([])
+ end
+
+ it 'returns an empty array when `as` option is not provided' do
+ result = cache.fetch(key) { 'block result' }
+
+ expect(result).to eq([])
+ end
+ end
+
+ context 'when the cached value is true' do
+ before do
+ backend.write(expanded_key, json_value(true))
+ end
+
+ it 'returns the cached value' do
+ result = cache.fetch(key) { 'block result' }
+
+ expect(result).to eq(true)
+ end
+
+ it 'does not execute the block' do
+ expect { |block| cache.fetch(key, &block) }.not_to yield_control
+ end
+
+ it 'does not write to the cache' do
+ expect(backend).not_to receive(:write)
+
+ cache.fetch(key) { 'block result' }
+ end
+ end
+
+ context 'when the cached value is false' do
+ before do
+ backend.write(expanded_key, json_value(false))
+ end
+
+ it 'returns the cached value' do
+ result = cache.fetch(key) { 'block result' }
+
+ expect(result).to eq(false)
+ end
+
+ it 'does not execute the block' do
+ expect { |block| cache.fetch(key, &block) }.not_to yield_control
+ end
+
+ it 'does not write to the cache' do
+ expect(backend).not_to receive(:write)
+
+ cache.fetch(key) { 'block result' }
+ end
+ end
+
+ context 'when the cached value is nil' do
+ before do
+ backend.write(expanded_key, json_value(nil))
+ end
+
+ it 'returns the result of the block' do
+ result = cache.fetch(key) { 'block result' }
+
+ expect(result).to eq('block result')
+ end
+
+ it 'writes the result of the block to the cache' do
+ expect(backend).to receive(:write).with(expanded_key, json_value('block result'), {})
+
+ cache.fetch(key) { 'block result' }
+ end
+ end
+ end
+ end
+ # rubocop:enable Style/RedundantFetchBlock
+end
diff --git a/spec/support/shared_examples/lib/gitlab/database/foreign_key_validators_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/database/foreign_key_validators_shared_examples.rb
new file mode 100644
index 00000000000..a1e75e4af7e
--- /dev/null
+++ b/spec/support/shared_examples/lib/gitlab/database/foreign_key_validators_shared_examples.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.shared_examples 'foreign key validators' do |validator, expected_result|
+ subject(:result) { validator.new(structure_file, database).execute }
+
+ let(:structure_file_path) { Rails.root.join('spec/fixtures/structure.sql') }
+ let(:structure_file) { Gitlab::Database::SchemaValidation::StructureSql.new(structure_file_path, schema) }
+ let(:inconsistency_type) { validator.name.demodulize.underscore }
+ let(:database_name) { 'main' }
+ let(:schema) { 'public' }
+ let(:database_model) { Gitlab::Database.database_base_models[database_name] }
+ let(:connection) { database_model.connection }
+ let(:database) { Gitlab::Database::SchemaValidation::Database.new(connection) }
+
+ let(:database_query) do
+ [
+ {
+ 'schema' => schema,
+ 'table_name' => 'web_hooks',
+ 'foreign_key_name' => 'web_hooks_project_id_fkey',
+ 'foreign_key_definition' => 'FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE'
+ },
+ {
+ 'schema' => schema,
+ 'table_name' => 'issues',
+ 'foreign_key_name' => 'wrong_definition_fk',
+ 'foreign_key_definition' => 'FOREIGN KEY (author_id) REFERENCES users(id) ON DELETE CASCADE'
+ },
+ {
+ 'schema' => schema,
+ 'table_name' => 'projects',
+ 'foreign_key_name' => 'extra_fk',
+ 'foreign_key_definition' => 'FOREIGN KEY (creator_id) REFERENCES users(id) ON DELETE CASCADE'
+ }
+ ]
+ end
+
+ before do
+ allow(connection).to receive(:exec_query).and_return(database_query)
+ end
+
+ it 'returns trigger inconsistencies' do
+ expect(result.map(&:object_name)).to match_array(expected_result)
+ expect(result.map(&:type)).to all(eql inconsistency_type)
+ end
+end
diff --git a/spec/support/shared_examples/lib/gitlab/position_formatters_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/position_formatters_shared_examples.rb
index c9300aff3e6..1e03ddac42e 100644
--- a/spec/support/shared_examples/lib/gitlab/position_formatters_shared_examples.rb
+++ b/spec/support/shared_examples/lib/gitlab/position_formatters_shared_examples.rb
@@ -2,10 +2,9 @@
RSpec.shared_examples "position formatter" do
let(:formatter) { described_class.new(attrs) }
+ let(:key) { [123, 456, 789, Digest::SHA1.hexdigest(formatter.old_path), Digest::SHA1.hexdigest(formatter.new_path), 1, 2] }
describe '#key' do
- let(:key) { [123, 456, 789, Digest::SHA1.hexdigest(formatter.old_path), Digest::SHA1.hexdigest(formatter.new_path), 1, 2] }
-
subject { formatter.key }
it { is_expected.to eq(key) }
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
new file mode 100644
index 00000000000..7bcefd07fc4
--- /dev/null
+++ b/spec/support/shared_examples/lib/gitlab/search_archived_filter_shared_examples.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'search results filtered by archived' do
+ 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
+ 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
+ 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
+ end
+ end
+end
diff --git a/spec/support/shared_examples/lib/gitlab/search_labels_filter_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/search_labels_filter_shared_examples.rb
new file mode 100644
index 00000000000..b7e408415c3
--- /dev/null
+++ b/spec/support/shared_examples/lib/gitlab/search_labels_filter_shared_examples.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'search results filtered by labels' do
+ let(:project_label) { create(:label, project: project) }
+ let!(:issue_1) { create(:labeled_issue, labels: [project_label], project: project, title: 'foo project') }
+ let!(:unlabeled_issue) { create(:issue, project: project, title: 'foo unlabeled') }
+
+ let(:filters) { { labels: [project_label.id] } }
+
+ before do
+ ensure_elasticsearch_index!
+ end
+
+ subject(:issue_results) { results.objects(scope) }
+
+ it 'filters by labels', :sidekiq_inline do
+ expect(issue_results).to contain_exactly(issue_1)
+ end
+end
diff --git a/spec/support/shared_examples/lib/sentry/client_shared_examples.rb b/spec/support/shared_examples/lib/sentry/client_shared_examples.rb
index fa3e9bf5340..842801708d0 100644
--- a/spec/support/shared_examples/lib/sentry/client_shared_examples.rb
+++ b/spec/support/shared_examples/lib/sentry/client_shared_examples.rb
@@ -92,7 +92,7 @@ RSpec.shared_examples 'Sentry API response size limit' do
it 'raises an exception when response is too large' do
expect { subject }.to raise_error(
ErrorTracking::SentryClient::ResponseInvalidSizeError,
- 'Sentry API response is too big. Limit is 1 MB.'
+ 'Sentry API response is too big. Limit is 1 MiB.'
)
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 c07d1552ba2..dc92e56d013 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
@@ -2,7 +2,7 @@
RSpec.shared_examples 'store ActiveRecord info in RequestStore' do |db_role|
let(:db_config_name) do
- db_config_name = ::Gitlab::Database.db_config_names.first
+ db_config_name = ::Gitlab::Database.db_config_names(with_schema: :gitlab_shared).first
db_config_name += "_replica" if db_role == :secondary
db_config_name
end
@@ -96,7 +96,7 @@ end
RSpec.shared_examples 'record ActiveRecord metrics in a metrics transaction' do |db_role|
let(:db_config_name) do
- db_config_name = ::Gitlab::Database.db_config_names.first
+ db_config_name = ::Gitlab::Database.db_config_names(with_schema: :gitlab_shared).first
db_config_name += "_replica" if db_role == :secondary
db_config_name
end
diff --git a/spec/support/shared_examples/models/chat_integration_shared_examples.rb b/spec/support/shared_examples/models/chat_integration_shared_examples.rb
index addd37cde32..0ce54fbc31f 100644
--- a/spec/support/shared_examples/models/chat_integration_shared_examples.rb
+++ b/spec/support/shared_examples/models/chat_integration_shared_examples.rb
@@ -165,7 +165,7 @@ RSpec.shared_examples "chat integration" do |integration_name|
context "with issue events" do
let(:opts) { { title: "Awesome issue", description: "please fix" } }
let(:sample_data) do
- service = Issues::CreateService.new(container: project, current_user: user, params: opts, spam_params: nil)
+ service = Issues::CreateService.new(container: project, current_user: user, params: opts)
issue = service.execute[:issue]
service.hook_data(issue, "open")
end
diff --git a/spec/support/shared_examples/models/ci/token_format_shared_examples.rb b/spec/support/shared_examples/models/ci/token_format_shared_examples.rb
index 0272982e2d0..7aa7d2be520 100644
--- a/spec/support/shared_examples/models/ci/token_format_shared_examples.rb
+++ b/spec/support/shared_examples/models/ci/token_format_shared_examples.rb
@@ -18,12 +18,6 @@ RSpec.shared_examples_for 'ensures runners_token is prefixed' do |factory|
it 'generates runners_token which starts with runner prefix' do
expect(record.runners_token).to match(a_string_starting_with(runners_prefix))
end
-
- it 'changes the attribute values for runners_token and runners_token_encrypted' do
- expect { record.runners_token }
- .to change { record[:runners_token] }.from(invalid_runners_token).to(nil)
- .and change { record[:runners_token_encrypted] }.from(nil)
- end
end
end
end
diff --git a/spec/support/shared_examples/models/concerns/participable_shared_examples.rb b/spec/support/shared_examples/models/concerns/participable_shared_examples.rb
index ec7a9105bb2..f772cfc6bbd 100644
--- a/spec/support/shared_examples/models/concerns/participable_shared_examples.rb
+++ b/spec/support/shared_examples/models/concerns/participable_shared_examples.rb
@@ -10,13 +10,14 @@ RSpec.shared_examples 'visible participants for issuable with read ability' do |
allow(model).to receive(:participant_attrs).and_return([:bar])
end
- shared_examples 'check for participables read ability' do |ability_name|
+ shared_examples 'check for participables read ability' do |ability_name, ability_source: nil|
it 'receives expected ability' do
instance = model.new
+ source = ability_source == :participable_source ? participable_source : instance
allow(instance).to receive(:bar).and_return(participable_source)
- expect(Ability).to receive(:allowed?).with(anything, ability_name, instance)
+ expect(Ability).to receive(:allowed?).with(anything, ability_name, source)
expect(instance.visible_participants(user1)).to be_empty
end
@@ -39,4 +40,10 @@ RSpec.shared_examples 'visible participants for issuable with read ability' do |
it_behaves_like 'check for participables read ability', :read_internal_note
end
+
+ context 'when source is a system note' do
+ let(:participable_source) { build(:system_note) }
+
+ it_behaves_like 'check for participables read ability', :read_note, ability_source: :participable_source
+ end
end
diff --git a/spec/support/shared_examples/models/mentionable_shared_examples.rb b/spec/support/shared_examples/models/mentionable_shared_examples.rb
index f9612dd61be..9874db8dbd7 100644
--- a/spec/support/shared_examples/models/mentionable_shared_examples.rb
+++ b/spec/support/shared_examples/models/mentionable_shared_examples.rb
@@ -208,13 +208,13 @@ end
RSpec.shared_examples 'mentions in description' do |mentionable_type|
context 'when storing user mentions' do
- before do
- mentionable.store_mentions!
- end
-
context 'when mentionable description has no mentions' do
let(:mentionable) { create(mentionable_type, description: "just some description") }
+ before do
+ mentionable.store_mentions!
+ end
+
it 'stores no mentions' do
expect(mentionable.user_mentions.count).to eq 0
end
@@ -228,13 +228,49 @@ RSpec.shared_examples 'mentions in description' do |mentionable_type|
let(:mentionable_desc) { "#{user.to_reference} #{user2.to_reference} #{user.to_reference} some description #{group.to_reference(full: true)} and #{user2.to_reference} @all" }
let(:mentionable) { create(mentionable_type, description: mentionable_desc) }
- it 'stores mentions' do
- add_member(user)
+ context 'when `disable_all_mention` FF is disabled' do
+ before do
+ stub_feature_flags(disable_all_mention: false)
- expect(mentionable.user_mentions.count).to eq 1
- expect(mentionable.referenced_users).to match_array([user, user2])
- expect(mentionable.referenced_projects(user)).to match_array([mentionable.project].compact) # epic.project is nil, and we want empty []
- expect(mentionable.referenced_groups(user)).to match_array([group])
+ mentionable.store_mentions!
+ end
+
+ it 'stores mentions' do
+ add_member(user)
+
+ expect(mentionable.user_mentions.count).to eq 1
+ expect(mentionable.referenced_users).to match_array([user, user2])
+ expect(mentionable.referenced_groups(user)).to match_array([group])
+
+ # NOTE: https://gitlab.com/gitlab-org/gitlab/-/issues/18442
+ #
+ # We created `Mentions` concern to track every note in which usernames are mentioned
+ # However, we never got to the point of utilizing the concern and its DB tables.
+ # See: https://gitlab.com/gitlab-org/gitlab/-/issues/21801
+ #
+ # The following test is checking `@all`, a type of user mention, is recording
+ # the id of the project for the mentionable that has the `@all` mention.
+ # It's _surmised_ that the original intent was
+ # the project id would be useful to store so everyone (@all) in the project -
+ # could be notified using its mention record only.
+ expect(mentionable.referenced_projects(user)).to match_array([mentionable.project].compact) # epic.project is nil, and we want empty []
+ end
+ end
+
+ context 'when `disable_all_mention` FF is enabled' do
+ before do
+ stub_feature_flags(disable_all_mention: true)
+
+ mentionable.store_mentions!
+ end
+
+ it 'stores mentions' do
+ add_member(user)
+
+ expect(mentionable.user_mentions.count).to eq 1
+ expect(mentionable.referenced_users).to match_array([user, user2])
+ expect(mentionable.referenced_groups(user)).to match_array([group])
+ end
end
end
end
@@ -248,17 +284,37 @@ RSpec.shared_examples 'mentions in notes' do |mentionable_type|
let(:note_desc) { "#{user.to_reference} #{user2.to_reference} #{user.to_reference} and #{group.to_reference(full: true)} and #{user2.to_reference} @all" }
let!(:mentionable) { note.noteable }
- before do
- note.update!(note: note_desc)
- note.store_mentions!
- add_member(user)
+ context 'when `disable_all_mention` FF is enabled' do
+ before do
+ stub_feature_flags(disable_all_mention: true)
+
+ note.update!(note: note_desc)
+ note.store_mentions!
+ add_member(user)
+ end
+
+ it 'returns all mentionable mentions' do
+ expect(mentionable.user_mentions.count).to eq 1
+ expect(mentionable.referenced_users).to match_array([user, user2])
+ expect(mentionable.referenced_groups(user)).to eq [group]
+ end
end
- it 'returns all mentionable mentions' do
- expect(mentionable.user_mentions.count).to eq 1
- expect(mentionable.referenced_users).to match_array([user, user2])
- expect(mentionable.referenced_projects(user)).to eq [mentionable.project].compact # epic.project is nil, and we want empty []
- expect(mentionable.referenced_groups(user)).to eq [group]
+ context 'when `disable_all_mention` FF is disabled' do
+ before do
+ stub_feature_flags(disable_all_mention: false)
+
+ note.update!(note: note_desc)
+ note.store_mentions!
+ add_member(user)
+ end
+
+ it 'returns all mentionable mentions' do
+ expect(mentionable.user_mentions.count).to eq 1
+ expect(mentionable.referenced_users).to match_array([user, user2])
+ expect(mentionable.referenced_groups(user)).to eq [group]
+ expect(mentionable.referenced_projects(user)).to eq [mentionable.project].compact # epic.project is nil, and we want empty []
+ end
end
if [:epic, :issue].include?(mentionable_type)
@@ -268,6 +324,9 @@ RSpec.shared_examples 'mentions in notes' do |mentionable_type|
let(:note_desc) { "#{guest.to_reference} and #{user2.to_reference} and #{user.to_reference}" }
before do
+ note.update!(note: note_desc)
+ note.store_mentions!
+ add_member(user)
note.resource_parent.add_reporter(user2)
note.resource_parent.add_guest(guest)
# Bypass :confidential update model validation for testing purposes
@@ -283,13 +342,15 @@ RSpec.shared_examples 'mentions in notes' do |mentionable_type|
end
RSpec.shared_examples 'load mentions from DB' do |mentionable_type|
- context 'load stored mentions' do
+ context 'load stored mentions (when `disable_all_mention` is disabled)' do
let_it_be(:user) { create(:user) }
let_it_be(:mentioned_user) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:note_desc) { "#{mentioned_user.to_reference} and #{group.to_reference(full: true)} and @all" }
before do
+ stub_feature_flags(disable_all_mention: false)
+
note.update!(note: note_desc)
note.store_mentions!
add_member(user)
@@ -341,6 +402,7 @@ RSpec.shared_examples 'load mentions from DB' do |mentionable_type|
let(:group_member) { create(:group_member, user: create(:user), group: private_group) }
before do
+ stub_feature_flags(disable_all_mention: false)
user_mention = note.user_mentions.first
mention_ids = {
mentioned_projects_ids: user_mention.mentioned_projects_ids.to_a << private_project.id,
@@ -368,6 +430,99 @@ RSpec.shared_examples 'load mentions from DB' do |mentionable_type|
end
end
end
+
+ context 'when `disable_all_mention` is enabled' do
+ context 'load stored mentions' do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:mentioned_user) { create(:user) }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:note_desc) { "#{mentioned_user.to_reference} and #{group.to_reference(full: true)} and @all" }
+
+ before do
+ stub_feature_flags(disable_all_mention: true)
+
+ note.update!(note: note_desc)
+ note.store_mentions!
+ add_member(user)
+ end
+
+ context 'when stored user mention contains ids of inexistent records' do
+ before do
+ user_mention = note.user_mentions.first
+ mention_ids = {
+ mentioned_users_ids: user_mention.mentioned_users_ids.to_a << non_existing_record_id,
+ mentioned_groups_ids: user_mention.mentioned_groups_ids.to_a << non_existing_record_id
+ }
+ user_mention.update!(mention_ids)
+ end
+
+ it 'filters out inexistent mentions' do
+ expect(mentionable.referenced_users).to match_array([mentioned_user])
+ expect(mentionable.referenced_projects(user)).to be_empty
+ expect(mentionable.referenced_groups(user)).to match_array([group])
+ end
+ end
+
+ if [:epic, :issue].include?(mentionable_type)
+ context 'and note is confidential' do
+ let_it_be(:guest) { create(:user) }
+
+ let(:note_desc) { "#{guest.to_reference} and #{mentioned_user.to_reference}" }
+
+ before do
+ note.resource_parent.add_reporter(mentioned_user)
+ note.resource_parent.add_guest(guest)
+ # Bypass :confidential update model validation for testing purposes
+ note.update_attribute(:confidential, true)
+ note.store_mentions!
+ end
+
+ it 'stores only mentioned users that has permissions' do
+ expect(mentionable.referenced_users).to contain_exactly(mentioned_user)
+ end
+ end
+ end
+
+ context 'when private projects and groups are mentioned' do
+ let(:mega_user) { create(:user) }
+ let(:private_project) { create(:project, :private) }
+ let(:project_member) { create(:project_member, user: create(:user), project: private_project) }
+ let(:private_group) { create(:group, :private) }
+ let(:group_member) { create(:group_member, user: create(:user), group: private_group) }
+
+ before do
+ user_mention = note.user_mentions.first
+ mention_ids = {
+ mentioned_projects_ids: user_mention.mentioned_projects_ids.to_a << private_project.id,
+ mentioned_groups_ids: user_mention.mentioned_groups_ids.to_a << private_group.id
+ }
+ user_mention.update!(mention_ids)
+ end
+
+ context 'when user has no access to some mentions' do
+ it 'filters out inaccessible mentions' do
+ expect(mentionable.referenced_projects(user)).to be_empty
+ expect(mentionable.referenced_groups(user)).to match_array([group])
+ end
+ end
+
+ context 'when user has access to the private project and group mentions' do
+ let(:user) { mega_user }
+
+ before do
+ add_member(user)
+ private_project.add_developer(user)
+ private_group.add_developer(user)
+ end
+
+ it 'returns all mentions' do
+ expect(mentionable.referenced_projects(user)).to match_array([private_project])
+ expect(mentionable.referenced_groups(user)).to match_array([group, private_group])
+ end
+ end
+ end
+ end
+ end
end
def add_member(user)
diff --git a/spec/support/shared_examples/models/wiki_shared_examples.rb b/spec/support/shared_examples/models/wiki_shared_examples.rb
index 017e51ecd24..a0187252108 100644
--- a/spec/support/shared_examples/models/wiki_shared_examples.rb
+++ b/spec/support/shared_examples/models/wiki_shared_examples.rb
@@ -939,7 +939,6 @@ RSpec.shared_examples 'wiki model' do
end
describe '#create_wiki_repository' do
- let(:head_path) { Gitlab::GitalyClient::StorageSettings.allow_disk_access { Rails.root.join(TestEnv.repos_path, "#{wiki.disk_path}.git", 'HEAD') } }
let(:default_branch) { 'foo' }
before do
@@ -956,7 +955,7 @@ RSpec.shared_examples 'wiki model' do
subject
- expect(File.read(head_path).squish).to eq "ref: refs/heads/#{default_branch}"
+ expect(wiki.repository.raw.root_ref(head_only: true)).to eq default_branch
end
end
@@ -968,7 +967,7 @@ RSpec.shared_examples 'wiki model' do
subject
- expect(File.read(head_path).squish).to eq "ref: refs/heads/#{default_branch}"
+ expect(wiki.repository.raw.root_ref(head_only: true)).to eq default_branch
end
end
end
diff --git a/spec/support/shared_examples/namespaces/traversal_scope_examples.rb b/spec/support/shared_examples/namespaces/traversal_scope_examples.rb
index 4afed5139d8..0c4e5ce51fc 100644
--- a/spec/support/shared_examples/namespaces/traversal_scope_examples.rb
+++ b/spec/support/shared_examples/namespaces/traversal_scope_examples.rb
@@ -139,29 +139,10 @@ RSpec.shared_examples 'namespace traversal scopes' do
end
describe '.self_and_ancestors' do
- context "use_traversal_ids_ancestor_scopes feature flag is true" do
- before do
- stub_feature_flags(use_traversal_ids: true)
- stub_feature_flags(use_traversal_ids_for_ancestor_scopes: true)
- end
-
- it_behaves_like '.self_and_ancestors'
-
- it 'not make recursive queries' do
- expect { described_class.where(id: [nested_group_1]).self_and_ancestors.load }.not_to make_queries_matching(/WITH RECURSIVE/)
- end
- end
-
- context "use_traversal_ids_ancestor_scopes feature flag is false" do
- before do
- stub_feature_flags(use_traversal_ids_for_ancestor_scopes: false)
- end
+ it_behaves_like '.self_and_ancestors'
- it_behaves_like '.self_and_ancestors'
-
- it 'makes recursive queries' do
- expect { described_class.where(id: [nested_group_1]).self_and_ancestors.load }.to make_queries_matching(/WITH RECURSIVE/)
- end
+ it 'not make recursive queries' do
+ expect { described_class.where(id: [nested_group_1]).self_and_ancestors.load }.not_to make_queries_matching(/WITH RECURSIVE/)
end
end
@@ -197,29 +178,10 @@ RSpec.shared_examples 'namespace traversal scopes' do
end
describe '.self_and_ancestor_ids' do
- context "use_traversal_ids_ancestor_scopes feature flag is true" do
- before do
- stub_feature_flags(use_traversal_ids: true)
- stub_feature_flags(use_traversal_ids_for_ancestor_scopes: true)
- end
-
- it_behaves_like '.self_and_ancestor_ids'
-
- it 'makes recursive queries' do
- expect { described_class.where(id: [nested_group_1]).self_and_ancestor_ids.load }.not_to make_queries_matching(/WITH RECURSIVE/)
- end
- end
-
- context "use_traversal_ids_ancestor_scopes feature flag is false" do
- before do
- stub_feature_flags(use_traversal_ids_for_ancestor_scopes: false)
- end
+ it_behaves_like '.self_and_ancestor_ids'
- it_behaves_like '.self_and_ancestor_ids'
-
- it 'makes recursive queries' do
- expect { described_class.where(id: [nested_group_1]).self_and_ancestor_ids.load }.to make_queries_matching(/WITH RECURSIVE/)
- end
+ it 'not make recursive queries' do
+ expect { described_class.where(id: [nested_group_1]).self_and_ancestor_ids.load }.not_to make_queries_matching(/WITH RECURSIVE/)
end
end
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
new file mode 100644
index 00000000000..9ccb7c0ae42
--- /dev/null
+++ b/spec/support/shared_examples/quick_actions/work_item/type_change_quick_actions_shared_examples.rb
@@ -0,0 +1,93 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'quick actions that change work item type' do
+ include_context 'with work item change type context'
+
+ describe 'type command' do
+ let(:command) { "/type #{new_type}" }
+
+ it 'populates :issue_type: and :work_item_type' do
+ _, updates, message = service.execute(command, work_item)
+
+ expect(message).to eq(_('Type changed successfully.'))
+ expect(updates).to eq({ issue_type: 'task', work_item_type: WorkItems::Type.default_by_type(:task) })
+ end
+
+ context 'when new type is invalid' do
+ let(:command) { '/type foo' }
+
+ it_behaves_like 'quick command error', 'Provided type is not supported'
+ end
+
+ context 'when new type is the same as current type' do
+ let(:command) { '/type Issue' }
+
+ it_behaves_like 'quick command error', 'Types are the same'
+ end
+
+ context 'when user has insufficient permissions to create new type' do
+ let(:with_access) { false }
+
+ it_behaves_like 'quick command error', 'You have insufficient permissions'
+ end
+ end
+
+ describe 'promote_to command' do
+ let(:new_type) { 'issue' }
+ let(:command) { "/promote_to #{new_type}" }
+
+ shared_examples 'action with validation errors' do
+ context 'when user has insufficient permissions to create new type' do
+ let(:with_access) { false }
+
+ it_behaves_like 'quick command error', 'You have insufficient permissions', 'promote'
+ end
+
+ context 'when new type is not supported' do
+ let(:new_type) { unsupported_type }
+
+ it_behaves_like 'quick command error', 'Provided type is not supported', 'promote'
+ end
+ end
+
+ context 'with issue' do
+ let(:new_type) { 'incident' }
+ let(:unsupported_type) { 'task' }
+
+ it 'populates :issue_type: and :work_item_type' do
+ _, updates, message = service.execute(command, work_item)
+
+ expect(message).to eq(_('Work Item promoted successfully.'))
+ expect(updates).to eq({ issue_type: 'incident', work_item_type: WorkItems::Type.default_by_type(:incident) })
+ end
+
+ it_behaves_like 'action with validation errors'
+ end
+
+ context 'with task' do
+ let_it_be_with_reload(:task) { create(:work_item, :task, project: project) }
+ let(:work_item) { task }
+ let(:new_type) { 'issue' }
+ let(:unsupported_type) { 'incident' }
+
+ it 'populates :issue_type: and :work_item_type' do
+ _, updates, message = service.execute(command, work_item)
+
+ expect(message).to eq(_('Work Item promoted successfully.'))
+ expect(updates).to eq({ issue_type: 'issue', work_item_type: WorkItems::Type.default_by_type(:issue) })
+ 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/debian_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/debian_packages_shared_examples.rb
index bc7ad570441..5cb6c3d310f 100644
--- a/spec/support/shared_examples/requests/api/debian_packages_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/debian_packages_shared_examples.rb
@@ -20,17 +20,11 @@ RSpec.shared_examples 'Debian packages upload request' do |status, body = nil|
if status == :created
it 'creates package files', :aggregate_failures do
expect(::Packages::Debian::CreatePackageFileService).to receive(:new).with(package: be_a(Packages::Package), current_user: be_an(User), params: be_an(Hash)).and_call_original
+ expect(::Packages::Debian::ProcessChangesWorker).not_to receive(:perform_async)
- if file_name.end_with? '.changes'
- expect(::Packages::Debian::ProcessChangesWorker).to receive(:perform_async)
- else
- expect(::Packages::Debian::ProcessChangesWorker).not_to receive(:perform_async)
- end
-
- if extra_params[:distribution]
+ if extra_params[:distribution] || file_name.end_with?('.changes')
expect(::Packages::Debian::FindOrCreateIncomingService).not_to receive(:new)
- expect(::Packages::Debian::ProcessPackageFileWorker).to receive(:perform_async)
-
+ expect(::Packages::Debian::ProcessPackageFileWorker).to receive(:perform_async).with(be_a(Integer), extra_params[:distribution], extra_params[:component])
expect { subject }
.to change { container.packages.debian.count }.by(1)
.and not_change { container.packages.debian.where(name: 'incoming').count }
diff --git a/spec/support/shared_examples/requests/api/diff_discussions_shared_examples.rb b/spec/support/shared_examples/requests/api/diff_discussions_shared_examples.rb
index 7f2c445e93d..e6b94f257e4 100644
--- a/spec/support/shared_examples/requests/api/diff_discussions_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/diff_discussions_shared_examples.rb
@@ -18,10 +18,12 @@ RSpec.shared_examples 'diff discussions API' do |parent_type, noteable_type, id_
it "returns a discussion by id" do
get api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/discussions/#{diff_note.discussion_id}", user)
+ position = diff_note.position.to_h.except(:ignore_whitespace_change)
+
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['id']).to eq(diff_note.discussion_id)
expect(json_response['notes'].first['body']).to eq(diff_note.note)
- expect(json_response['notes'].first['position']).to eq(diff_note.position.to_h.stringify_keys)
+ expect(json_response['notes'].first['position']).to eq(position.stringify_keys)
expect(json_response['notes'].first['line_range']).to eq(nil)
end
end
@@ -39,7 +41,7 @@ RSpec.shared_examples 'diff discussions API' do |parent_type, noteable_type, id_
}
}
- position = diff_note.position.to_h.merge({ line_range: line_range })
+ position = diff_note.position.to_h.merge({ line_range: line_range }).except(:ignore_whitespace_change)
post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/discussions", user),
params: { body: 'hi!', position: position }
diff --git a/spec/support/shared_examples/requests/api/graphql/releases_and_group_releases_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/releases_and_group_releases_shared_examples.rb
index b40cf6daea9..fd7a530fcd6 100644
--- a/spec/support/shared_examples/requests/api/graphql/releases_and_group_releases_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/graphql/releases_and_group_releases_shared_examples.rb
@@ -14,6 +14,20 @@ RSpec.shared_examples 'correct total count' do
end
end
+RSpec.shared_examples 'when there are no releases' do
+ let(:data) { graphql_data.dig(resource_type.to_s, 'releases') }
+
+ before do
+ project.releases.delete_all
+
+ post_query
+ end
+
+ it 'returns an empty array' do
+ expect(data['nodes']).to eq([])
+ end
+end
+
RSpec.shared_examples 'full access to all repository-related fields' do
describe 'repository-related fields' do
before do
@@ -57,6 +71,7 @@ RSpec.shared_examples 'full access to all repository-related fields' do
end
it_behaves_like 'correct total count'
+ it_behaves_like 'when there are no releases'
end
RSpec.shared_examples 'no access to any repository-related fields' do
diff --git a/spec/support/shared_examples/requests/api/ml/mlflow/mlflow_shared_examples.rb b/spec/support/shared_examples/requests/api/ml/mlflow/mlflow_shared_examples.rb
index 2ca62698daf..f2c38d70508 100644
--- a/spec/support/shared_examples/requests/api/ml/mlflow/mlflow_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/ml/mlflow/mlflow_shared_examples.rb
@@ -47,8 +47,13 @@ RSpec.shared_examples 'MLflow|shared error cases' do
end
end
- context 'when ff is disabled' do
- let(:ff_value) { false }
+ context 'when model experiments is unavailable' do
+ before do
+ allow(Ability).to receive(:allowed?).and_call_original
+ allow(Ability).to receive(:allowed?)
+ .with(current_user, :read_model_experiments, project)
+ .and_return(false)
+ end
it "is Not Found" do
is_expected.to have_gitlab_http_status(:not_found)
diff --git a/spec/support/shared_examples/requests/api/ml_model_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/ml_model_packages_shared_examples.rb
new file mode 100644
index 00000000000..81ff004779a
--- /dev/null
+++ b/spec/support/shared_examples/requests/api/ml_model_packages_shared_examples.rb
@@ -0,0 +1,108 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'Endpoint not found if read_model_registry not available' do
+ context 'when read_model_registry disabled for current project' do
+ before do
+ allow(Ability).to receive(:allowed?).and_call_original
+ allow(Ability).to receive(:allowed?)
+ .with(user, :read_model_registry, project)
+ .and_return(false)
+ end
+
+ it "is not found" do
+ is_expected.to have_gitlab_http_status(:not_found)
+ end
+ end
+end
+
+RSpec.shared_examples 'creates model experiments package files' do
+ it 'creates package files', :aggregate_failures do
+ expect { api_response }
+ .to change { project.packages.count }.by(1)
+ .and change { Packages::PackageFile.count }.by(1)
+ expect(api_response).to have_gitlab_http_status(:created)
+
+ package_file = project.packages.last.package_files.reload.last
+ expect(package_file.file_name).to eq(file_name)
+ end
+
+ it 'returns bad request if package creation fails' do
+ allow_next_instance_of(::Packages::MlModel::CreatePackageFileService) do |instance|
+ expect(instance).to receive(:execute).and_return(nil)
+ end
+
+ expect(api_response).to have_gitlab_http_status(:bad_request)
+ end
+
+ context 'when file is too large' do
+ it 'is bad request', :aggregate_failures do
+ allow_next_instance_of(UploadedFile) do |uploaded_file|
+ allow(uploaded_file).to receive(:size).and_return(project.actual_limits.ml_model_max_file_size + 1)
+ end
+
+ expect(api_response).to have_gitlab_http_status(:bad_request)
+ end
+ end
+end
+
+RSpec.shared_examples 'process ml model package upload' do
+ context 'with object storage disabled' do
+ before do
+ stub_package_file_object_storage(enabled: false)
+ end
+
+ context 'without a file from workhorse' do
+ let(:send_rewritten_field) { false }
+
+ it_behaves_like 'returning response status', :bad_request
+ end
+
+ context 'with correct params' do
+ it_behaves_like 'package workhorse uploads'
+ it_behaves_like 'creates model experiments package files'
+ # To be reactivated with https://gitlab.com/gitlab-org/gitlab/-/issues/414270
+ # it_behaves_like 'a package tracking event', '::API::MlModelPackages', 'push_package'
+ end
+ end
+
+ context 'with object storage enabled' do
+ let(:tmp_object) do
+ fog_connection.directories.new(key: 'packages').files.create( # rubocop:disable Rails/SaveBang
+ key: "tmp/uploads/#{file_name}",
+ body: 'content'
+ )
+ end
+
+ let(:fog_file) { fog_to_uploaded_file(tmp_object) }
+ let(:params) { { file: fog_file, 'file.remote_id' => file_name } }
+
+ context 'and direct upload enabled' do
+ let(:fog_connection) do
+ stub_package_file_object_storage(direct_upload: true)
+ end
+
+ it_behaves_like 'creates model experiments package files'
+
+ ['123123', '../../123123'].each do |remote_id|
+ context "with invalid remote_id: #{remote_id}" do
+ let(:params) do
+ {
+ file: fog_file,
+ 'file.remote_id' => remote_id
+ }
+ end
+
+ it { is_expected.to have_gitlab_http_status(:forbidden) }
+ end
+ end
+ end
+
+ context 'and direct upload disabled' do
+ let(:fog_connection) do
+ stub_package_file_object_storage(direct_upload: false)
+ end
+
+ it_behaves_like 'creates model experiments package files'
+ end
+ end
+end
diff --git a/spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb
index f430db61976..5284ed2de21 100644
--- a/spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb
@@ -259,8 +259,13 @@ RSpec.shared_examples 'handling get metadata requests' do |scope: :project|
before do
project.send("add_#{user_role}", user) if user_role
project.update!(visibility: visibility.to_s)
+
+ group.send("add_#{user_role}", user) if user_role && scope == :group
+ group.update!(visibility: visibility.to_s) if scope == :group
+
package.update!(name: package_name) unless package_name == 'non-existing-package'
- if scope == :instance
+
+ if %i[instance group].include?(scope)
allow_fetch_application_setting(attribute: "npm_package_requests_forwarding", return_value: request_forward)
else
allow_fetch_cascade_application_setting(attribute: "npm_package_requests_forwarding", return_value: request_forward)
@@ -280,6 +285,8 @@ RSpec.shared_examples 'handling get metadata requests' do |scope: :project|
end
end
+ status = :not_found if scope == :group && params[:package_name_type] == :non_existing && !params[:request_forward]
+
it_behaves_like example_name, status: status
end
end
@@ -300,6 +307,7 @@ RSpec.shared_examples 'handling get metadata requests' do |scope: :project|
let(:headers) { build_token_auth_header(personal_access_token.token) }
before do
+ group.add_developer(user) if scope == :group
project.add_developer(user)
end
@@ -441,7 +449,7 @@ RSpec.shared_examples 'handling audit request' do |path:, scope: :project|
project.send("add_#{user_role}", user) if user_role
project.update!(visibility: visibility.to_s)
- if scope == :instance
+ if %i[instance group].include?(scope)
allow_fetch_application_setting(attribute: "npm_package_requests_forwarding", return_value: request_forward)
else
allow_fetch_cascade_application_setting(attribute: "npm_package_requests_forwarding", return_value: request_forward)
@@ -451,7 +459,7 @@ RSpec.shared_examples 'handling audit request' do |path:, scope: :project|
example_name = "#{params[:expected_result]} audit request"
status = params[:expected_status]
- if scope == :instance && params[:expected_status] != :unauthorized
+ if %i[instance group].include?(scope) && params[:expected_status] != :unauthorized
if params[:request_forward]
example_name = 'redirect audit request'
status = :temporary_redirect
@@ -630,6 +638,8 @@ RSpec.shared_examples 'handling get dist tags requests' do |scope: :project|
status = :not_found
end
+ status = :not_found if scope == :group && params[:package_name_type] == :non_existing
+
it_behaves_like example_name, status: status
end
end
@@ -846,6 +856,8 @@ RSpec.shared_examples 'handling different package names, visibilities and user r
status = params[:auth].nil? ? :unauthorized : :not_found
end
+ status = :not_found if scope == :group && params[:package_name_type] == :non_existing && params[:auth].present?
+
it_behaves_like example_name, status: status
end
end
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 7cafe8bb368..432e67ee21e 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,100 +1,40 @@
# frozen_string_literal: true
-RSpec.shared_examples 'handling nuget service requests' do |example_names_with_status: {}|
- anonymous_requests_example_name = example_names_with_status.fetch(:anonymous_requests_example_name, 'process nuget service index request')
- anonymous_requests_status = example_names_with_status.fetch(:anonymous_requests_status, :success)
- guest_requests_example_name = example_names_with_status.fetch(:guest_requests_example_name, 'rejects nuget packages access')
- guest_requests_status = example_names_with_status.fetch(:guest_requests_status, :forbidden)
-
+RSpec.shared_examples 'handling nuget service requests' do
subject { get api(url) }
context 'with valid target' do
using RSpec::Parameterized::TableSyntax
- context 'personal token' do
- where(:visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do
- 'PUBLIC' | :developer | true | true | 'process nuget service index request' | :success
- 'PUBLIC' | :guest | true | true | 'process nuget service index request' | :success
- 'PUBLIC' | :developer | true | false | 'rejects nuget packages access' | :unauthorized
- 'PUBLIC' | :guest | true | false | 'rejects nuget packages access' | :unauthorized
- 'PUBLIC' | :developer | false | true | 'process nuget service index request' | :success
- 'PUBLIC' | :guest | false | true | 'process nuget service index request' | :success
- 'PUBLIC' | :developer | false | false | 'rejects nuget packages access' | :unauthorized
- 'PUBLIC' | :guest | false | false | 'rejects nuget packages access' | :unauthorized
- 'PUBLIC' | :anonymous | false | true | anonymous_requests_example_name | anonymous_requests_status
- 'PRIVATE' | :developer | true | true | 'process nuget service index request' | :success
- 'PRIVATE' | :guest | true | true | guest_requests_example_name | guest_requests_status
- 'PRIVATE' | :developer | true | false | 'rejects nuget packages access' | :unauthorized
- 'PRIVATE' | :guest | true | false | 'rejects nuget packages access' | :unauthorized
- 'PRIVATE' | :developer | false | true | 'rejects nuget packages access' | :not_found
- 'PRIVATE' | :guest | false | true | 'rejects nuget packages access' | :not_found
- 'PRIVATE' | :developer | false | false | 'rejects nuget packages access' | :unauthorized
- 'PRIVATE' | :guest | false | false | 'rejects nuget packages access' | :unauthorized
- 'PRIVATE' | :anonymous | false | true | 'rejects nuget packages access' | :unauthorized
- end
-
- with_them do
- let(:token) { user_token ? personal_access_token.token : 'wrong' }
- let(:headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) }
- let(:snowplow_gitlab_standard_context) { snowplow_context(user_role: user_role) }
-
- subject { get api(url), headers: headers }
-
- 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]
- end
+ where(:visibility_level, :user_role, :member, :shared_examples_name, :expected_status) do
+ 'PUBLIC' | :developer | true | 'process nuget service index request' | :success
+ 'PUBLIC' | :guest | true | 'process nuget service index request' | :success
+ 'PUBLIC' | :developer | false | 'process nuget service index request' | :success
+ 'PUBLIC' | :guest | false | 'process nuget service index request' | :success
+ 'PUBLIC' | :anonymous | false | 'process nuget service index request' | :success
+ 'PRIVATE' | :developer | true | 'process nuget service index request' | :success
+ 'PRIVATE' | :guest | true | 'process nuget service index request' | :success
+ 'PRIVATE' | :developer | false | 'process nuget service index request' | :success
+ 'PRIVATE' | :guest | false | 'process nuget service index request' | :success
+ 'PRIVATE' | :anonymous | false | 'process nuget service index request' | :success
end
- context 'with job token' do
- where(:visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do
- 'PUBLIC' | :developer | true | true | 'process nuget service index request' | :success
- 'PUBLIC' | :guest | true | true | 'process nuget service index request' | :success
- 'PUBLIC' | :developer | true | false | 'rejects nuget packages access' | :unauthorized
- 'PUBLIC' | :guest | true | false | 'rejects nuget packages access' | :unauthorized
- 'PUBLIC' | :developer | false | true | 'process nuget service index request' | :success
- 'PUBLIC' | :guest | false | true | 'process nuget service index request' | :success
- 'PUBLIC' | :developer | false | false | 'rejects nuget packages access' | :unauthorized
- 'PUBLIC' | :guest | false | false | 'rejects nuget packages access' | :unauthorized
- 'PUBLIC' | :anonymous | false | true | anonymous_requests_example_name | anonymous_requests_status
- 'PRIVATE' | :developer | true | true | 'process nuget service index request' | :success
- 'PRIVATE' | :guest | true | true | guest_requests_example_name | guest_requests_status
- 'PRIVATE' | :developer | true | false | 'rejects nuget packages access' | :unauthorized
- 'PRIVATE' | :guest | true | false | 'rejects nuget packages access' | :unauthorized
- 'PRIVATE' | :developer | false | true | 'rejects nuget packages access' | :not_found
- 'PRIVATE' | :guest | false | true | 'rejects nuget packages access' | :not_found
- 'PRIVATE' | :developer | false | false | 'rejects nuget packages access' | :unauthorized
- 'PRIVATE' | :guest | false | false | 'rejects nuget packages access' | :unauthorized
- 'PRIVATE' | :anonymous | false | true | 'rejects nuget packages access' | :unauthorized
- end
-
- with_them do
- let(:job) { user_token ? create(:ci_build, project: project, user: user, status: :running) : double(token: 'wrong') }
- let(:headers) { user_role == :anonymous ? {} : job_basic_auth_header(job) }
- let(:snowplow_gitlab_standard_context) { snowplow_context(user_role: user_role) }
-
- subject { get api(url), headers: headers }
+ with_them do
+ let(:snowplow_gitlab_standard_context) { snowplow_context(user_role: :anonymous) }
- before do
- update_visibility_to(Gitlab::VisibilityLevel.const_get(visibility_level, false))
- end
+ subject { get api(url) }
- it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member]
+ before do
+ update_visibility_to(Gitlab::VisibilityLevel.const_get(visibility_level, false))
end
- end
- end
- it_behaves_like 'deploy token for package GET requests' do
- before do
- update_visibility_to(Gitlab::VisibilityLevel::PRIVATE)
+ it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member]
end
end
- it_behaves_like 'rejects nuget access with unknown target id'
+ it_behaves_like 'rejects nuget access with unknown target id', not_found_response: :not_found
- it_behaves_like 'rejects nuget access with invalid target id'
+ it_behaves_like 'rejects nuget access with invalid target id', not_found_response: :not_found
end
RSpec.shared_examples 'handling nuget metadata requests with package name' do |example_names_with_status: {}|
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 3abe545db59..d6a0055700d 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
@@ -399,7 +399,7 @@ RSpec.shared_examples 'process empty nuget search request' do |user_type, status
it_behaves_like 'a package tracking event', 'API::NugetPackages', 'search_package'
end
-RSpec.shared_examples 'rejects nuget access with invalid target id' do
+RSpec.shared_examples 'rejects nuget access with invalid target id' do |not_found_response: :unauthorized|
context 'with a target id with invalid integers' do
using RSpec::Parameterized::TableSyntax
@@ -411,7 +411,7 @@ RSpec.shared_examples 'rejects nuget access with invalid target id' do
'%20' | :bad_request
'%2e%2e%2f' | :bad_request
'NaN' | :bad_request
- 00002345 | :unauthorized
+ 00002345 | not_found_response
'anything25' | :bad_request
end
@@ -421,12 +421,12 @@ RSpec.shared_examples 'rejects nuget access with invalid target id' do
end
end
-RSpec.shared_examples 'rejects nuget access with unknown target id' do
+RSpec.shared_examples 'rejects nuget access with unknown target id' do |not_found_response: :unauthorized|
context 'with an unknown target' do
let(:target) { double(id: 1234567890) }
context 'as anonymous' do
- it_behaves_like 'rejects nuget packages access', :anonymous, :unauthorized
+ it_behaves_like 'rejects nuget packages access', :anonymous, not_found_response
end
context 'as authenticated user' do
@@ -441,30 +441,59 @@ RSpec.shared_examples 'nuget authorize upload endpoint' do
using RSpec::Parameterized::TableSyntax
context 'with valid project' do
- where(:visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do
- 'PUBLIC' | :developer | true | true | 'process nuget workhorse authorization' | :success
- 'PUBLIC' | :guest | true | true | 'rejects nuget packages access' | :forbidden
- 'PUBLIC' | :developer | true | false | 'rejects nuget packages access' | :unauthorized
- 'PUBLIC' | :guest | true | false | 'rejects nuget packages access' | :unauthorized
- 'PUBLIC' | :developer | false | true | 'rejects nuget packages access' | :forbidden
- 'PUBLIC' | :guest | false | true | 'rejects nuget packages access' | :forbidden
- 'PUBLIC' | :developer | false | false | 'rejects nuget packages access' | :unauthorized
- 'PUBLIC' | :guest | false | false | 'rejects nuget packages access' | :unauthorized
- 'PUBLIC' | :anonymous | false | true | 'rejects nuget packages access' | :unauthorized
- 'PRIVATE' | :developer | true | true | 'process nuget workhorse authorization' | :success
- 'PRIVATE' | :guest | true | true | 'rejects nuget packages access' | :forbidden
- 'PRIVATE' | :developer | true | false | 'rejects nuget packages access' | :unauthorized
- 'PRIVATE' | :guest | true | false | 'rejects nuget packages access' | :unauthorized
- 'PRIVATE' | :developer | false | true | 'rejects nuget packages access' | :not_found
- 'PRIVATE' | :guest | false | true | 'rejects nuget packages access' | :not_found
- 'PRIVATE' | :developer | false | false | 'rejects nuget packages access' | :unauthorized
- 'PRIVATE' | :guest | false | false | 'rejects nuget packages access' | :unauthorized
- 'PRIVATE' | :anonymous | false | true | 'rejects nuget packages access' | :unauthorized
+ where(:visibility_level, :user_role, :member, :user_token, :sent_through, :shared_examples_name, :expected_status) do
+ 'PUBLIC' | :developer | true | true | :basic_auth | 'process nuget workhorse authorization' | :success
+ 'PUBLIC' | :guest | true | true | :basic_auth | 'rejects nuget packages access' | :forbidden
+ 'PUBLIC' | :developer | true | false | :basic_auth | 'rejects nuget packages access' | :unauthorized
+ 'PUBLIC' | :guest | true | false | :basic_auth | 'rejects nuget packages access' | :unauthorized
+ 'PUBLIC' | :developer | false | true | :basic_auth | 'rejects nuget packages access' | :forbidden
+ 'PUBLIC' | :guest | false | true | :basic_auth | 'rejects nuget packages access' | :forbidden
+ 'PUBLIC' | :developer | false | false | :basic_auth | 'rejects nuget packages access' | :unauthorized
+ 'PUBLIC' | :guest | false | false | :basic_auth | 'rejects nuget packages access' | :unauthorized
+ 'PRIVATE' | :developer | true | true | :basic_auth | 'process nuget workhorse authorization' | :success
+ 'PRIVATE' | :guest | true | true | :basic_auth | 'rejects nuget packages access' | :forbidden
+ 'PRIVATE' | :developer | true | false | :basic_auth | 'rejects nuget packages access' | :unauthorized
+ 'PRIVATE' | :guest | true | false | :basic_auth | 'rejects nuget packages access' | :unauthorized
+ 'PRIVATE' | :developer | false | true | :basic_auth | 'rejects nuget packages access' | :not_found
+ 'PRIVATE' | :guest | false | true | :basic_auth | 'rejects nuget packages access' | :not_found
+ 'PRIVATE' | :developer | false | false | :basic_auth | 'rejects nuget packages access' | :unauthorized
+ 'PRIVATE' | :guest | false | false | :basic_auth | 'rejects nuget packages access' | :unauthorized
+
+ 'PUBLIC' | :developer | true | true | :api_key | 'process nuget workhorse authorization' | :success
+ 'PUBLIC' | :guest | true | true | :api_key | 'rejects nuget packages access' | :forbidden
+ 'PUBLIC' | :developer | true | false | :api_key | 'rejects nuget packages access' | :unauthorized
+ 'PUBLIC' | :guest | true | false | :api_key | 'rejects nuget packages access' | :unauthorized
+ 'PUBLIC' | :developer | false | true | :api_key | 'rejects nuget packages access' | :forbidden
+ 'PUBLIC' | :guest | false | true | :api_key | 'rejects nuget packages access' | :forbidden
+ 'PUBLIC' | :developer | false | false | :api_key | 'rejects nuget packages access' | :unauthorized
+ 'PUBLIC' | :guest | false | false | :api_key | 'rejects nuget packages access' | :unauthorized
+ 'PRIVATE' | :developer | true | true | :api_key | 'process nuget workhorse authorization' | :success
+ 'PRIVATE' | :guest | true | true | :api_key | 'rejects nuget packages access' | :forbidden
+ 'PRIVATE' | :developer | true | false | :api_key | 'rejects nuget packages access' | :unauthorized
+ 'PRIVATE' | :guest | true | false | :api_key | 'rejects nuget packages access' | :unauthorized
+ 'PRIVATE' | :developer | false | true | :api_key | 'rejects nuget packages access' | :not_found
+ 'PRIVATE' | :guest | false | true | :api_key | 'rejects nuget packages access' | :not_found
+ 'PRIVATE' | :developer | false | false | :api_key | 'rejects nuget packages access' | :unauthorized
+ 'PRIVATE' | :guest | false | false | :api_key | 'rejects nuget packages access' | :unauthorized
+
+ 'PUBLIC' | :anonymous | false | true | nil | 'rejects nuget packages access' | :unauthorized
+ 'PRIVATE' | :anonymous | false | true | nil | 'rejects nuget packages access' | :unauthorized
end
with_them do
let(:token) { user_token ? personal_access_token.token : 'wrong' }
- let(:user_headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) }
+
+ let(:user_headers) do
+ case sent_through
+ when :basic_auth
+ basic_auth_header(user.username, token)
+ when :api_key
+ { 'X-NuGet-ApiKey' => token }
+ else
+ {}
+ end
+ end
+
let(:headers) { user_headers.merge(workhorse_headers) }
before do
@@ -490,30 +519,59 @@ RSpec.shared_examples 'nuget upload endpoint' do |symbol_package: false|
using RSpec::Parameterized::TableSyntax
context 'with valid project' do
- where(:visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do
- 'PUBLIC' | :developer | true | true | 'process nuget upload' | :created
- 'PUBLIC' | :guest | true | true | 'rejects nuget packages access' | :forbidden
- 'PUBLIC' | :developer | true | false | 'rejects nuget packages access' | :unauthorized
- 'PUBLIC' | :guest | true | false | 'rejects nuget packages access' | :unauthorized
- 'PUBLIC' | :developer | false | true | 'rejects nuget packages access' | :forbidden
- 'PUBLIC' | :guest | false | true | 'rejects nuget packages access' | :forbidden
- 'PUBLIC' | :developer | false | false | 'rejects nuget packages access' | :unauthorized
- 'PUBLIC' | :guest | false | false | 'rejects nuget packages access' | :unauthorized
- 'PUBLIC' | :anonymous | false | true | 'rejects nuget packages access' | :unauthorized
- 'PRIVATE' | :developer | true | true | 'process nuget upload' | :created
- 'PRIVATE' | :guest | true | true | 'rejects nuget packages access' | :forbidden
- 'PRIVATE' | :developer | true | false | 'rejects nuget packages access' | :unauthorized
- 'PRIVATE' | :guest | true | false | 'rejects nuget packages access' | :unauthorized
- 'PRIVATE' | :developer | false | true | 'rejects nuget packages access' | :not_found
- 'PRIVATE' | :guest | false | true | 'rejects nuget packages access' | :not_found
- 'PRIVATE' | :developer | false | false | 'rejects nuget packages access' | :unauthorized
- 'PRIVATE' | :guest | false | false | 'rejects nuget packages access' | :unauthorized
- 'PRIVATE' | :anonymous | false | true | 'rejects nuget packages access' | :unauthorized
+ where(:visibility_level, :user_role, :member, :user_token, :sent_through, :shared_examples_name, :expected_status) do
+ 'PUBLIC' | :developer | true | true | :basic_auth | 'process nuget upload' | :created
+ 'PUBLIC' | :guest | true | true | :basic_auth | 'rejects nuget packages access' | :forbidden
+ 'PUBLIC' | :developer | true | false | :basic_auth | 'rejects nuget packages access' | :unauthorized
+ 'PUBLIC' | :guest | true | false | :basic_auth | 'rejects nuget packages access' | :unauthorized
+ 'PUBLIC' | :developer | false | true | :basic_auth | 'rejects nuget packages access' | :forbidden
+ 'PUBLIC' | :guest | false | true | :basic_auth | 'rejects nuget packages access' | :forbidden
+ 'PUBLIC' | :developer | false | false | :basic_auth | 'rejects nuget packages access' | :unauthorized
+ 'PUBLIC' | :guest | false | false | :basic_auth | 'rejects nuget packages access' | :unauthorized
+ 'PRIVATE' | :developer | true | true | :basic_auth | 'process nuget upload' | :created
+ 'PRIVATE' | :guest | true | true | :basic_auth | 'rejects nuget packages access' | :forbidden
+ 'PRIVATE' | :developer | true | false | :basic_auth | 'rejects nuget packages access' | :unauthorized
+ 'PRIVATE' | :guest | true | false | :basic_auth | 'rejects nuget packages access' | :unauthorized
+ 'PRIVATE' | :developer | false | true | :basic_auth | 'rejects nuget packages access' | :not_found
+ 'PRIVATE' | :guest | false | true | :basic_auth | 'rejects nuget packages access' | :not_found
+ 'PRIVATE' | :developer | false | false | :basic_auth | 'rejects nuget packages access' | :unauthorized
+ 'PRIVATE' | :guest | false | false | :basic_auth | 'rejects nuget packages access' | :unauthorized
+
+ 'PUBLIC' | :developer | true | true | :api_key | 'process nuget upload' | :created
+ 'PUBLIC' | :guest | true | true | :api_key | 'rejects nuget packages access' | :forbidden
+ 'PUBLIC' | :developer | true | false | :api_key | 'rejects nuget packages access' | :unauthorized
+ 'PUBLIC' | :guest | true | false | :api_key | 'rejects nuget packages access' | :unauthorized
+ 'PUBLIC' | :developer | false | true | :api_key | 'rejects nuget packages access' | :forbidden
+ 'PUBLIC' | :guest | false | true | :api_key | 'rejects nuget packages access' | :forbidden
+ 'PUBLIC' | :developer | false | false | :api_key | 'rejects nuget packages access' | :unauthorized
+ 'PUBLIC' | :guest | false | false | :api_key | 'rejects nuget packages access' | :unauthorized
+ 'PRIVATE' | :developer | true | true | :api_key | 'process nuget upload' | :created
+ 'PRIVATE' | :guest | true | true | :api_key | 'rejects nuget packages access' | :forbidden
+ 'PRIVATE' | :developer | true | false | :api_key | 'rejects nuget packages access' | :unauthorized
+ 'PRIVATE' | :guest | true | false | :api_key | 'rejects nuget packages access' | :unauthorized
+ 'PRIVATE' | :developer | false | true | :api_key | 'rejects nuget packages access' | :not_found
+ 'PRIVATE' | :guest | false | true | :api_key | 'rejects nuget packages access' | :not_found
+ 'PRIVATE' | :developer | false | false | :api_key | 'rejects nuget packages access' | :unauthorized
+ 'PRIVATE' | :guest | false | false | :api_key | 'rejects nuget packages access' | :unauthorized
+
+ 'PUBLIC' | :anonymous | false | true | nil | 'rejects nuget packages access' | :unauthorized
+ 'PRIVATE' | :anonymous | false | true | nil | 'rejects nuget packages access' | :unauthorized
end
with_them do
let(:token) { user_token ? personal_access_token.token : 'wrong' }
- let(:user_headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) }
+
+ let(:user_headers) do
+ case sent_through
+ when :basic_auth
+ basic_auth_header(user.username, token)
+ when :api_key
+ { 'X-NuGet-ApiKey' => token }
+ else
+ {}
+ end
+ 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' } }
diff --git a/spec/support/shared_examples/requests/api/packages_shared_examples.rb b/spec/support/shared_examples/requests/api/packages_shared_examples.rb
index 3168f25e4fa..283ab565dc4 100644
--- a/spec/support/shared_examples/requests/api/packages_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/packages_shared_examples.rb
@@ -119,7 +119,7 @@ RSpec.shared_examples 'job token for package uploads' do |authorize_endpoint: fa
pkg = ::Packages::Package.order_created
.last
- expect(pkg.build_infos).to be
+ expect(pkg.build_infos).to be_present
end
end
end
diff --git a/spec/support/shared_examples/services/packages_shared_examples.rb b/spec/support/shared_examples/services/packages_shared_examples.rb
index 7a4d7f81e96..7e7d8605d0b 100644
--- a/spec/support/shared_examples/services/packages_shared_examples.rb
+++ b/spec/support/shared_examples/services/packages_shared_examples.rb
@@ -8,8 +8,8 @@ RSpec.shared_examples 'assigns build to package' do
it 'assigns the pipeline to the package' do
package = subject
- expect(package.original_build_info).to be_present
- expect(package.original_build_info.pipeline).to eq job.pipeline
+ expect(package.last_build_info).to be_present
+ expect(package.last_build_info.pipeline).to eq job.pipeline
end
end
end
@@ -214,6 +214,7 @@ RSpec.shared_examples 'filters on each package_type' do |is_project: false|
let_it_be(:package11) { create(:helm_package, project: project) }
let_it_be(:package12) { create(:terraform_module_package, project: project) }
let_it_be(:package13) { create(:rpm_package, project: project) }
+ let_it_be(:package14) { create(:ml_model_package, project: project) }
Packages::Package.package_types.keys.each do |package_type|
context "for package type #{package_type}" do
diff --git a/spec/support/shared_examples/services/rate_limited_service_shared_examples.rb b/spec/support/shared_examples/services/rate_limited_service_shared_examples.rb
index b79f1a332a6..70848044527 100644
--- a/spec/support/shared_examples/services/rate_limited_service_shared_examples.rb
+++ b/spec/support/shared_examples/services/rate_limited_service_shared_examples.rb
@@ -7,7 +7,7 @@
# let(:key) { :issues_create }
# let(:key_scope) { %i[project current_user external_author] }
# let(:application_limit_key) { :issues_create_limit }
-# let(:service) { described_class.new(project: project, current_user: user, params: { title: 'title' }, spam_params: double) }
+# let(:service) { described_class.new(project: project, current_user: user, params: { title: 'title' }) }
# let(:created_model) { Issue }
# end
@@ -29,10 +29,6 @@ RSpec.shared_examples 'rate limited service' do
end
describe '#execute' do
- before do
- stub_spam_services
- end
-
context 'when rate limiting is in effect', :freeze_time, :clean_gitlab_redis_rate_limiting do
let(:user) { create(:user) }
diff --git a/spec/support/shared_examples/services/service_ping/complete_service_ping_payload_shared_examples.rb b/spec/support/shared_examples/services/service_ping/complete_service_ping_payload_shared_examples.rb
index 8dcff99fb6f..fd3c53f3675 100644
--- a/spec/support/shared_examples/services/service_ping/complete_service_ping_payload_shared_examples.rb
+++ b/spec/support/shared_examples/services/service_ping/complete_service_ping_payload_shared_examples.rb
@@ -3,7 +3,7 @@
RSpec.shared_examples 'complete service ping payload' do
it_behaves_like 'service ping payload with all expected metrics' do
let(:expected_metrics) do
- standard_metrics + subscription_metrics + operational_metrics + optional_metrics
+ standard_metrics + operational_metrics + optional_metrics
end
end
end
diff --git a/spec/support/shared_examples/services/snippets_shared_examples.rb b/spec/support/shared_examples/services/snippets_shared_examples.rb
index 65893d84798..d8db0f53df5 100644
--- a/spec/support/shared_examples/services/snippets_shared_examples.rb
+++ b/spec/support/shared_examples/services/snippets_shared_examples.rb
@@ -12,7 +12,6 @@ RSpec.shared_examples 'checking spam' do
Spam::SpamActionService,
{
spammable: kind_of(Snippet),
- spam_params: spam_params,
user: an_instance_of(User),
action: action,
extra_features: { files: an_instance_of(Array) }
diff --git a/spec/support/shared_examples/uploaders/gitlab_uploader_shared_examples.rb b/spec/support/shared_examples/uploaders/gitlab_uploader_shared_examples.rb
index 7126d3ace96..a7e5892d439 100644
--- a/spec/support/shared_examples/uploaders/gitlab_uploader_shared_examples.rb
+++ b/spec/support/shared_examples/uploaders/gitlab_uploader_shared_examples.rb
@@ -63,8 +63,8 @@ RSpec.shared_examples "builds correct paths" do |**patterns|
end
it "throws an exception" do
- expect { subject.cache!(fixture_file_upload(fixture)) }.to raise_error(Gitlab::Utils::PathTraversalAttackError)
- expect { subject.store!(fixture_file_upload(fixture)) }.to raise_error(Gitlab::Utils::PathTraversalAttackError)
+ expect { subject.cache!(fixture_file_upload(fixture)) }.to raise_error(Gitlab::PathTraversal::PathTraversalAttackError)
+ expect { subject.store!(fixture_file_upload(fixture)) }.to raise_error(Gitlab::PathTraversal::PathTraversalAttackError)
end
end
end
diff --git a/spec/support/shared_examples/work_items/update_service_shared_examples.rb b/spec/support/shared_examples/work_items/update_service_shared_examples.rb
new file mode 100644
index 00000000000..2d220c0ef58
--- /dev/null
+++ b/spec/support/shared_examples/work_items/update_service_shared_examples.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'update service that triggers GraphQL work_item_updated subscription' do
+ let(:update_subject) do
+ if defined?(work_item)
+ work_item
+ elsif defined?(issue)
+ issue
+ end
+ end
+
+ it 'triggers graphql subscription workItemUpdated' do
+ expect(GraphqlTriggers).to receive(:work_item_updated).with(update_subject).and_call_original
+
+ execute_service
+ end
+end
diff --git a/spec/support/shared_examples/workers/batched_background_migration_execution_worker_shared_example.rb b/spec/support/shared_examples/workers/batched_background_migration_execution_worker_shared_example.rb
index 095c32c3136..8fdd59d1d8c 100644
--- a/spec/support/shared_examples/workers/batched_background_migration_execution_worker_shared_example.rb
+++ b/spec/support/shared_examples/workers/batched_background_migration_execution_worker_shared_example.rb
@@ -202,6 +202,21 @@ RSpec.shared_examples 'batched background migrations execution worker' do
worker.perform_work(database_name, migration.id)
end
+
+ it 'assigns proper feature category to the context and the worker' do
+ # max_value is set to create and execute a batched_job, where we fetch feature_category from the job_class
+ migration.update!(max_value: create(:event).id)
+ expect(migration.job_class).to receive(:feature_category).and_return(:code_review_workflow)
+
+ allow_next_instance_of(migration.job_class) do |job_class|
+ allow(job_class).to receive(:perform)
+ end
+
+ expect { worker.perform_work(database_name, migration.id) }.to change {
+ Gitlab::ApplicationContext.current["meta.feature_category"]
+ }.to('code_review_workflow')
+ .and change { described_class.get_feature_category }.from(:database).to('code_review_workflow')
+ end
end
end
end
diff --git a/spec/support/shared_examples/workers/batched_background_migration_worker_shared_examples.rb b/spec/support/shared_examples/workers/batched_background_migration_worker_shared_examples.rb
index 06877aee565..e7385f9abb6 100644
--- a/spec/support/shared_examples/workers/batched_background_migration_worker_shared_examples.rb
+++ b/spec/support/shared_examples/workers/batched_background_migration_worker_shared_examples.rb
@@ -64,8 +64,7 @@ RSpec.shared_examples 'it runs batched background migration jobs' do |tracking_d
end
it 'does nothing' do
- expect(worker).not_to receive(:active_migration)
- expect(worker).not_to receive(:run_active_migration)
+ expect(worker).not_to receive(:queue_migrations_for_execution)
expect { worker.perform }.not_to raise_error
end
@@ -94,8 +93,7 @@ RSpec.shared_examples 'it runs batched background migration jobs' do |tracking_d
end
it 'does nothing' do
- expect(worker).not_to receive(:active_migration)
- expect(worker).not_to receive(:run_active_migration)
+ expect(worker).not_to receive(:queue_migrations_for_execution)
worker.perform
end
@@ -106,66 +104,47 @@ RSpec.shared_examples 'it runs batched background migration jobs' do |tracking_d
skip_if_shared_database(tracking_database)
end
- context 'when the feature flag is disabled' do
+ context 'when the execute_batched_migrations_on_schedule feature flag is disabled' do
before do
stub_feature_flags(execute_batched_migrations_on_schedule: false)
end
it 'does nothing' do
- expect(worker).not_to receive(:active_migration)
- expect(worker).not_to receive(:run_active_migration)
+ expect(worker).not_to receive(:queue_migrations_for_execution)
worker.perform
end
end
- context 'when the feature flag is enabled' do
+ context 'when the execute_batched_migrations_on_schedule feature flag is enabled' do
let(:base_model) { Gitlab::Database.database_base_models[tracking_database] }
let(:connection) { base_model.connection }
before do
stub_feature_flags(execute_batched_migrations_on_schedule: true)
-
- allow(Gitlab::Database::BackgroundMigration::BatchedMigration).to receive(:active_migration)
- .with(connection: connection)
- .and_return(nil)
end
context 'when database config is shared' do
it 'does nothing' do
expect(Gitlab::Database).to receive(:db_config_share_with)
- .with(base_model.connection_db_config).and_return('main')
+ .with(base_model.connection_db_config).and_return('main')
- expect(worker).not_to receive(:active_migration)
- expect(worker).not_to receive(:run_active_migration)
+ expect(worker).not_to receive(:queue_migrations_for_execution)
worker.perform
end
end
context 'when no active migrations exist' do
- context 'when parallel execution is disabled' do
- before do
- stub_feature_flags(batched_migrations_parallel_execution: false)
- end
-
- it 'does nothing' do
- expect(worker).not_to receive(:run_active_migration)
-
- worker.perform
- end
- end
-
- context 'when parallel execution is enabled' do
- before do
- stub_feature_flags(batched_migrations_parallel_execution: true)
- end
+ it 'does nothing' do
+ allow(Gitlab::Database::BackgroundMigration::BatchedMigration)
+ .to receive(:active_migrations_distinct_on_table)
+ .with(connection: connection, limit: worker.execution_worker_class.max_running_jobs)
+ .and_return([])
- it 'does nothing' do
- expect(worker).not_to receive(:queue_migrations_for_execution)
+ expect(worker).not_to receive(:queue_migrations_for_execution)
- worker.perform
- end
+ worker.perform
end
end
@@ -190,75 +169,20 @@ RSpec.shared_examples 'it runs batched background migration jobs' do |tracking_d
end
end
- before do
- allow(Gitlab::Database::BackgroundMigration::BatchedMigration).to receive(:active_migration)
- .with(connection: connection)
- .and_return(migration)
- end
-
- context 'when parallel execution is disabled' do
- before do
- stub_feature_flags(batched_migrations_parallel_execution: false)
- end
-
- let(:execution_worker) { instance_double(execution_worker_class) }
-
- context 'when the calculated timeout is less than the minimum allowed' do
- let(:minimum_timeout) { described_class::MINIMUM_LEASE_TIMEOUT }
- let(:job_interval) { 2.minutes }
-
- it 'sets the lease timeout to the minimum value' do
- expect_to_obtain_exclusive_lease(lease_key, timeout: minimum_timeout)
-
- expect(execution_worker_class).to receive(:new).and_return(execution_worker)
- expect(execution_worker).to receive(:perform_work).with(tracking_database, migration_id)
-
- expect(worker).to receive(:run_active_migration).and_call_original
-
- worker.perform
- end
- end
-
- it 'always cleans up the exclusive lease' do
- lease = stub_exclusive_lease_taken(lease_key, timeout: lease_timeout)
-
- expect(lease).to receive(:try_obtain).and_return(true)
-
- expect(worker).to receive(:run_active_migration).and_raise(RuntimeError, 'I broke')
- expect(lease).to receive(:cancel)
-
- expect { worker.perform }.to raise_error(RuntimeError, 'I broke')
- end
-
- it 'delegetes the execution to ExecutionWorker' do
- expect(Gitlab::Database::SharedModel).to receive(:using_connection).with(connection).and_yield
- expect(execution_worker_class).to receive(:new).and_return(execution_worker)
- expect(execution_worker).to receive(:perform_work).with(tracking_database, migration_id)
-
- worker.perform
- end
- end
-
- context 'when parallel execution is enabled' do
- before do
- stub_feature_flags(batched_migrations_parallel_execution: true)
- end
-
- it 'delegetes the execution to ExecutionWorker' do
- expect(Gitlab::Database::BackgroundMigration::BatchedMigration)
- .to receive(:active_migrations_distinct_on_table).with(
- connection: base_model.connection,
- limit: execution_worker_class.max_running_jobs
- ).and_return([migration])
+ it 'delegetes the execution to ExecutionWorker' do
+ expect(Gitlab::Database::BackgroundMigration::BatchedMigration)
+ .to receive(:active_migrations_distinct_on_table).with(
+ connection: base_model.connection,
+ limit: execution_worker_class.max_running_jobs
+ ).and_return([migration])
- expected_arguments = [
- [tracking_database.to_s, migration_id]
- ]
+ expected_arguments = [
+ [tracking_database.to_s, migration_id]
+ ]
- expect(execution_worker_class).to receive(:perform_with_capacity).with(expected_arguments)
+ expect(execution_worker_class).to receive(:perform_with_capacity).with(expected_arguments)
- worker.perform
- end
+ worker.perform
end
end
end
@@ -266,67 +190,68 @@ RSpec.shared_examples 'it runs batched background migration jobs' do |tracking_d
end
end
- describe 'executing an entire migration', :freeze_time, if: Gitlab::Database.has_database?(tracking_database) do
- include Gitlab::Database::DynamicModelHelpers
- include Database::DatabaseHelpers
-
- let(:migration_class) do
- Class.new(Gitlab::BackgroundMigration::BatchedMigrationJob) do
- job_arguments :matching_status
- operation_name :update_all
- feature_category :code_review_workflow
-
- def perform
- each_sub_batch(
- batching_scope: -> (relation) { relation.where(status: matching_status) }
- ) do |sub_batch|
- sub_batch.update_all(some_column: 0)
+ describe 'executing an entire migration', :freeze_time, :sidekiq_inline,
+ if: Gitlab::Database.has_database?(tracking_database) do
+ include Gitlab::Database::DynamicModelHelpers
+ include Database::DatabaseHelpers
+
+ let(:migration_class) do
+ Class.new(Gitlab::BackgroundMigration::BatchedMigrationJob) do
+ job_arguments :matching_status
+ operation_name :update_all
+ feature_category :code_review_workflow
+
+ def perform
+ each_sub_batch(
+ batching_scope: -> (relation) { relation.where(status: matching_status) }
+ ) do |sub_batch|
+ sub_batch.update_all(some_column: 0)
+ end
end
end
end
- end
- let(:gitlab_schema) { "gitlab_#{tracking_database}" }
- let!(:migration) do
- create(
- :batched_background_migration,
- :active,
- table_name: new_table_name,
- column_name: :id,
- max_value: migration_records,
- batch_size: batch_size,
- sub_batch_size: sub_batch_size,
- job_class_name: 'ExampleDataMigration',
- job_arguments: [1],
- gitlab_schema: gitlab_schema
- )
- end
+ let(:gitlab_schema) { "gitlab_#{tracking_database}" }
+ let!(:migration) do
+ create(
+ :batched_background_migration,
+ :active,
+ table_name: new_table_name,
+ column_name: :id,
+ max_value: migration_records,
+ batch_size: batch_size,
+ sub_batch_size: sub_batch_size,
+ job_class_name: 'ExampleDataMigration',
+ job_arguments: [1],
+ gitlab_schema: gitlab_schema
+ )
+ end
- let(:base_model) { Gitlab::Database.database_base_models[tracking_database] }
- let(:new_table_name) { '_test_example_data' }
- let(:batch_size) { 5 }
- let(:sub_batch_size) { 2 }
- let(:number_of_batches) { 10 }
- let(:migration_records) { batch_size * number_of_batches }
+ let(:base_model) { Gitlab::Database.database_base_models[tracking_database] }
+ let(:new_table_name) { '_test_example_data' }
+ let(:batch_size) { 5 }
+ let(:sub_batch_size) { 2 }
+ let(:number_of_batches) { 10 }
+ let(:migration_records) { batch_size * number_of_batches }
- let(:connection) { Gitlab::Database.database_base_models[tracking_database].connection }
- let(:example_data) { define_batchable_model(new_table_name, connection: connection) }
+ let(:connection) { Gitlab::Database.database_base_models[tracking_database].connection }
+ let(:example_data) { define_batchable_model(new_table_name, connection: connection) }
- around do |example|
- Gitlab::Database::SharedModel.using_connection(connection) do
- example.run
+ around do |example|
+ Gitlab::Database::SharedModel.using_connection(connection) do
+ example.run
+ end
end
- end
-
- before do
- stub_feature_flags(execute_batched_migrations_on_schedule: true)
- # Create example table populated with test data to migrate.
- #
- # Test data should have two records that won't be updated:
- # - one record beyond the migration's range
- # - one record that doesn't match the migration job's batch condition
- connection.execute(<<~SQL)
+ before do
+ stub_feature_flags(execute_batched_migrations_on_schedule: true)
+
+ # Create example table populated with test data to migrate.
+ #
+ # Test data should have two records that won't be updated:
+ # - one record beyond the migration's range
+ # - one record that doesn't match the migration job's batch condition
+ connection.execute(<<~SQL)
CREATE TABLE #{new_table_name} (
id integer primary key,
some_column integer,
@@ -339,21 +264,20 @@ RSpec.shared_examples 'it runs batched background migration jobs' do |tracking_d
UPDATE #{new_table_name}
SET status = 0
WHERE some_column = #{migration_records - 5};
- SQL
+ SQL
- stub_const('Gitlab::BackgroundMigration::ExampleDataMigration', migration_class)
- end
+ stub_const('Gitlab::BackgroundMigration::ExampleDataMigration', migration_class)
+ end
- subject(:full_migration_run) do
- # process all batches, then do an extra execution to mark the job as finished
- (number_of_batches + 1).times do
- described_class.new.perform
+ subject(:full_migration_run) do
+ # process all batches, then do an extra execution to mark the job as finished
+ (number_of_batches + 1).times do
+ described_class.new.perform
- travel_to((migration.interval + described_class::INTERVAL_VARIANCE).seconds.from_now)
+ travel_to((migration.interval + described_class::INTERVAL_VARIANCE).seconds.from_now)
+ end
end
- end
- shared_examples 'batched background migration execution' do
it 'marks the migration record as finished' do
expect { full_migration_run }.to change { migration.reload.status }.from(1).to(3) # active -> finished
end
@@ -407,8 +331,8 @@ RSpec.shared_examples 'it runs batched background migration jobs' do |tracking_d
end
it 'puts migration on hold when the pending WAL count is above the limit' do
- sql = Gitlab::Database::BackgroundMigration::HealthStatus::Indicators::WriteAheadLog::PENDING_WAL_COUNT_SQL
- limit = Gitlab::Database::BackgroundMigration::HealthStatus::Indicators::WriteAheadLog::LIMIT
+ sql = Gitlab::Database::HealthStatus::Indicators::WriteAheadLog::PENDING_WAL_COUNT_SQL
+ limit = Gitlab::Database::HealthStatus::Indicators::WriteAheadLog::LIMIT
expect(connection).to receive(:execute).with(sql).and_return([{ 'pending_wal_count' => limit + 1 }])
@@ -416,30 +340,4 @@ RSpec.shared_examples 'it runs batched background migration jobs' do |tracking_d
end
end
end
-
- context 'when parallel execution is disabled' do
- before do
- stub_feature_flags(batched_migrations_parallel_execution: false)
- end
-
- it_behaves_like 'batched background migration execution'
-
- it 'assigns proper feature category to the context and the worker' do
- expected_feature_category = migration_class.feature_category.to_s
-
- expect { full_migration_run }.to change {
- Gitlab::ApplicationContext.current["meta.feature_category"]
- }.to(expected_feature_category)
- .and change { described_class.get_feature_category }.from(:database).to(expected_feature_category)
- end
- end
-
- context 'when parallel execution is enabled', :sidekiq_inline do
- before do
- stub_feature_flags(batched_migrations_parallel_execution: true)
- end
-
- it_behaves_like 'batched background migration execution'
- end
- end
end