diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-06-20 13:43:29 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-06-20 13:43:29 +0300 |
commit | 3b1af5cc7ed2666ff18b718ce5d30fa5a2756674 (patch) | |
tree | 3bc4a40e0ee51ec27eabf917c537033c0c5b14d4 /spec/support/shared_examples | |
parent | 9bba14be3f2c211bf79e15769cd9b77bc73a13bc (diff) |
Add latest changes from gitlab-org/gitlab@16-1-stable-eev16.1.0-rc42
Diffstat (limited to 'spec/support/shared_examples')
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 |