summaryrefslogtreecommitdiff
path: root/spec/support/shared_examples
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-02-18 10:34:06 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2021-02-18 10:34:06 +0000
commit859a6fb938bb9ee2a317c46dfa4fcc1af49608f0 (patch)
treed7f2700abe6b4ffcb2dcfc80631b2d87d0609239 /spec/support/shared_examples
parent446d496a6d000c73a304be52587cd9bbc7493136 (diff)
downloadgitlab-ce-859a6fb938bb9ee2a317c46dfa4fcc1af49608f0.tar.gz
Add latest changes from gitlab-org/gitlab@13-9-stable-eev13.9.0-rc42
Diffstat (limited to 'spec/support/shared_examples')
-rw-r--r--spec/support/shared_examples/alert_notification_service_shared_examples.rb18
-rw-r--r--spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/controllers/unique_hll_events_examples.rb18
-rw-r--r--spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/features/comment_and_close_button_shared_examples.rb29
-rw-r--r--spec/support/shared_examples/features/discussion_comments_shared_example.rb3
-rw-r--r--spec/support/shared_examples/features/editable_merge_request_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/features/multiple_reviewers_mr_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/features/navbar_shared_examples.rb5
-rw-r--r--spec/support/shared_examples/features/protected_branches_access_control_ce_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/features/protected_branches_with_deploy_keys_examples.rb3
-rw-r--r--spec/support/shared_examples/features/search_settings_shared_examples.rb42
-rw-r--r--spec/support/shared_examples/finders/packages_shared_examples.rb20
-rw-r--r--spec/support/shared_examples/graphql/label_fields.rb4
-rw-r--r--spec/support/shared_examples/graphql/mutations/boards_list_create_shared_examples.rb83
-rw-r--r--spec/support/shared_examples/graphql/mutations/can_mutate_spammable_examples.rb36
-rw-r--r--spec/support/shared_examples/graphql/mutations/http_integrations_shared_examples.rb75
-rw-r--r--spec/support/shared_examples/graphql/mutations/spammable_mutation_fields_examples.rb50
-rw-r--r--spec/support/shared_examples/graphql/notes_creation_shared_examples.rb19
-rw-r--r--spec/support/shared_examples/helpers/issuable_description_templates_shared_examples.rb49
-rw-r--r--spec/support/shared_examples/lib/api/internal_base_shared_examples.rb36
-rw-r--r--spec/support/shared_examples/lib/gitlab/search_results_sorted_shared_examples.rb24
-rw-r--r--spec/support/shared_examples/lib/gitlab/usage_data_counters/issue_activity_shared_examples.rb8
-rw-r--r--spec/support/shared_examples/models/atomic_internal_id_shared_examples.rb95
-rw-r--r--spec/support/shared_examples/models/concerns/can_housekeep_repository_shared_examples.rb45
-rw-r--r--spec/support/shared_examples/models/concerns/can_move_repository_storage_shared_examples.rb11
-rw-r--r--spec/support/shared_examples/models/concerns/has_repository_shared_examples.rb32
-rw-r--r--spec/support/shared_examples/models/concerns/repositories/can_housekeep_repository_shared_examples.rb6
-rw-r--r--spec/support/shared_examples/models/concerns/repository_storage_movable_shared_examples.rb11
-rw-r--r--spec/support/shared_examples/models/packages/debian/architecture_shared_examples.rb1
-rw-r--r--spec/support/shared_examples/models/packages/debian/component_file_shared_example.rb245
-rw-r--r--spec/support/shared_examples/models/packages/debian/component_shared_examples.rb51
-rw-r--r--spec/support/shared_examples/models/packages/debian/distribution_shared_examples.rb6
-rw-r--r--spec/support/shared_examples/models/wiki_shared_examples.rb9
-rw-r--r--spec/support/shared_examples/models/with_debian_distributions_shared_examples.rb24
-rw-r--r--spec/support/shared_examples/namespaces/recursive_traversal_examples.rb78
-rw-r--r--spec/support/shared_examples/quick_actions/issuable/close_quick_action_shared_examples.rb1
-rw-r--r--spec/support/shared_examples/quick_actions/issue/clone_quick_action_shared_examples.rb8
-rw-r--r--spec/support/shared_examples/quick_actions/issue/create_merge_request_quick_action_shared_examples.rb6
-rw-r--r--spec/support/shared_examples/quick_actions/issue/move_quick_action_shared_examples.rb6
-rw-r--r--spec/support/shared_examples/quick_actions/issue/zoom_quick_actions_shared_examples.rb10
-rw-r--r--spec/support/shared_examples/requests/api/debian_packages_shared_examples.rb43
-rw-r--r--spec/support/shared_examples/requests/api/graphql/mutations/create_list_shared_examples.rb51
-rw-r--r--spec/support/shared_examples/requests/api/graphql/noteable_shared_examples.rb62
-rw-r--r--spec/support/shared_examples/requests/api/notes_shared_examples.rb29
-rw-r--r--spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb490
-rw-r--r--spec/support/shared_examples/requests/api/npm_packages_tags_shared_examples.rb190
-rw-r--r--spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/requests/api/packages_shared_examples.rb10
-rw-r--r--spec/support/shared_examples/requests/api/packages_tags_shared_examples.rb185
-rw-r--r--spec/support/shared_examples/requests/api/read_user_shared_examples.rb32
-rw-r--r--spec/support/shared_examples/requests/api/repository_storage_moves_shared_examples.rb44
-rw-r--r--spec/support/shared_examples/requests/api/resolvable_discussions_shared_examples.rb3
-rw-r--r--spec/support/shared_examples/requests/rack_attack_shared_examples.rb8
-rw-r--r--spec/support/shared_examples/services/boards/boards_list_service_shared_examples.rb28
-rw-r--r--spec/support/shared_examples/services/boards/lists_create_service_shared_examples.rb81
-rw-r--r--spec/support/shared_examples/services/issuable_shared_examples.rb21
-rw-r--r--spec/support/shared_examples/services/packages_shared_examples.rb52
-rw-r--r--spec/support/shared_examples/services/projects/update_repository_storage_service_shared_examples.rb19
-rw-r--r--spec/support/shared_examples/services/repositories/housekeeping_shared_examples.rb18
-rw-r--r--spec/support/shared_examples/services/resource_events/change_milestone_service_shared_examples.rb8
-rw-r--r--spec/support/shared_examples/services/snippets_shared_examples.rb68
-rw-r--r--spec/support/shared_examples/workers/concerns/git_garbage_collect_methods_shared_examples.rb320
63 files changed, 2261 insertions, 680 deletions
diff --git a/spec/support/shared_examples/alert_notification_service_shared_examples.rb b/spec/support/shared_examples/alert_notification_service_shared_examples.rb
index 1568e4357a1..7bd6df8c608 100644
--- a/spec/support/shared_examples/alert_notification_service_shared_examples.rb
+++ b/spec/support/shared_examples/alert_notification_service_shared_examples.rb
@@ -3,7 +3,7 @@
RSpec.shared_examples 'Alert Notification Service sends notification email' do
let(:notification_service) { spy }
- it 'sends a notification for firing alerts only' do
+ it 'sends a notification' do
expect(NotificationService)
.to receive(:new)
.and_return(notification_service)
@@ -15,15 +15,15 @@ RSpec.shared_examples 'Alert Notification Service sends notification email' do
end
end
-RSpec.shared_examples 'Alert Notification Service sends no notifications' do |http_status:|
- let(:notification_service) { spy }
- let(:create_events_service) { spy }
-
+RSpec.shared_examples 'Alert Notification Service sends no notifications' do |http_status: nil|
it 'does not notify' do
- expect(notification_service).not_to receive(:async)
- expect(create_events_service).not_to receive(:execute)
+ expect(NotificationService).not_to receive(:new)
- expect(subject).to be_error
- expect(subject.http_status).to eq(http_status)
+ if http_status.present?
+ expect(subject).to be_error
+ expect(subject.http_status).to eq(http_status)
+ else
+ expect(subject).to be_success
+ end
end
end
diff --git a/spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb b/spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb
index f89d52f81ad..7f49d20c83e 100644
--- a/spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb
+++ b/spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb
@@ -3,6 +3,8 @@
RSpec.shared_examples 'multiple issue boards' do
context 'authorized user' do
before do
+ stub_feature_flags(board_new_list: false)
+
parent.add_maintainer(user)
login_as(user)
diff --git a/spec/support/shared_examples/controllers/unique_hll_events_examples.rb b/spec/support/shared_examples/controllers/unique_hll_events_examples.rb
index c5d65743810..842ad89bafd 100644
--- a/spec/support/shared_examples/controllers/unique_hll_events_examples.rb
+++ b/spec/support/shared_examples/controllers/unique_hll_events_examples.rb
@@ -5,20 +5,14 @@
# - expected_type
# - target_id
-RSpec.shared_examples 'tracking unique hll events' do |feature_flag|
+RSpec.shared_examples 'tracking unique hll events' do
it 'tracks unique event' do
- expect(Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:track_event).with(target_id, values: expected_type)
+ expect(Gitlab::UsageDataCounters::HLLRedisCounter).to(
+ receive(:track_event)
+ .with(target_id, values: expected_type)
+ .and_call_original # we call original to trigger additional validations; otherwise the method is stubbed
+ )
request
end
-
- context 'when feature flag is disabled' do
- it 'does not track unique event' do
- stub_feature_flags(feature_flag => false)
-
- expect(Gitlab::UsageDataCounters::HLLRedisCounter).not_to receive(:track_event)
-
- request
- end
- end
end
diff --git a/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb b/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb
index dcbf494186a..0a040557ffe 100644
--- a/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb
+++ b/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb
@@ -218,7 +218,7 @@ RSpec.shared_examples 'wiki controller actions' do
end
context 'page view tracking' do
- it_behaves_like 'tracking unique hll events', :track_unique_wiki_page_views do
+ it_behaves_like 'tracking unique hll events' do
let(:target_id) { 'wiki_action' }
let(:expected_type) { instance_of(String) }
end
diff --git a/spec/support/shared_examples/features/comment_and_close_button_shared_examples.rb b/spec/support/shared_examples/features/comment_and_close_button_shared_examples.rb
new file mode 100644
index 00000000000..4ee2840ed9f
--- /dev/null
+++ b/spec/support/shared_examples/features/comment_and_close_button_shared_examples.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'page with comment and close button' do |button_text|
+ context 'when remove_comment_close_reopen feature flag is enabled' do
+ before do
+ stub_feature_flags(remove_comment_close_reopen: true)
+ setup
+ end
+
+ it "does not show #{button_text} button" do
+ within '.note-form-actions' do
+ expect(page).not_to have_button(button_text)
+ end
+ end
+ end
+
+ context 'when remove_comment_close_reopen feature flag is disabled' do
+ before do
+ stub_feature_flags(remove_comment_close_reopen: false)
+ setup
+ end
+
+ it "shows #{button_text} button" do
+ within '.note-form-actions' do
+ expect(page).to have_button(button_text)
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/features/discussion_comments_shared_example.rb b/spec/support/shared_examples/features/discussion_comments_shared_example.rb
index 560cfbfb117..6bebd59ed70 100644
--- a/spec/support/shared_examples/features/discussion_comments_shared_example.rb
+++ b/spec/support/shared_examples/features/discussion_comments_shared_example.rb
@@ -150,12 +150,13 @@ RSpec.shared_examples 'thread comments' do |resource_name|
wait_for_requests
end
- it 'clicking "Start thread" will post a thread' do
+ it 'clicking "Start thread" will post a thread and show a reply component' do
expect(page).to have_content(comment)
new_comment = all(comments_selector).last
expect(new_comment).to have_selector('.discussion')
+ expect(new_comment).to have_css('.discussion-with-resolve-btn')
end
if resource_name =~ /(issue|merge request)/
diff --git a/spec/support/shared_examples/features/editable_merge_request_shared_examples.rb b/spec/support/shared_examples/features/editable_merge_request_shared_examples.rb
index 2fff4137934..ccd063faac4 100644
--- a/spec/support/shared_examples/features/editable_merge_request_shared_examples.rb
+++ b/spec/support/shared_examples/features/editable_merge_request_shared_examples.rb
@@ -48,7 +48,7 @@ RSpec.shared_examples 'an editable merge request' do
end
page.within '.reviewer' do
- expect(page).to have_content user.name
+ expect(page).to have_content user.username
end
page.within '.milestone' do
diff --git a/spec/support/shared_examples/features/multiple_reviewers_mr_shared_examples.rb b/spec/support/shared_examples/features/multiple_reviewers_mr_shared_examples.rb
index 48cde90bd9b..ad6ca3e1900 100644
--- a/spec/support/shared_examples/features/multiple_reviewers_mr_shared_examples.rb
+++ b/spec/support/shared_examples/features/multiple_reviewers_mr_shared_examples.rb
@@ -40,7 +40,7 @@ RSpec.shared_examples 'multiple reviewers merge request' do |action, save_button
# Closing dropdown to persist
click_link 'Edit'
- expect(page).to have_content user2.name
+ expect(page).to have_content user2.username
end
end
end
diff --git a/spec/support/shared_examples/features/navbar_shared_examples.rb b/spec/support/shared_examples/features/navbar_shared_examples.rb
index c768e95c45a..9b89a3b5e54 100644
--- a/spec/support/shared_examples/features/navbar_shared_examples.rb
+++ b/spec/support/shared_examples/features/navbar_shared_examples.rb
@@ -8,12 +8,13 @@ RSpec.shared_examples 'verified navigation bar' do
end
it 'renders correctly' do
- current_structure = page.all('.sidebar-top-level-items > li', class: ['!hidden']).map do |item|
+ # we are using * here in the selectors to prevent a regression where we added a non 'li' inside an 'ul'
+ current_structure = page.all('.sidebar-top-level-items > *', class: ['!hidden']).map do |item|
next if item.find_all('a').empty?
nav_item = item.find_all('a').first.text.gsub(/\s+\d+$/, '') # remove counts at the end
- nav_sub_items = item.all('.sidebar-sub-level-items > li', class: ['!fly-out-top-item']).map do |list_item|
+ nav_sub_items = item.all('.sidebar-sub-level-items > *', class: ['!fly-out-top-item']).map do |list_item|
list_item.all('a').first.text
end
diff --git a/spec/support/shared_examples/features/protected_branches_access_control_ce_shared_examples.rb b/spec/support/shared_examples/features/protected_branches_access_control_ce_shared_examples.rb
index a46382bc292..56154c7cd03 100644
--- a/spec/support/shared_examples/features/protected_branches_access_control_ce_shared_examples.rb
+++ b/spec/support/shared_examples/features/protected_branches_access_control_ce_shared_examples.rb
@@ -56,6 +56,8 @@ RSpec.shared_examples "protected branches > access control > CE" do
expect(first("li")).to have_content("Roles")
find(:link, access_type_name).click
end
+
+ find(".js-allowed-to-push").click
end
wait_for_requests
diff --git a/spec/support/shared_examples/features/protected_branches_with_deploy_keys_examples.rb b/spec/support/shared_examples/features/protected_branches_with_deploy_keys_examples.rb
index a2d2143271c..28fe198c9c3 100644
--- a/spec/support/shared_examples/features/protected_branches_with_deploy_keys_examples.rb
+++ b/spec/support/shared_examples/features/protected_branches_with_deploy_keys_examples.rb
@@ -1,8 +1,7 @@
# frozen_string_literal: true
-RSpec.shared_examples 'when the deploy_keys_on_protected_branches FF is turned on' do
+RSpec.shared_examples 'Deploy keys with protected branches' do
before do
- stub_feature_flags(deploy_keys_on_protected_branches: true)
project.add_maintainer(user)
sign_in(user)
end
diff --git a/spec/support/shared_examples/features/search_settings_shared_examples.rb b/spec/support/shared_examples/features/search_settings_shared_examples.rb
new file mode 100644
index 00000000000..6a507c4be56
--- /dev/null
+++ b/spec/support/shared_examples/features/search_settings_shared_examples.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'cannot search settings' do
+ it 'does note have search settings field' do
+ expect(page).not_to have_field(placeholder: SearchHelpers::INPUT_PLACEHOLDER)
+ end
+end
+
+RSpec.shared_examples 'can search settings' do |search_term, non_match_section|
+ it 'has search settings field' do
+ expect(page).to have_field(placeholder: SearchHelpers::INPUT_PLACEHOLDER)
+ end
+
+ it 'hides unmatching sections on search' do
+ expect(page).to have_content(non_match_section)
+
+ fill_in SearchHelpers::INPUT_PLACEHOLDER, with: search_term
+
+ expect(page).to have_content(search_term)
+ expect(page).not_to have_content(non_match_section)
+ end
+end
+
+RSpec.shared_examples 'can search settings with feature flag check' do |search_term, non_match_section|
+ let(:flag) { true }
+
+ before do
+ stub_feature_flags(search_settings_in_page: flag)
+
+ visit(visit_path)
+ end
+
+ context 'with feature flag on' do
+ it_behaves_like 'can search settings', search_term, non_match_section
+ end
+
+ context 'with feature flag off' do
+ let(:flag) { false }
+
+ it_behaves_like 'cannot search settings'
+ end
+end
diff --git a/spec/support/shared_examples/finders/packages_shared_examples.rb b/spec/support/shared_examples/finders/packages_shared_examples.rb
index 52976565b21..2d4e8d0df1f 100644
--- a/spec/support/shared_examples/finders/packages_shared_examples.rb
+++ b/spec/support/shared_examples/finders/packages_shared_examples.rb
@@ -17,3 +17,23 @@ RSpec.shared_examples 'concerning versionless param' do
it { is_expected.not_to include(versionless_package) }
end
end
+
+RSpec.shared_examples 'concerning package statuses' do
+ let_it_be(:hidden_package) { create(:maven_package, :hidden, project: project) }
+
+ context 'hidden packages' do
+ it { is_expected.not_to include(hidden_package) }
+ end
+
+ context 'with status param' do
+ let(:params) { { status: :hidden } }
+
+ it { is_expected.to match_array([hidden_package]) }
+ end
+
+ context 'with invalid status param' do
+ let(:params) { { status: 'invalid_status' } }
+
+ it { expect { subject }.to raise_exception(described_class::InvalidStatusError) }
+ end
+end
diff --git a/spec/support/shared_examples/graphql/label_fields.rb b/spec/support/shared_examples/graphql/label_fields.rb
index caf5dae409a..4159e4e03ab 100644
--- a/spec/support/shared_examples/graphql/label_fields.rb
+++ b/spec/support/shared_examples/graphql/label_fields.rb
@@ -18,7 +18,7 @@ RSpec.shared_examples 'a GraphQL type with labels' do
subject { described_class.fields['labels'] }
it { is_expected.to have_graphql_type(Types::LabelType.connection_type) }
- it { is_expected.to have_graphql_arguments(:search_term) }
+ it { is_expected.to have_graphql_arguments(labels_resolver_arguments) }
end
end
@@ -105,7 +105,7 @@ RSpec.shared_examples 'querying a GraphQL type with labels' do
run_query(query_for(label_a))
end
- it 'batches queries for labels by title' do
+ it 'batches queries for labels by title', :request_store do
multi_selection = query_for(label_b, label_c)
single_selection = query_for(label_d)
diff --git a/spec/support/shared_examples/graphql/mutations/boards_list_create_shared_examples.rb b/spec/support/shared_examples/graphql/mutations/boards_list_create_shared_examples.rb
new file mode 100644
index 00000000000..b096a5e17c0
--- /dev/null
+++ b/spec/support/shared_examples/graphql/mutations/boards_list_create_shared_examples.rb
@@ -0,0 +1,83 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.shared_examples 'board lists create mutation' do
+ include GraphqlHelpers
+
+ let_it_be(:user) { create(:user) }
+
+ let(:mutation) { described_class.new(object: nil, context: { current_user: user }, field: nil) }
+ let(:list_create_params) { {} }
+
+ subject { mutation.resolve(board_id: board.to_global_id.to_s, **list_create_params) }
+
+ describe '#ready?' do
+ it 'raises an error if required arguments are missing' do
+ expect { mutation.ready?(board_id: 'some id') }
+ .to raise_error(Gitlab::Graphql::Errors::ArgumentError, /one and only one of/)
+ end
+
+ it 'raises an error if too many required arguments are specified' do
+ expect { mutation.ready?(board_id: 'some id', backlog: true, label_id: 'some label') }
+ .to raise_error(Gitlab::Graphql::Errors::ArgumentError, /one and only one of/)
+ end
+ end
+
+ describe '#resolve' do
+ context 'with proper permissions' do
+ before_all do
+ group.add_reporter(user)
+ end
+
+ describe 'backlog list' do
+ let(:list_create_params) { { backlog: true } }
+
+ it 'creates one and only one backlog' do
+ expect { subject }.to change { board.lists.backlog.count }.by(1)
+ expect(board.lists.backlog.first.list_type).to eq 'backlog'
+
+ backlog_id = board.lists.backlog.first.id
+
+ expect { subject }.not_to change { board.lists.backlog.count }
+ expect(board.lists.backlog.last.id).to eq backlog_id
+ end
+ end
+
+ describe 'label list' do
+ let_it_be(:dev_label) do
+ create(:group_label, title: 'Development', color: '#FFAABB', group: group)
+ end
+
+ let(:list_create_params) { { label_id: dev_label.to_global_id.to_s } }
+
+ it 'creates a new label board list' do
+ expect { subject }.to change { board.lists.count }.by(1)
+
+ new_list = subject[:list]
+
+ expect(new_list.title).to eq dev_label.title
+ expect(new_list.position).to eq 0
+ end
+
+ context 'when label not found' do
+ let(:list_create_params) { { label_id: "gid://gitlab/Label/#{non_existing_record_id}" } }
+
+ it 'returns an error' do
+ expect(subject[:errors]).to include 'Label not found'
+ end
+ end
+ end
+ end
+
+ context 'without proper permissions' do
+ before_all do
+ group.add_guest(user)
+ end
+
+ it 'raises an error' do
+ expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
+ end
+ end
+ end
+end
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
new file mode 100644
index 00000000000..d294f034d2e
--- /dev/null
+++ b/spec/support/shared_examples/graphql/mutations/can_mutate_spammable_examples.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.shared_examples 'a mutation which can mutate a spammable' do
+ describe "#additional_spam_params" do
+ it 'passes additional spam params to the service' do
+ args = [
+ anything,
+ anything,
+ hash_including(
+ api: true,
+ request: instance_of(ActionDispatch::Request),
+ captcha_response: captcha_response,
+ spam_log_id: spam_log_id
+ )
+ ]
+ expect(service).to receive(:new).with(*args).and_call_original
+
+ subject
+ end
+ end
+
+ describe "#with_spam_action_fields" do
+ it 'resolves with spam action fields' do
+ subject
+
+ # NOTE: We do not need to assert on the specific values of spam action fields here, we only need
+ # to verify that #with_spam_action_fields was invoked and that the fields are present in the
+ # response. The specific behavior of #with_spam_action_fields is covered in the
+ # CanMutateSpammable unit tests.
+ expect(mutation_response.keys)
+ .to include('spam', 'spamLogId', 'needsCaptchaResponse', 'captchaSiteKey')
+ end
+ end
+end
diff --git a/spec/support/shared_examples/graphql/mutations/http_integrations_shared_examples.rb b/spec/support/shared_examples/graphql/mutations/http_integrations_shared_examples.rb
index 0338eb43f8d..4468af1a603 100644
--- a/spec/support/shared_examples/graphql/mutations/http_integrations_shared_examples.rb
+++ b/spec/support/shared_examples/graphql/mutations/http_integrations_shared_examples.rb
@@ -17,3 +17,78 @@ RSpec.shared_examples 'creating a new HTTP integration' do
expect(integration_response['apiUrl']).to eq(nil)
end
end
+
+RSpec.shared_examples 'updating an existing HTTP integration' do
+ it 'updates the integration' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ integration_response = mutation_response['integration']
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(integration_response['id']).to eq(GitlabSchema.id_from_object(integration).to_s)
+ expect(integration_response['name']).to eq('Modified Name')
+ expect(integration_response['active']).to be_falsey
+ expect(integration_response['url']).to include('modified-name')
+ end
+end
+
+RSpec.shared_examples 'validating the payload_example' do
+ context 'with invalid payloadExample attribute' do
+ let(:payload_example) { 'not a JSON' }
+
+ it 'responds with errors' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect_graphql_errors_to_include(/was provided invalid value for payloadExample \(Invalid JSON string/)
+ end
+ end
+
+ it 'validates the payload_example size' do
+ allow(::Gitlab::Utils::DeepSize)
+ .to receive(:new)
+ .with(Gitlab::Json.parse(payload_example))
+ .and_return(double(valid?: false))
+
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect_graphql_errors_to_include(/payloadExample JSON is too big/)
+ end
+end
+
+RSpec.shared_examples 'validating the payload_attribute_mappings' do
+ context 'with invalid payloadAttributeMapping attribute does not contain fieldName' do
+ let(:payload_attribute_mappings) do
+ [{ path: %w[alert name], type: 'STRING' }]
+ end
+
+ it 'responds with errors' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect_graphql_errors_to_include(/was provided invalid value for payloadAttributeMappings\.0\.fieldName \(Expected value to not be null/)
+ end
+ end
+
+ context 'with invalid payloadAttributeMapping attribute does not contain path' do
+ let(:payload_attribute_mappings) do
+ [{ fieldName: 'TITLE', type: 'STRING' }]
+ end
+
+ it 'responds with errors' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect_graphql_errors_to_include(/was provided invalid value for payloadAttributeMappings\.0\.path \(Expected value to not be null/)
+ end
+ end
+
+ context 'with invalid payloadAttributeMapping attribute does not contain type' do
+ let(:payload_attribute_mappings) do
+ [{ fieldName: 'TITLE', path: %w[alert name] }]
+ end
+
+ it 'responds with errors' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect_graphql_errors_to_include(/was provided invalid value for payloadAttributeMappings\.0\.type \(Expected value to not be null/)
+ end
+ end
+end
diff --git a/spec/support/shared_examples/graphql/mutations/spammable_mutation_fields_examples.rb b/spec/support/shared_examples/graphql/mutations/spammable_mutation_fields_examples.rb
deleted file mode 100644
index 8678b23ad31..00000000000
--- a/spec/support/shared_examples/graphql/mutations/spammable_mutation_fields_examples.rb
+++ /dev/null
@@ -1,50 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.shared_examples 'spam flag is present' do
- specify :aggregate_failures do
- subject
-
- expect(mutation_response).to have_key('spam')
- expect(mutation_response['spam']).to be_falsey
- end
-end
-
-RSpec.shared_examples 'can raise spam flag' do
- it 'spam parameters are passed to the service' do
- args = [anything, anything, hash_including(api: true, request: instance_of(ActionDispatch::Request))]
- expect(service).to receive(:new).with(*args).and_call_original
-
- subject
- end
-
- context 'when the snippet is detected as spam' do
- it 'raises spam flag' do
- allow_next_instance_of(service) do |instance|
- allow(instance).to receive(:spam_check) do |snippet, user, _|
- snippet.spam!
- end
- end
-
- subject
-
- expect(mutation_response['spam']).to be true
- expect(mutation_response['errors']).to include("Your snippet has been recognized as spam and has been discarded.")
- end
- end
-
- context 'when :snippet_spam flag is disabled' do
- before do
- stub_feature_flags(snippet_spam: false)
- end
-
- it 'request parameter is not passed to the service' do
- expect(service).to receive(:new)
- .with(anything, anything, hash_not_including(request: instance_of(ActionDispatch::Request)))
- .and_call_original
-
- subject
- end
- end
-end
diff --git a/spec/support/shared_examples/graphql/notes_creation_shared_examples.rb b/spec/support/shared_examples/graphql/notes_creation_shared_examples.rb
index 24c8a247c93..fb598b978f6 100644
--- a/spec/support/shared_examples/graphql/notes_creation_shared_examples.rb
+++ b/spec/support/shared_examples/graphql/notes_creation_shared_examples.rb
@@ -64,3 +64,22 @@ RSpec.shared_examples 'a Note mutation when the given resource id is not for a N
let(:match_errors) { include(/does not represent an instance of Note/) }
end
end
+
+RSpec.shared_examples 'a Note mutation when there are rate limit validation errors' do
+ before do
+ stub_application_setting(notes_create_limit: 3)
+ 3.times { post_graphql_mutation(mutation, current_user: current_user) }
+ end
+
+ it_behaves_like 'a Note mutation that does not create a Note'
+ it_behaves_like 'a mutation that returns top-level errors',
+ errors: ['This endpoint has been requested too many times. Try again later.']
+
+ context 'when the user is in the allowlist' do
+ before do
+ stub_application_setting(notes_create_limit_allowlist: ["#{current_user.username}"])
+ end
+
+ it_behaves_like 'a Note mutation that creates a Note'
+ end
+end
diff --git a/spec/support/shared_examples/helpers/issuable_description_templates_shared_examples.rb b/spec/support/shared_examples/helpers/issuable_description_templates_shared_examples.rb
new file mode 100644
index 00000000000..9e8c96d576a
--- /dev/null
+++ b/spec/support/shared_examples/helpers/issuable_description_templates_shared_examples.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+RSpec.shared_context 'project issuable templates context' do
+ let_it_be(:issuable_template_files) do
+ {
+ '.gitlab/issue_templates/issue-bar.md' => 'Issue Template Bar',
+ '.gitlab/issue_templates/issue-foo.md' => 'Issue Template Foo',
+ '.gitlab/issue_templates/issue-bad.txt' => 'Issue Template Bad',
+ '.gitlab/issue_templates/issue-baz.xyz' => 'Issue Template Baz',
+
+ '.gitlab/merge_request_templates/merge_request-bar.md' => 'Merge Request Template Bar',
+ '.gitlab/merge_request_templates/merge_request-foo.md' => 'Merge Request Template Foo',
+ '.gitlab/merge_request_templates/merge_request-bad.txt' => 'Merge Request Template Bad',
+ '.gitlab/merge_request_templates/merge_request-baz.xyz' => 'Merge Request Template Baz'
+ }
+ end
+end
+
+RSpec.shared_examples 'project issuable templates' do
+ context 'issuable templates' do
+ before do
+ allow(helper).to receive(:current_user).and_return(user)
+ end
+
+ it 'returns only md files as issue templates' do
+ expect(helper.issuable_templates(project, 'issue')).to eq(templates('issue', project))
+ end
+
+ it 'returns only md files as merge_request templates' do
+ expect(helper.issuable_templates(project, 'merge_request')).to eq(templates('merge_request', project))
+ end
+ end
+
+ def expected_templates(issuable_type)
+ expectation = {}
+
+ expectation["Project Templates"] = templates(issuable_type, project)
+ expectation["Group #{inherited_from.namespace.full_name}"] = templates(issuable_type, inherited_from) if inherited_from.present?
+
+ expectation
+ end
+
+ def templates(issuable_type, inherited_from)
+ [
+ { id: "#{issuable_type}-bar", key: "#{issuable_type}-bar", name: "#{issuable_type}-bar", project_id: inherited_from&.id },
+ { id: "#{issuable_type}-foo", key: "#{issuable_type}-foo", name: "#{issuable_type}-foo", project_id: inherited_from&.id }
+ ]
+ end
+end
diff --git a/spec/support/shared_examples/lib/api/internal_base_shared_examples.rb b/spec/support/shared_examples/lib/api/internal_base_shared_examples.rb
new file mode 100644
index 00000000000..dfa1388e0bb
--- /dev/null
+++ b/spec/support/shared_examples/lib/api/internal_base_shared_examples.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'actor key validations' do
+ context 'key id is not provided' do
+ let(:key_id) { nil }
+
+ it 'returns an error message' do
+ subject
+
+ expect(json_response['success']).to be_falsey
+ expect(json_response['message']).to eq('Could not find a user without a key')
+ end
+ end
+
+ context 'key does not exist' do
+ let(:key_id) { non_existing_record_id }
+
+ it 'returns an error message' do
+ subject
+
+ expect(json_response['success']).to be_falsey
+ expect(json_response['message']).to eq('Could not find the given key')
+ end
+ end
+
+ context 'key without user' do
+ let(:key_id) { create(:key, user: nil).id }
+
+ it 'returns an error message' do
+ subject
+
+ expect(json_response['success']).to be_falsey
+ expect(json_response['message']).to eq('Could not find a user for the given key')
+ end
+ end
+end
diff --git a/spec/support/shared_examples/lib/gitlab/search_results_sorted_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/search_results_sorted_shared_examples.rb
index 07d01d5c50e..eafb49cef71 100644
--- a/spec/support/shared_examples/lib/gitlab/search_results_sorted_shared_examples.rb
+++ b/spec/support/shared_examples/lib/gitlab/search_results_sorted_shared_examples.rb
@@ -1,19 +1,35 @@
# frozen_string_literal: true
RSpec.shared_examples 'search results sorted' do
- context 'sort: newest' do
+ context 'sort: created_desc' do
let(:sort) { 'created_desc' }
it 'sorts results by created_at' do
- expect(results.objects(scope).map(&:id)).to eq([new_result.id, old_result.id, very_old_result.id])
+ expect(results_created.objects(scope).map(&:id)).to eq([new_result.id, old_result.id, very_old_result.id])
end
end
- context 'sort: oldest' do
+ context 'sort: created_asc' do
let(:sort) { 'created_asc' }
it 'sorts results by created_at' do
- expect(results.objects(scope).map(&:id)).to eq([very_old_result.id, old_result.id, new_result.id])
+ expect(results_created.objects(scope).map(&:id)).to eq([very_old_result.id, old_result.id, new_result.id])
+ end
+ end
+
+ context 'sort: updated_desc' do
+ let(:sort) { 'updated_desc' }
+
+ it 'sorts results by updated_desc' do
+ expect(results_updated.objects(scope).map(&:id)).to eq([new_updated.id, old_updated.id, very_old_updated.id])
+ end
+ end
+
+ context 'sort: updated_asc' do
+ let(:sort) { 'updated_asc' }
+
+ it 'sorts results by updated_asc' do
+ expect(results_updated.objects(scope).map(&:id)).to eq([very_old_updated.id, old_updated.id, new_updated.id])
end
end
end
diff --git a/spec/support/shared_examples/lib/gitlab/usage_data_counters/issue_activity_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/usage_data_counters/issue_activity_shared_examples.rb
index 286305f2506..edd9b6cdf37 100644
--- a/spec/support/shared_examples/lib/gitlab/usage_data_counters/issue_activity_shared_examples.rb
+++ b/spec/support/shared_examples/lib/gitlab/usage_data_counters/issue_activity_shared_examples.rb
@@ -24,12 +24,4 @@ RSpec.shared_examples 'a tracked issue edit event' do |event|
it 'does not track edit actions if author is not present' do
expect(track_action(author: nil)).to be_nil
end
-
- context 'when feature flag track_issue_activity_actions is disabled' do
- it 'does not track edit actions' do
- stub_feature_flags(track_issue_activity_actions: false)
-
- expect(track_action(author: user1)).to be_nil
- end
- end
end
diff --git a/spec/support/shared_examples/models/atomic_internal_id_shared_examples.rb b/spec/support/shared_examples/models/atomic_internal_id_shared_examples.rb
index fe99b1cacd9..42f82987989 100644
--- a/spec/support/shared_examples/models/atomic_internal_id_shared_examples.rb
+++ b/spec/support/shared_examples/models/atomic_internal_id_shared_examples.rb
@@ -9,19 +9,30 @@ RSpec.shared_examples 'AtomicInternalId' do |validate_presence: true|
end
describe 'Validation' do
- before do
- allow_any_instance_of(described_class).to receive(:"ensure_#{scope}_#{internal_id_attribute}!")
-
- instance.valid?
- end
-
context 'when presence validation is required' do
before do
skip unless validate_presence
end
- it 'validates presence' do
- expect(instance.errors[internal_id_attribute]).to include("can't be blank")
+ context 'when creating an object' do
+ before do
+ allow_any_instance_of(described_class).to receive(:"ensure_#{scope}_#{internal_id_attribute}!")
+ end
+
+ it 'raises an error if the internal id is blank' do
+ expect { instance.save! }.to raise_error(AtomicInternalId::MissingValueError)
+ end
+ end
+
+ context 'when updating an object' do
+ it 'raises an error if the internal id is blank' do
+ instance.save!
+
+ write_internal_id(nil)
+ allow(instance).to receive(:"ensure_#{scope}_#{internal_id_attribute}!")
+
+ expect { instance.save! }.to raise_error(AtomicInternalId::MissingValueError)
+ end
end
end
@@ -30,8 +41,27 @@ RSpec.shared_examples 'AtomicInternalId' do |validate_presence: true|
skip if validate_presence
end
- it 'does not validate presence' do
- expect(instance.errors[internal_id_attribute]).to be_empty
+ context 'when creating an object' do
+ before do
+ allow_any_instance_of(described_class).to receive(:"ensure_#{scope}_#{internal_id_attribute}!")
+ end
+
+ it 'does not raise an error if the internal id is blank' do
+ expect(read_internal_id).to be_nil
+
+ expect { instance.save! }.not_to raise_error
+ end
+ end
+
+ context 'when updating an object' do
+ it 'does not raise an error if the internal id is blank' do
+ instance.save!
+
+ write_internal_id(nil)
+ allow(instance).to receive(:"ensure_#{scope}_#{internal_id_attribute}!")
+
+ expect { instance.save! }.not_to raise_error
+ end
end
end
end
@@ -76,6 +106,51 @@ RSpec.shared_examples 'AtomicInternalId' do |validate_presence: true|
end
end
+ describe 'unsetting the instance internal id on rollback' do
+ context 'when the internal id has been changed' do
+ context 'when the internal id is automatically set' do
+ it 'clears it on the instance' do
+ expect_iid_to_be_set_and_rollback
+
+ expect(read_internal_id).to be_nil
+ end
+ end
+
+ context 'when the internal id is manually set' do
+ it 'does not clear it on the instance' do
+ write_internal_id(100)
+
+ expect_iid_to_be_set_and_rollback
+
+ expect(read_internal_id).not_to be_nil
+ end
+ end
+ end
+
+ context 'when the internal id has not been changed' do
+ it 'preserves the value on the instance' do
+ instance.save!
+ original_id = read_internal_id
+
+ expect(original_id).not_to be_nil
+
+ expect_iid_to_be_set_and_rollback
+
+ expect(read_internal_id).to eq(original_id)
+ end
+ end
+
+ def expect_iid_to_be_set_and_rollback
+ ActiveRecord::Base.transaction(requires_new: true) do
+ instance.save!
+
+ expect(read_internal_id).not_to be_nil
+
+ raise ActiveRecord::Rollback
+ end
+ end
+ end
+
describe 'supply of internal ids' do
let(:scope_value) { scope_attrs.each_value.first }
let(:method_name) { :"with_#{scope}_#{internal_id_attribute}_supply" }
diff --git a/spec/support/shared_examples/models/concerns/can_housekeep_repository_shared_examples.rb b/spec/support/shared_examples/models/concerns/can_housekeep_repository_shared_examples.rb
deleted file mode 100644
index 2f0b95427d2..00000000000
--- a/spec/support/shared_examples/models/concerns/can_housekeep_repository_shared_examples.rb
+++ /dev/null
@@ -1,45 +0,0 @@
-# frozen_string_literal: true
-
-RSpec.shared_examples 'can housekeep repository' do
- context 'with a clean redis state', :clean_gitlab_redis_shared_state do
- describe '#pushes_since_gc' do
- context 'without any pushes' do
- it 'returns 0' do
- expect(resource.pushes_since_gc).to eq(0)
- end
- end
-
- context 'with a number of pushes' do
- it 'returns the number of pushes' do
- 3.times { resource.increment_pushes_since_gc }
-
- expect(resource.pushes_since_gc).to eq(3)
- end
- end
- end
-
- describe '#increment_pushes_since_gc' do
- it 'increments the number of pushes since the last GC' do
- 3.times { resource.increment_pushes_since_gc }
-
- expect(resource.pushes_since_gc).to eq(3)
- end
- end
-
- describe '#reset_pushes_since_gc' do
- it 'resets the number of pushes since the last GC' do
- 3.times { resource.increment_pushes_since_gc }
-
- resource.reset_pushes_since_gc
-
- expect(resource.pushes_since_gc).to eq(0)
- end
- end
-
- describe '#pushes_since_gc_redis_shared_state_key' do
- it 'returns the proper redis key format' do
- expect(resource.send(:pushes_since_gc_redis_shared_state_key)).to eq("#{resource_key}/#{resource.id}/pushes_since_gc")
- end
- end
- end
-end
diff --git a/spec/support/shared_examples/models/concerns/can_move_repository_storage_shared_examples.rb b/spec/support/shared_examples/models/concerns/can_move_repository_storage_shared_examples.rb
index 85a2c6f1449..8deeecea30d 100644
--- a/spec/support/shared_examples/models/concerns/can_move_repository_storage_shared_examples.rb
+++ b/spec/support/shared_examples/models/concerns/can_move_repository_storage_shared_examples.rb
@@ -2,11 +2,12 @@
RSpec.shared_examples 'can move repository storage' do
let(:container) { raise NotImplementedError }
+ let(:repository) { container.repository }
describe '#set_repository_read_only!' do
it 'makes the repository read-only' do
expect { container.set_repository_read_only! }
- .to change(container, :repository_read_only?)
+ .to change { container.repository_read_only? }
.from(false)
.to(true)
end
@@ -28,7 +29,7 @@ RSpec.shared_examples 'can move repository storage' do
allow(container).to receive(:git_transfer_in_progress?) { true }
expect { container.set_repository_read_only!(skip_git_transfer_check: true) }
- .to change(container, :repository_read_only?)
+ .to change { container.repository_read_only? }
.from(false)
.to(true)
end
@@ -38,16 +39,16 @@ RSpec.shared_examples 'can move repository storage' do
describe '#set_repository_writable!' do
it 'sets repository_read_only to false' do
expect { container.set_repository_writable! }
- .to change(container, :repository_read_only)
+ .to change { container.repository_read_only? }
.from(true).to(false)
end
end
describe '#reference_counter' do
it 'returns a Gitlab::ReferenceCounter object' do
- expect(Gitlab::ReferenceCounter).to receive(:new).with(container.repository.gl_repository).and_call_original
+ expect(Gitlab::ReferenceCounter).to receive(:new).with(repository.gl_repository).and_call_original
- result = container.reference_counter(type: container.repository.repo_type)
+ result = container.reference_counter(type: repository.repo_type)
expect(result).to be_a Gitlab::ReferenceCounter
end
diff --git a/spec/support/shared_examples/models/concerns/has_repository_shared_examples.rb b/spec/support/shared_examples/models/concerns/has_repository_shared_examples.rb
index 826ee453919..1be4d9b80a4 100644
--- a/spec/support/shared_examples/models/concerns/has_repository_shared_examples.rb
+++ b/spec/support/shared_examples/models/concerns/has_repository_shared_examples.rb
@@ -152,36 +152,4 @@ RSpec.shared_examples 'model with repository' do
it { is_expected.to respond_to(:disk_path) }
it { is_expected.to respond_to(:gitlab_shell) }
end
-
- describe '.pick_repository_storage' do
- subject { described_class.pick_repository_storage }
-
- before do
- storages = {
- 'default' => Gitlab::GitalyClient::StorageSettings.new('path' => 'tmp/tests/repositories'),
- 'picked' => Gitlab::GitalyClient::StorageSettings.new('path' => 'tmp/tests/repositories')
- }
- allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
- end
-
- it 'picks storage from ApplicationSetting' do
- expect(Gitlab::CurrentSettings).to receive(:pick_repository_storage).and_return('picked')
-
- expect(subject).to eq('picked')
- end
-
- it 'picks from the available storages based on weight', :request_store do
- stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
- Gitlab::CurrentSettings.expire_current_application_settings
- Gitlab::CurrentSettings.current_application_settings
-
- settings = ApplicationSetting.last
- settings.repository_storages_weighted = { 'picked' => 100, 'default' => 0 }
- settings.save!
-
- expect(Gitlab::CurrentSettings.repository_storages_weighted).to eq({ 'default' => 100 })
- expect(subject).to eq('picked')
- expect(Gitlab::CurrentSettings.repository_storages_weighted).to eq({ 'default' => 0, 'picked' => 100 })
- end
- end
end
diff --git a/spec/support/shared_examples/models/concerns/repositories/can_housekeep_repository_shared_examples.rb b/spec/support/shared_examples/models/concerns/repositories/can_housekeep_repository_shared_examples.rb
index 2f0b95427d2..4006b8226ce 100644
--- a/spec/support/shared_examples/models/concerns/repositories/can_housekeep_repository_shared_examples.rb
+++ b/spec/support/shared_examples/models/concerns/repositories/can_housekeep_repository_shared_examples.rb
@@ -41,5 +41,11 @@ RSpec.shared_examples 'can housekeep repository' do
expect(resource.send(:pushes_since_gc_redis_shared_state_key)).to eq("#{resource_key}/#{resource.id}/pushes_since_gc")
end
end
+
+ describe '#git_garbage_collect_worker_klass' do
+ it 'defines a git gargabe collect worker' do
+ expect(resource.git_garbage_collect_worker_klass).to eq(expected_worker_class)
+ end
+ end
end
end
diff --git a/spec/support/shared_examples/models/concerns/repository_storage_movable_shared_examples.rb b/spec/support/shared_examples/models/concerns/repository_storage_movable_shared_examples.rb
index 4c617f3ba46..819cf6018fe 100644
--- a/spec/support/shared_examples/models/concerns/repository_storage_movable_shared_examples.rb
+++ b/spec/support/shared_examples/models/concerns/repository_storage_movable_shared_examples.rb
@@ -33,7 +33,7 @@ RSpec.shared_examples 'handles repository moves' do
subject { build(repository_storage_factory_key, container: container) }
it "does not allow the container to be read-only on create" do
- container.update!(repository_read_only: true)
+ container.set_repository_read_only!
expect(subject).not_to be_valid
expect(subject.errors[error_key].first).to match(/is read only/)
@@ -45,8 +45,8 @@ RSpec.shared_examples 'handles repository moves' do
context 'destination_storage_name' do
subject { build(repository_storage_factory_key) }
- it 'picks storage from ApplicationSetting' do
- expect(Gitlab::CurrentSettings).to receive(:pick_repository_storage).and_return('picked').at_least(:once)
+ it 'can pick new storage' do
+ expect(Repository).to receive(:pick_storage_shard).and_return('picked').at_least(:once)
expect(subject.destination_storage_name).to eq('picked')
end
@@ -99,6 +99,11 @@ RSpec.shared_examples 'handles repository moves' do
expect(container).not_to be_repository_read_only
end
+
+ it 'updates the updated_at column of the container', :aggregate_failures do
+ expect { storage_move.finish_replication! }.to change { container.updated_at }
+ expect(storage_move.container.updated_at).to be >= storage_move.updated_at
+ end
end
context 'and transits to failed' do
diff --git a/spec/support/shared_examples/models/packages/debian/architecture_shared_examples.rb b/spec/support/shared_examples/models/packages/debian/architecture_shared_examples.rb
index 38983f752f4..b73ff516670 100644
--- a/spec/support/shared_examples/models/packages/debian/architecture_shared_examples.rb
+++ b/spec/support/shared_examples/models/packages/debian/architecture_shared_examples.rb
@@ -11,6 +11,7 @@ RSpec.shared_examples 'Debian Distribution Architecture' do |factory, container,
describe 'relationships' do
it { is_expected.to belong_to(:distribution).class_name("Packages::Debian::#{container.capitalize}Distribution").inverse_of(:architectures) }
+ it { is_expected.to have_many(:files).class_name("Packages::Debian::#{container.capitalize}ComponentFile").inverse_of(:architecture) }
end
describe 'validations' do
diff --git a/spec/support/shared_examples/models/packages/debian/component_file_shared_example.rb b/spec/support/shared_examples/models/packages/debian/component_file_shared_example.rb
new file mode 100644
index 00000000000..02ced49ee94
--- /dev/null
+++ b/spec/support/shared_examples/models/packages/debian/component_file_shared_example.rb
@@ -0,0 +1,245 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.shared_examples 'Debian Component File' do |container_type, can_freeze|
+ let_it_be(:container1, freeze: can_freeze) { create(container_type) } # rubocop:disable Rails/SaveBang
+ let_it_be(:container2, freeze: can_freeze) { create(container_type) } # rubocop:disable Rails/SaveBang
+ let_it_be(:distribution1, freeze: can_freeze) { create("debian_#{container_type}_distribution", container: container1) }
+ let_it_be(:distribution2, freeze: can_freeze) { create("debian_#{container_type}_distribution", container: container2) }
+ let_it_be(:architecture1_1, freeze: can_freeze) { create("debian_#{container_type}_architecture", distribution: distribution1) }
+ let_it_be(:architecture1_2, freeze: can_freeze) { create("debian_#{container_type}_architecture", distribution: distribution1) }
+ let_it_be(:architecture2_1, freeze: can_freeze) { create("debian_#{container_type}_architecture", distribution: distribution2) }
+ let_it_be(:architecture2_2, freeze: can_freeze) { create("debian_#{container_type}_architecture", distribution: distribution2) }
+ let_it_be(:component1_1, freeze: can_freeze) { create("debian_#{container_type}_component", distribution: distribution1) }
+ let_it_be(:component1_2, freeze: can_freeze) { create("debian_#{container_type}_component", distribution: distribution1) }
+ let_it_be(:component2_1, freeze: can_freeze) { create("debian_#{container_type}_component", distribution: distribution2) }
+ let_it_be(:component2_2, freeze: can_freeze) { create("debian_#{container_type}_component", distribution: distribution2) }
+
+ let_it_be_with_refind(:component_file_with_architecture) { create("debian_#{container_type}_component_file", component: component1_1, architecture: architecture1_1) }
+ let_it_be(:component_file_other_architecture, freeze: can_freeze) { create("debian_#{container_type}_component_file", component: component1_1, architecture: architecture1_2) }
+ let_it_be(:component_file_other_component, freeze: can_freeze) { create("debian_#{container_type}_component_file", component: component1_2, architecture: architecture1_1) }
+ let_it_be(:component_file_other_compression_type, freeze: can_freeze) { create("debian_#{container_type}_component_file", component: component1_1, architecture: architecture1_1, compression_type: :xz) }
+ let_it_be(:component_file_other_file_md5, freeze: can_freeze) { create("debian_#{container_type}_component_file", component: component1_1, architecture: architecture1_1, file_md5: 'other_md5') }
+ let_it_be(:component_file_other_file_sha256, freeze: can_freeze) { create("debian_#{container_type}_component_file", component: component1_1, architecture: architecture1_1, file_sha256: 'other_sha256') }
+ let_it_be(:component_file_other_container, freeze: can_freeze) { create("debian_#{container_type}_component_file", component: component2_1, architecture: architecture2_1) }
+ let_it_be_with_refind(:component_file_with_file_type_source) { create("debian_#{container_type}_component_file", :source, component: component1_1) }
+ let_it_be(:component_file_with_file_type_di_packages, freeze: can_freeze) { create("debian_#{container_type}_component_file", :di_packages, component: component1_1, architecture: architecture1_1) }
+
+ subject { component_file_with_architecture }
+
+ describe 'relationships' do
+ context 'with stubbed uploader' do
+ before do
+ allow_next_instance_of(Packages::Debian::ComponentFileUploader) do |uploader|
+ allow(uploader).to receive(:dynamic_segment).and_return('stubbed')
+ end
+ end
+
+ it { is_expected.to belong_to(:component).class_name("Packages::Debian::#{container_type.capitalize}Component").inverse_of(:files) }
+ end
+
+ context 'with packages file_type' do
+ it { is_expected.to belong_to(:architecture).class_name("Packages::Debian::#{container_type.capitalize}Architecture").inverse_of(:files) }
+ end
+
+ context 'with :source file_type' do
+ subject { component_file_with_file_type_source }
+
+ it { is_expected.to belong_to(:architecture).class_name("Packages::Debian::#{container_type.capitalize}Architecture").inverse_of(:files).optional }
+ end
+ end
+
+ describe 'validations' do
+ describe "#component" do
+ before do
+ allow_next_instance_of(Packages::Debian::ComponentFileUploader) do |uploader|
+ allow(uploader).to receive(:dynamic_segment).and_return('stubbed')
+ end
+ end
+
+ it { is_expected.to validate_presence_of(:component) }
+ end
+
+ describe "#architecture" do
+ context 'with packages file_type' do
+ it { is_expected.to validate_presence_of(:architecture) }
+ end
+
+ context 'with :source file_type' do
+ subject { component_file_with_file_type_source }
+
+ it { is_expected.to validate_absence_of(:architecture) }
+ end
+ end
+
+ describe '#file_type' do
+ it { is_expected.to validate_presence_of(:file_type) }
+
+ it { is_expected.to allow_value(:packages).for(:file_type) }
+ end
+
+ describe '#compression_type' do
+ it { is_expected.not_to validate_presence_of(:compression_type) }
+
+ it { is_expected.to allow_value(nil).for(:compression_type) }
+ it { is_expected.to allow_value(:gz).for(:compression_type) }
+ end
+
+ describe '#file' do
+ subject { component_file_with_architecture.file }
+
+ context 'the uploader api' do
+ it { is_expected.to respond_to(:store_dir) }
+ it { is_expected.to respond_to(:cache_dir) }
+ it { is_expected.to respond_to(:work_dir) }
+ end
+ end
+
+ describe '#file_store' do
+ it { is_expected.to validate_presence_of(:file_store) }
+ end
+
+ describe '#file_md5' do
+ it { is_expected.to validate_presence_of(:file_md5) }
+ end
+
+ describe '#file_sha256' do
+ it { is_expected.to validate_presence_of(:file_sha256) }
+ end
+ end
+
+ describe 'scopes' do
+ describe '.with_container' do
+ subject { described_class.with_container(container2) }
+
+ it do
+ queries = ActiveRecord::QueryRecorder.new do
+ expect(subject.to_a).to contain_exactly(component_file_other_container)
+ end
+
+ expect(queries.count).to eq(1)
+ end
+ end
+
+ describe '.with_codename_or_suite' do
+ subject { described_class.with_codename_or_suite(distribution2.codename) }
+
+ it do
+ queries = ActiveRecord::QueryRecorder.new do
+ expect(subject.to_a).to contain_exactly(component_file_other_container)
+ end
+
+ expect(queries.count).to eq(1)
+ end
+ end
+
+ describe '.with_component_name' do
+ subject { described_class.with_component_name(component1_2.name) }
+
+ it do
+ queries = ActiveRecord::QueryRecorder.new do
+ expect(subject.to_a).to contain_exactly(component_file_other_component)
+ end
+
+ expect(queries.count).to eq(1)
+ end
+ end
+
+ describe '.with_file_type' do
+ subject { described_class.with_file_type(:source) }
+
+ it do
+ # let_it_be_with_refind triggers a query
+ component_file_with_file_type_source
+
+ queries = ActiveRecord::QueryRecorder.new do
+ expect(subject.to_a).to contain_exactly(component_file_with_file_type_source)
+ end
+
+ expect(queries.count).to eq(1)
+ end
+ end
+
+ describe '.with_architecture_name' do
+ subject { described_class.with_architecture_name(architecture1_2.name) }
+
+ it do
+ queries = ActiveRecord::QueryRecorder.new do
+ expect(subject.to_a).to contain_exactly(component_file_other_architecture)
+ end
+
+ expect(queries.count).to eq(1)
+ end
+ end
+
+ describe '.with_compression_type' do
+ subject { described_class.with_compression_type(:xz) }
+
+ it do
+ queries = ActiveRecord::QueryRecorder.new do
+ expect(subject.to_a).to contain_exactly(component_file_other_compression_type)
+ end
+
+ expect(queries.count).to eq(1)
+ end
+ end
+
+ describe '.with_file_sha256' do
+ subject { described_class.with_file_sha256('other_sha256') }
+
+ it do
+ queries = ActiveRecord::QueryRecorder.new do
+ expect(subject.to_a).to contain_exactly(component_file_other_file_sha256)
+ end
+
+ expect(queries.count).to eq(1)
+ end
+ end
+ end
+
+ describe 'callbacks' do
+ let(:component_file) { build("debian_#{container_type}_component_file", component: component1_1, architecture: architecture1_1, size: nil) }
+
+ subject { component_file.save! }
+
+ it 'updates metadata columns' do
+ expect(component_file)
+ .to receive(:update_file_store)
+ .and_call_original
+
+ expect(component_file)
+ .to receive(:update_column)
+ .with(:file_store, ::Packages::PackageFileUploader::Store::LOCAL)
+ .and_call_original
+
+ expect { subject }.to change { component_file.size }.from(nil).to(74)
+ end
+ end
+
+ describe '#relative_path' do
+ context 'with a Packages file_type' do
+ subject { component_file_with_architecture.relative_path }
+
+ it { is_expected.to eq("#{component1_1.name}/binary-#{architecture1_1.name}/Packages") }
+ end
+
+ context 'with a Source file_type' do
+ subject { component_file_with_file_type_source.relative_path }
+
+ it { is_expected.to eq("#{component1_1.name}/source/Source") }
+ end
+
+ context 'with a DI Packages file_type' do
+ subject { component_file_with_file_type_di_packages.relative_path }
+
+ it { is_expected.to eq("#{component1_1.name}/debian-installer/binary-#{architecture1_1.name}/Packages") }
+ end
+
+ context 'with an xz compression_type' do
+ subject { component_file_other_compression_type.relative_path }
+
+ it { is_expected.to eq("#{component1_1.name}/binary-#{architecture1_1.name}/Packages.xz") }
+ end
+ end
+end
diff --git a/spec/support/shared_examples/models/packages/debian/component_shared_examples.rb b/spec/support/shared_examples/models/packages/debian/component_shared_examples.rb
new file mode 100644
index 00000000000..bf6fc23116c
--- /dev/null
+++ b/spec/support/shared_examples/models/packages/debian/component_shared_examples.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.shared_examples 'Debian Distribution Component' do |factory, container, can_freeze|
+ let_it_be_with_refind(:component) { create(factory) } # rubocop:disable Rails/SaveBang
+ let_it_be(:component_same_distribution, freeze: can_freeze) { create(factory, distribution: component.distribution) }
+ let_it_be(:component_same_name, freeze: can_freeze) { create(factory, name: component.name) }
+
+ subject { component }
+
+ describe 'relationships' do
+ it { is_expected.to belong_to(:distribution).class_name("Packages::Debian::#{container.capitalize}Distribution").inverse_of(:components) }
+ it { is_expected.to have_many(:files).class_name("Packages::Debian::#{container.capitalize}ComponentFile").inverse_of(:component) }
+ end
+
+ describe 'validations' do
+ describe "#distribution" do
+ it { is_expected.to validate_presence_of(:distribution) }
+ end
+
+ describe '#name' do
+ it { is_expected.to validate_presence_of(:name) }
+
+ it { is_expected.to allow_value('main').for(:name) }
+ it { is_expected.to allow_value('non-free').for(:name) }
+ it { is_expected.to allow_value('a' * 255).for(:name) }
+ it { is_expected.not_to allow_value('a' * 256).for(:name) }
+ it { is_expected.not_to allow_value('non/free').for(:name) }
+ it { is_expected.not_to allow_value('hé').for(:name) }
+ end
+ end
+
+ describe 'scopes' do
+ describe '.with_distribution' do
+ subject { described_class.with_distribution(component.distribution) }
+
+ it 'does not return other distributions' do
+ expect(subject.to_a).to eq([component, component_same_distribution])
+ end
+ end
+
+ describe '.with_name' do
+ subject { described_class.with_name(component.name) }
+
+ it 'does not return other distributions' do
+ expect(subject.to_a).to eq([component, component_same_name])
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/models/packages/debian/distribution_shared_examples.rb b/spec/support/shared_examples/models/packages/debian/distribution_shared_examples.rb
index af87d30099f..b4ec146df14 100644
--- a/spec/support/shared_examples/models/packages/debian/distribution_shared_examples.rb
+++ b/spec/support/shared_examples/models/packages/debian/distribution_shared_examples.rb
@@ -17,7 +17,13 @@ RSpec.shared_examples 'Debian Distribution' do |factory, container, can_freeze|
it { is_expected.to belong_to(container) }
it { is_expected.to belong_to(:creator).class_name('User') }
+ it { is_expected.to have_many(:components).class_name("Packages::Debian::#{container.capitalize}Component").inverse_of(:distribution) }
it { is_expected.to have_many(:architectures).class_name("Packages::Debian::#{container.capitalize}Architecture").inverse_of(:distribution) }
+
+ if container != :group
+ it { is_expected.to have_many(:publications).class_name('Packages::Debian::Publication').inverse_of(:distribution).with_foreign_key(:distribution_id) }
+ it { is_expected.to have_many(:packages).class_name('Packages::Package').through(:publications) }
+ end
end
describe 'validations' do
diff --git a/spec/support/shared_examples/models/wiki_shared_examples.rb b/spec/support/shared_examples/models/wiki_shared_examples.rb
index 62da9e15259..89d30688b5c 100644
--- a/spec/support/shared_examples/models/wiki_shared_examples.rb
+++ b/spec/support/shared_examples/models/wiki_shared_examples.rb
@@ -154,6 +154,15 @@ RSpec.shared_examples 'wiki model' do
it 'returns true' do
expect(subject.empty?).to be(true)
end
+
+ context 'when the repository does not exist' do
+ let(:wiki_container) { wiki_container_without_repo }
+
+ it 'returns true and does not create the repo' do
+ expect(subject.empty?).to be(true)
+ expect(wiki.repository_exists?).to be false
+ end
+ end
end
context 'when the wiki has pages' do
diff --git a/spec/support/shared_examples/models/with_debian_distributions_shared_examples.rb b/spec/support/shared_examples/models/with_debian_distributions_shared_examples.rb
new file mode 100644
index 00000000000..e86f1e77447
--- /dev/null
+++ b/spec/support/shared_examples/models/with_debian_distributions_shared_examples.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'model with Debian distributions' do
+ let(:container_type) { subject.class.name.downcase }
+ let!(:distributions) { create_list("debian_#{container_type}_distribution", 2, :with_file, container: subject) }
+ let!(:components) { create_list("debian_#{container_type}_component", 5, distribution: distributions[0]) }
+ let!(:component_files) { create_list("debian_#{container_type}_component_file", 3, component: components[0]) }
+
+ it 'removes distribution files on removal' do
+ distribution_file_paths = distributions.map do |distribution|
+ [distribution.file.path] +
+ distribution.component_files.map do |component_file|
+ component_file.file.path
+ end
+ end.flatten
+
+ expect { subject.destroy! }
+ .to change {
+ distribution_file_paths.select do |path|
+ File.exist? path
+ end.length
+ }.from(distribution_file_paths.length).to(0)
+ end
+end
diff --git a/spec/support/shared_examples/namespaces/recursive_traversal_examples.rb b/spec/support/shared_examples/namespaces/recursive_traversal_examples.rb
new file mode 100644
index 00000000000..2c94be61bc1
--- /dev/null
+++ b/spec/support/shared_examples/namespaces/recursive_traversal_examples.rb
@@ -0,0 +1,78 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'recursive namespace traversal' do
+ describe '#self_and_hierarchy' do
+ let!(:group) { create(:group, path: 'git_lab') }
+ let!(:nested_group) { create(:group, parent: group) }
+ let!(:deep_nested_group) { create(:group, parent: nested_group) }
+ let!(:very_deep_nested_group) { create(:group, parent: deep_nested_group) }
+ let!(:another_group) { create(:group, path: 'gitllab') }
+ let!(:another_group_nested) { create(:group, path: 'foo', parent: another_group) }
+
+ it 'returns the correct tree' do
+ expect(group.self_and_hierarchy).to contain_exactly(group, nested_group, deep_nested_group, very_deep_nested_group)
+ expect(nested_group.self_and_hierarchy).to contain_exactly(group, nested_group, deep_nested_group, very_deep_nested_group)
+ expect(very_deep_nested_group.self_and_hierarchy).to contain_exactly(group, nested_group, deep_nested_group, very_deep_nested_group)
+ end
+ end
+
+ describe '#ancestors' do
+ let(:group) { create(:group) }
+ let(:nested_group) { create(:group, parent: group) }
+ let(:deep_nested_group) { create(:group, parent: nested_group) }
+ let(:very_deep_nested_group) { create(:group, parent: deep_nested_group) }
+
+ it 'returns the correct ancestors' do
+ expect(very_deep_nested_group.ancestors).to include(group, nested_group, deep_nested_group)
+ expect(deep_nested_group.ancestors).to include(group, nested_group)
+ expect(nested_group.ancestors).to include(group)
+ expect(group.ancestors).to eq([])
+ end
+ end
+
+ describe '#self_and_ancestors' do
+ let(:group) { create(:group) }
+ let(:nested_group) { create(:group, parent: group) }
+ let(:deep_nested_group) { create(:group, parent: nested_group) }
+ let(:very_deep_nested_group) { create(:group, parent: deep_nested_group) }
+
+ it 'returns the correct ancestors' do
+ expect(very_deep_nested_group.self_and_ancestors).to contain_exactly(group, nested_group, deep_nested_group, very_deep_nested_group)
+ expect(deep_nested_group.self_and_ancestors).to contain_exactly(group, nested_group, deep_nested_group)
+ expect(nested_group.self_and_ancestors).to contain_exactly(group, nested_group)
+ expect(group.self_and_ancestors).to contain_exactly(group)
+ end
+ end
+
+ describe '#descendants' do
+ let!(:group) { create(:group, path: 'git_lab') }
+ let!(:nested_group) { create(:group, parent: group) }
+ let!(:deep_nested_group) { create(:group, parent: nested_group) }
+ let!(:very_deep_nested_group) { create(:group, parent: deep_nested_group) }
+ let!(:another_group) { create(:group, path: 'gitllab') }
+ let!(:another_group_nested) { create(:group, path: 'foo', parent: another_group) }
+
+ it 'returns the correct descendants' do
+ expect(very_deep_nested_group.descendants.to_a).to eq([])
+ expect(deep_nested_group.descendants.to_a).to include(very_deep_nested_group)
+ expect(nested_group.descendants.to_a).to include(deep_nested_group, very_deep_nested_group)
+ expect(group.descendants.to_a).to include(nested_group, deep_nested_group, very_deep_nested_group)
+ end
+ end
+
+ describe '#self_and_descendants' do
+ let!(:group) { create(:group, path: 'git_lab') }
+ let!(:nested_group) { create(:group, parent: group) }
+ let!(:deep_nested_group) { create(:group, parent: nested_group) }
+ let!(:very_deep_nested_group) { create(:group, parent: deep_nested_group) }
+ let!(:another_group) { create(:group, path: 'gitllab') }
+ let!(:another_group_nested) { create(:group, path: 'foo', parent: another_group) }
+
+ it 'returns the correct descendants' do
+ expect(very_deep_nested_group.self_and_descendants).to contain_exactly(very_deep_nested_group)
+ expect(deep_nested_group.self_and_descendants).to contain_exactly(deep_nested_group, very_deep_nested_group)
+ expect(nested_group.self_and_descendants).to contain_exactly(nested_group, deep_nested_group, very_deep_nested_group)
+ expect(group.self_and_descendants).to contain_exactly(group, nested_group, deep_nested_group, very_deep_nested_group)
+ end
+ end
+end
diff --git a/spec/support/shared_examples/quick_actions/issuable/close_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/close_quick_action_shared_examples.rb
index e2582f20ece..4fde68efd60 100644
--- a/spec/support/shared_examples/quick_actions/issuable/close_quick_action_shared_examples.rb
+++ b/spec/support/shared_examples/quick_actions/issuable/close_quick_action_shared_examples.rb
@@ -44,7 +44,6 @@ RSpec.shared_examples 'close quick action' do |issuable_type|
it 'creates the note and interprets the close quick action accordingly' do
add_note("this is done, close\n\n/close")
- wait_for_requests
expect(page).not_to have_content '/close'
expect(page).to have_content 'this is done, close'
diff --git a/spec/support/shared_examples/quick_actions/issue/clone_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issue/clone_quick_action_shared_examples.rb
index a99304f7214..ab04692616a 100644
--- a/spec/support/shared_examples/quick_actions/issue/clone_quick_action_shared_examples.rb
+++ b/spec/support/shared_examples/quick_actions/issue/clone_quick_action_shared_examples.rb
@@ -54,8 +54,6 @@ RSpec.shared_examples 'clone quick action' do
# Note that this is missing one `-`
add_note("/clone -with_notes #{target_project.full_path}")
- wait_for_requests
-
expect(page).to have_content 'Failed to clone this issue: wrong parameters.'
expect(issue.reload).to be_open
end
@@ -68,8 +66,6 @@ RSpec.shared_examples 'clone quick action' do
it 'does not clone the issue' do
add_note("/clone #{project_unauthorized.full_path}")
- wait_for_requests
-
expect(page).to have_content "Cloned this issue to #{project_unauthorized.full_path}."
expect(issue.reload).to be_open
@@ -83,8 +79,6 @@ RSpec.shared_examples 'clone quick action' do
it 'does not clone the issue' do
add_note("/clone not/valid")
- wait_for_requests
-
expect(page).to have_content "Failed to clone this issue because target project doesn't exist."
expect(issue.reload).to be_open
end
@@ -154,7 +148,6 @@ RSpec.shared_examples 'clone quick action' do
expect(issue.reload).not_to be_closed
edit_note("/cloe #{target_project.full_path}", "test note.\n/clone #{target_project.full_path}")
- wait_for_all_requests
expect(page).to have_content 'test note.'
expect(issue.reload).to be_open
@@ -172,7 +165,6 @@ RSpec.shared_examples 'clone quick action' do
expect(page).not_to have_content 'Commands applied'
edit_note("/cloe #{target_project.full_path}", "/clone #{target_project.full_path}")
- wait_for_all_requests
expect(page).not_to have_content "/clone #{target_project.full_path}"
expect(issue.reload).to be_open
diff --git a/spec/support/shared_examples/quick_actions/issue/create_merge_request_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issue/create_merge_request_quick_action_shared_examples.rb
index 910805dbdea..9dc39c6cf73 100644
--- a/spec/support/shared_examples/quick_actions/issue/create_merge_request_quick_action_shared_examples.rb
+++ b/spec/support/shared_examples/quick_actions/issue/create_merge_request_quick_action_shared_examples.rb
@@ -22,8 +22,6 @@ RSpec.shared_examples 'create_merge_request quick action' do
branch_name = 'invalid branch name'
add_note("/create_merge_request #{branch_name}")
- wait_for_requests
-
expect_mr_quickaction(false, branch_name)
end
@@ -31,16 +29,12 @@ RSpec.shared_examples 'create_merge_request quick action' do
branch_name = 'feature'
add_note("/create_merge_request #{branch_name}")
- wait_for_requests
-
expect_mr_quickaction(false, branch_name)
end
it 'creates a new merge request using issue iid and title as branch name when the branch name is empty' do
add_note("/create_merge_request")
- wait_for_requests
-
expect_mr_quickaction(true)
created_mr = project.merge_requests.last
diff --git a/spec/support/shared_examples/quick_actions/issue/move_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issue/move_quick_action_shared_examples.rb
index 32c46753006..5892fc32e94 100644
--- a/spec/support/shared_examples/quick_actions/issue/move_quick_action_shared_examples.rb
+++ b/spec/support/shared_examples/quick_actions/issue/move_quick_action_shared_examples.rb
@@ -27,8 +27,6 @@ RSpec.shared_examples 'move quick action' do
it 'does not move the issue' do
add_note("/move #{project_unauthorized.full_path}")
- wait_for_requests
-
expect(page).to have_content "Moved this issue to #{project_unauthorized.full_path}."
expect(issue.reload).to be_open
end
@@ -38,8 +36,6 @@ RSpec.shared_examples 'move quick action' do
it 'does not move the issue' do
add_note("/move not/valid")
- wait_for_requests
-
expect(page).to have_content "Failed to move this issue because target project doesn't exist."
expect(issue.reload).to be_open
end
@@ -110,7 +106,6 @@ RSpec.shared_examples 'move quick action' do
expect(issue.reload).not_to be_closed
edit_note("/mvoe #{target_project.full_path}", "test note.\n/move #{target_project.full_path}")
- wait_for_all_requests
expect(page).to have_content 'test note.'
expect(issue.reload).to be_closed
@@ -129,7 +124,6 @@ RSpec.shared_examples 'move quick action' do
expect(issue.reload).not_to be_closed
edit_note("/mvoe #{target_project.full_path}", "/move #{target_project.full_path}")
- wait_for_all_requests
expect(page).not_to have_content "/move #{target_project.full_path}"
expect(issue.reload).to be_closed
diff --git a/spec/support/shared_examples/quick_actions/issue/zoom_quick_actions_shared_examples.rb b/spec/support/shared_examples/quick_actions/issue/zoom_quick_actions_shared_examples.rb
index 1ea249d5f9d..34937949174 100644
--- a/spec/support/shared_examples/quick_actions/issue/zoom_quick_actions_shared_examples.rb
+++ b/spec/support/shared_examples/quick_actions/issue/zoom_quick_actions_shared_examples.rb
@@ -10,8 +10,6 @@ RSpec.shared_examples 'zoom quick actions' do
it 'skips addition silently' do
add_note("/zoom #{zoom_link}")
- wait_for_requests
-
expect(page).not_to have_content('Zoom meeting added')
expect(page).not_to have_content('Failed to add a Zoom meeting')
expect(ZoomMeeting.canonical_meeting_url(issue.reload)).not_to eq(zoom_link)
@@ -22,8 +20,6 @@ RSpec.shared_examples 'zoom quick actions' do
it 'adds a Zoom link' do
add_note("/zoom #{zoom_link}")
- wait_for_requests
-
expect(page).to have_content('Zoom meeting added')
expect(ZoomMeeting.canonical_meeting_url(issue.reload)).to eq(zoom_link)
end
@@ -35,8 +31,6 @@ RSpec.shared_examples 'zoom quick actions' do
it 'cannot add invalid zoom link' do
add_note("/zoom #{invalid_zoom_link}")
- wait_for_requests
-
expect(page).to have_content('Failed to add a Zoom meeting')
expect(page).not_to have_content(zoom_link)
end
@@ -64,8 +58,6 @@ RSpec.shared_examples 'zoom quick actions' do
it 'skips removal silently' do
add_note('/remove_zoom')
- wait_for_requests
-
expect(page).not_to have_content('Zoom meeting removed')
expect(page).not_to have_content('Failed to remove a Zoom meeting')
expect(ZoomMeeting.canonical_meeting_url(issue.reload)).to be_nil
@@ -78,8 +70,6 @@ RSpec.shared_examples 'zoom quick actions' do
it 'removes last Zoom link' do
add_note('/remove_zoom')
- wait_for_requests
-
expect(page).to have_content('Zoom meeting removed')
expect(ZoomMeeting.canonical_meeting_url(issue.reload)).to be_nil
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 83ba72c12aa..acaa0d8c2bc 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
@@ -1,6 +1,8 @@
# frozen_string_literal: true
RSpec.shared_context 'Debian repository shared context' do |object_type|
+ include_context 'workhorse headers'
+
before do
stub_feature_flags(debian_packages: true)
end
@@ -37,16 +39,15 @@ RSpec.shared_context 'Debian repository shared context' do |object_type|
let(:params) { workhorse_params }
let(:auth_headers) { {} }
- let(:workhorse_token) { JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256') }
- let(:workhorse_headers) do
+ let(:wh_headers) do
if method == :put
- { 'GitLab-Workhorse' => '1.0', Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER => workhorse_token }
+ workhorse_headers
else
{}
end
end
- let(:headers) { auth_headers.merge(workhorse_headers) }
+ let(:headers) { auth_headers.merge(wh_headers) }
let(:send_rewritten_field) { true }
@@ -201,7 +202,7 @@ RSpec.shared_examples 'rejects Debian access with unknown project id' do
let(:project) { double(id: non_existing_record_id) }
context 'as anonymous' do
- it_behaves_like 'Debian project repository GET request', :anonymous, true, :not_found, nil
+ it_behaves_like 'Debian project repository GET request', :anonymous, true, :unauthorized, nil
end
context 'as authenticated user' do
@@ -228,13 +229,13 @@ RSpec.shared_examples 'Debian project repository GET endpoint' do |success_statu
'PUBLIC' | :anonymous | false | true | success_status | success_body
'PRIVATE' | :developer | true | true | success_status | success_body
'PRIVATE' | :guest | true | true | :forbidden | nil
- 'PRIVATE' | :developer | true | false | :not_found | nil
- 'PRIVATE' | :guest | true | false | :not_found | nil
+ 'PRIVATE' | :developer | true | false | :unauthorized | nil
+ 'PRIVATE' | :guest | true | false | :unauthorized | nil
'PRIVATE' | :developer | false | true | :not_found | nil
'PRIVATE' | :guest | false | true | :not_found | nil
- 'PRIVATE' | :developer | false | false | :not_found | nil
- 'PRIVATE' | :guest | false | false | :not_found | nil
- 'PRIVATE' | :anonymous | false | true | :not_found | nil
+ 'PRIVATE' | :developer | false | false | :unauthorized | nil
+ 'PRIVATE' | :guest | false | false | :unauthorized | nil
+ 'PRIVATE' | :anonymous | false | true | :unauthorized | nil
end
with_them do
@@ -263,13 +264,13 @@ RSpec.shared_examples 'Debian project repository PUT endpoint' do |success_statu
'PUBLIC' | :anonymous | false | true | :unauthorized | nil
'PRIVATE' | :developer | true | true | success_status | nil
'PRIVATE' | :guest | true | true | :forbidden | nil
- 'PRIVATE' | :developer | true | false | :not_found | nil
- 'PRIVATE' | :guest | true | false | :not_found | nil
+ 'PRIVATE' | :developer | true | false | :unauthorized | nil
+ 'PRIVATE' | :guest | true | false | :unauthorized | nil
'PRIVATE' | :developer | false | true | :not_found | nil
'PRIVATE' | :guest | false | true | :not_found | nil
- 'PRIVATE' | :developer | false | false | :not_found | nil
- 'PRIVATE' | :guest | false | false | :not_found | nil
- 'PRIVATE' | :anonymous | false | true | :not_found | nil
+ 'PRIVATE' | :developer | false | false | :unauthorized | nil
+ 'PRIVATE' | :guest | false | false | :unauthorized | nil
+ 'PRIVATE' | :anonymous | false | true | :unauthorized | nil
end
with_them do
@@ -321,7 +322,7 @@ RSpec.shared_examples 'rejects Debian access with unknown group id' do
let(:group) { double(id: non_existing_record_id) }
context 'as anonymous' do
- it_behaves_like 'Debian group repository GET request', :anonymous, true, :not_found, nil
+ it_behaves_like 'Debian group repository GET request', :anonymous, true, :unauthorized, nil
end
context 'as authenticated user' do
@@ -348,13 +349,13 @@ RSpec.shared_examples 'Debian group repository GET endpoint' do |success_status,
'PUBLIC' | :anonymous | false | true | success_status | success_body
'PRIVATE' | :developer | true | true | success_status | success_body
'PRIVATE' | :guest | true | true | :forbidden | nil
- 'PRIVATE' | :developer | true | false | :not_found | nil
- 'PRIVATE' | :guest | true | false | :not_found | nil
+ 'PRIVATE' | :developer | true | false | :unauthorized | nil
+ 'PRIVATE' | :guest | true | false | :unauthorized | nil
'PRIVATE' | :developer | false | true | :not_found | nil
'PRIVATE' | :guest | false | true | :not_found | nil
- 'PRIVATE' | :developer | false | false | :not_found | nil
- 'PRIVATE' | :guest | false | false | :not_found | nil
- 'PRIVATE' | :anonymous | false | true | :not_found | nil
+ 'PRIVATE' | :developer | false | false | :unauthorized | nil
+ 'PRIVATE' | :guest | false | false | :unauthorized | nil
+ 'PRIVATE' | :anonymous | false | true | :unauthorized | nil
end
with_them do
diff --git a/spec/support/shared_examples/requests/api/graphql/mutations/create_list_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/mutations/create_list_shared_examples.rb
new file mode 100644
index 00000000000..fe2cdbe3182
--- /dev/null
+++ b/spec/support/shared_examples/requests/api/graphql/mutations/create_list_shared_examples.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.shared_examples 'board lists create request' do
+ include GraphqlHelpers
+
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:dev_label) do
+ create(:group_label, title: 'Development', color: '#FFAABB', group: group)
+ end
+
+ let(:mutation) { graphql_mutation(mutation_name, input) }
+ let(:mutation_response) { graphql_mutation_response(mutation_name) }
+
+ context 'the user is not allowed to read board lists' do
+ let(:input) { { board_id: board.to_global_id.to_s, backlog: true } }
+
+ it_behaves_like 'a mutation that returns a top-level access error'
+ end
+
+ context 'when user has permissions to admin board lists' do
+ before do
+ group.add_reporter(current_user)
+ end
+
+ describe 'backlog list' do
+ let(:input) { { board_id: board.to_global_id.to_s, backlog: true } }
+
+ it 'creates the list' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_response['list'])
+ .to include('position' => nil, 'listType' => 'backlog')
+ end
+ end
+
+ describe 'label list' do
+ let(:input) { { board_id: board.to_global_id.to_s, label_id: dev_label.to_global_id.to_s } }
+
+ it 'creates the list' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_response['list'])
+ .to include('position' => 0, 'listType' => 'label', 'label' => include('title' => 'Development'))
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/requests/api/graphql/noteable_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/noteable_shared_examples.rb
new file mode 100644
index 00000000000..9cf5bc04f65
--- /dev/null
+++ b/spec/support/shared_examples/requests/api/graphql/noteable_shared_examples.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+# Requires `query(fields)`, `path_to_noteable`, `project`, and `noteable` bindings
+RSpec.shared_examples 'a noteable graphql type we can query' do
+ let(:note_factory) { :note }
+ let(:discussion_factory) { :discussion_note }
+
+ describe '.discussions' do
+ let(:fields) do
+ "discussions { nodes { #{all_graphql_fields_for('Discussion')} } }"
+ end
+
+ def expected
+ noteable.discussions.map do |discussion|
+ include(
+ 'id' => global_id_of(discussion),
+ 'replyId' => global_id_of(discussion, id: discussion.reply_id),
+ 'createdAt' => discussion.created_at.iso8601,
+ 'notes' => include(
+ 'nodes' => have_attributes(size: discussion.notes.size)
+ )
+ )
+ end
+ end
+
+ it 'can fetch discussions' do
+ create(discussion_factory, project: project, noteable: noteable)
+
+ post_graphql(query(fields), current_user: current_user)
+
+ expect(graphql_data_at(*path_to_noteable, :discussions, :nodes))
+ .to match_array(expected)
+ end
+ end
+
+ describe '.notes' do
+ let(:fields) do
+ "notes { nodes { #{all_graphql_fields_for('Note', max_depth: 2)} } }"
+ end
+
+ def expected
+ noteable.notes.map do |note|
+ include(
+ 'id' => global_id_of(note),
+ 'project' => include('id' => global_id_of(project)),
+ 'author' => include('id' => global_id_of(note.author)),
+ 'createdAt' => note.created_at.iso8601,
+ 'body' => eq(note.note)
+ )
+ end
+ end
+
+ it 'can fetch notes' do
+ create(note_factory, project: project, noteable: noteable)
+
+ post_graphql(query(fields), current_user: current_user)
+
+ expect(graphql_data_at(*path_to_noteable, :notes, :nodes))
+ .to match_array(expected)
+ end
+ end
+end
diff --git a/spec/support/shared_examples/requests/api/notes_shared_examples.rb b/spec/support/shared_examples/requests/api/notes_shared_examples.rb
index 7066f803f9d..40799688144 100644
--- a/spec/support/shared_examples/requests/api/notes_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/notes_shared_examples.rb
@@ -127,6 +127,12 @@ RSpec.shared_examples 'noteable API' do |parent_type, noteable_type, id_name|
end
describe "POST /#{parent_type}/:id/#{noteable_type}/:noteable_id/notes" do
+ let(:params) { { body: 'hi!' } }
+
+ subject do
+ post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", user), params: params
+ end
+
it "creates a new note" do
post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", user), params: { body: 'hi!' }
@@ -274,6 +280,29 @@ RSpec.shared_examples 'noteable API' do |parent_type, noteable_type, id_name|
expect(response).to have_gitlab_http_status(:not_found)
end
end
+
+ context 'when request exceeds the rate limit' do
+ before do
+ stub_application_setting(notes_create_limit: 1)
+ allow(::Gitlab::ApplicationRateLimiter).to receive(:increment).and_return(2)
+ end
+
+ it 'prevents user from creating more notes' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:too_many_requests)
+ expect(json_response['message']['error']).to eq('This endpoint has been requested too many times. Try again later.')
+ end
+
+ it 'allows user in allow-list to create notes' do
+ stub_application_setting(notes_create_limit_allowlist: ["#{user.username}"])
+ subject
+
+ expect(response).to have_gitlab_http_status(:created)
+ expect(json_response['body']).to eq('hi!')
+ expect(json_response['author']['username']).to eq(user.username)
+ end
+ end
end
describe "PUT /#{parent_type}/:id/#{noteable_type}/:noteable_id/notes/:note_id" do
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 d3ad7aa0595..be051dcbb7b 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
@@ -1,270 +1,430 @@
# frozen_string_literal: true
-RSpec.shared_examples 'handling get metadata requests' do
+RSpec.shared_examples 'handling get metadata requests' do |scope: :project|
+ using RSpec::Parameterized::TableSyntax
+
let_it_be(:package_dependency_link1) { create(:packages_dependency_link, package: package, dependency_type: :dependencies) }
let_it_be(:package_dependency_link2) { create(:packages_dependency_link, package: package, dependency_type: :devDependencies) }
let_it_be(:package_dependency_link3) { create(:packages_dependency_link, package: package, dependency_type: :bundleDependencies) }
let_it_be(:package_dependency_link4) { create(:packages_dependency_link, package: package, dependency_type: :peerDependencies) }
- let(:params) { {} }
let(:headers) { {} }
- subject { get(url, params: params, headers: headers) }
+ subject { get(url, headers: headers) }
- shared_examples 'returning the npm package info' do
- it 'returns the package info' do
+ shared_examples 'accept metadata request' do |status:|
+ it 'accepts the metadata request' do
subject
- expect_a_valid_package_response
+ expect(response).to have_gitlab_http_status(status)
+ expect(response.media_type).to eq('application/json')
+ expect(response).to match_response_schema('public_api/v4/packages/npm_package')
+ expect(json_response['name']).to eq(package.name)
+ expect(json_response['versions'][package.version]).to match_schema('public_api/v4/packages/npm_package_version')
+ ::Packages::Npm::PackagePresenter::NPM_VALID_DEPENDENCY_TYPES.each do |dependency_type|
+ expect(json_response.dig('versions', package.version, dependency_type.to_s)).to be_any
+ end
+ expect(json_response['dist-tags']).to match_schema('public_api/v4/packages/npm_package_tags')
end
end
- shared_examples 'a package that requires auth' do
- it 'denies request without oauth token' do
+ shared_examples 'reject metadata request' do |status:|
+ it 'rejects the metadata request' do
subject
- expect(response).to have_gitlab_http_status(:not_found)
+ expect(response).to have_gitlab_http_status(status)
end
+ end
- context 'with oauth token' do
- let(:params) { { access_token: token.token } }
-
- it 'returns the package info with oauth token' do
- subject
+ shared_examples 'redirect metadata request' do |status:|
+ it 'redirects metadata request' do
+ subject
- expect_a_valid_package_response
- end
+ expect(response).to have_gitlab_http_status(:found)
+ expect(response.headers['Location']).to eq("https://registry.npmjs.org/#{package_name}")
end
+ end
- context 'with job token' do
- let(:params) { { job_token: job.token } }
-
- it 'returns the package info with running job token' do
- subject
+ where(:auth, :package_name_type, :request_forward, :visibility, :user_role, :expected_result, :expected_status) do
+ nil | :scoped_naming_convention | true | 'PUBLIC' | nil | :accept | :ok
+ nil | :scoped_naming_convention | false | 'PUBLIC' | nil | :accept | :ok
+ nil | :non_existing | true | 'PUBLIC' | nil | :redirect | :redirected
+ nil | :non_existing | false | 'PUBLIC' | nil | :reject | :not_found
+ nil | :scoped_naming_convention | true | 'PRIVATE' | nil | :reject | :not_found
+ nil | :scoped_naming_convention | false | 'PRIVATE' | nil | :reject | :not_found
+ nil | :non_existing | true | 'PRIVATE' | nil | :redirect | :redirected
+ nil | :non_existing | false | 'PRIVATE' | nil | :reject | :not_found
+ nil | :scoped_naming_convention | true | 'INTERNAL' | nil | :reject | :not_found
+ nil | :scoped_naming_convention | false | 'INTERNAL' | nil | :reject | :not_found
+ nil | :non_existing | true | 'INTERNAL' | nil | :redirect | :redirected
+ nil | :non_existing | false | 'INTERNAL' | nil | :reject | :not_found
+
+ :oauth | :scoped_naming_convention | true | 'PUBLIC' | :guest | :accept | :ok
+ :oauth | :scoped_naming_convention | true | 'PUBLIC' | :reporter | :accept | :ok
+ :oauth | :scoped_naming_convention | false | 'PUBLIC' | :guest | :accept | :ok
+ :oauth | :scoped_naming_convention | false | 'PUBLIC' | :reporter | :accept | :ok
+ :oauth | :non_existing | true | 'PUBLIC' | :guest | :redirect | :redirected
+ :oauth | :non_existing | true | 'PUBLIC' | :reporter | :redirect | :redirected
+ :oauth | :non_existing | false | 'PUBLIC' | :guest | :reject | :not_found
+ :oauth | :non_existing | false | 'PUBLIC' | :reporter | :reject | :not_found
+ :oauth | :scoped_naming_convention | true | 'PRIVATE' | :guest | :reject | :forbidden
+ :oauth | :scoped_naming_convention | true | 'PRIVATE' | :reporter | :accept | :ok
+ :oauth | :scoped_naming_convention | false | 'PRIVATE' | :guest | :reject | :forbidden
+ :oauth | :scoped_naming_convention | false | 'PRIVATE' | :reporter | :accept | :ok
+ :oauth | :non_existing | true | 'PRIVATE' | :guest | :redirect | :redirected
+ :oauth | :non_existing | true | 'PRIVATE' | :reporter | :redirect | :redirected
+ :oauth | :non_existing | false | 'PRIVATE' | :guest | :reject | :forbidden
+ :oauth | :non_existing | false | 'PRIVATE' | :reporter | :reject | :not_found
+ :oauth | :scoped_naming_convention | true | 'INTERNAL' | :guest | :accept | :ok
+ :oauth | :scoped_naming_convention | true | 'INTERNAL' | :reporter | :accept | :ok
+ :oauth | :scoped_naming_convention | false | 'INTERNAL' | :guest | :accept | :ok
+ :oauth | :scoped_naming_convention | false | 'INTERNAL' | :reporter | :accept | :ok
+ :oauth | :non_existing | true | 'INTERNAL' | :guest | :redirect | :redirected
+ :oauth | :non_existing | true | 'INTERNAL' | :reporter | :redirect | :redirected
+ :oauth | :non_existing | false | 'INTERNAL' | :guest | :reject | :not_found
+ :oauth | :non_existing | false | 'INTERNAL' | :reporter | :reject | :not_found
+
+ :personal_access_token | :scoped_naming_convention | true | 'PUBLIC' | :guest | :accept | :ok
+ :personal_access_token | :scoped_naming_convention | true | 'PUBLIC' | :reporter | :accept | :ok
+ :personal_access_token | :scoped_naming_convention | false | 'PUBLIC' | :guest | :accept | :ok
+ :personal_access_token | :scoped_naming_convention | false | 'PUBLIC' | :reporter | :accept | :ok
+ :personal_access_token | :non_existing | true | 'PUBLIC' | :guest | :redirect | :redirected
+ :personal_access_token | :non_existing | true | 'PUBLIC' | :reporter | :redirect | :redirected
+ :personal_access_token | :non_existing | false | 'PUBLIC' | :guest | :reject | :not_found
+ :personal_access_token | :non_existing | false | 'PUBLIC' | :reporter | :reject | :not_found
+ :personal_access_token | :scoped_naming_convention | true | 'PRIVATE' | :guest | :reject | :forbidden
+ :personal_access_token | :scoped_naming_convention | true | 'PRIVATE' | :reporter | :accept | :ok
+ :personal_access_token | :scoped_naming_convention | false | 'PRIVATE' | :guest | :reject | :forbidden
+ :personal_access_token | :scoped_naming_convention | false | 'PRIVATE' | :reporter | :accept | :ok
+ :personal_access_token | :non_existing | true | 'PRIVATE' | :guest | :redirect | :redirected
+ :personal_access_token | :non_existing | true | 'PRIVATE' | :reporter | :redirect | :redirected
+ :personal_access_token | :non_existing | false | 'PRIVATE' | :guest | :reject | :forbidden
+ :personal_access_token | :non_existing | false | 'PRIVATE' | :reporter | :reject | :not_found
+ :personal_access_token | :scoped_naming_convention | true | 'INTERNAL' | :guest | :accept | :ok
+ :personal_access_token | :scoped_naming_convention | true | 'INTERNAL' | :reporter | :accept | :ok
+ :personal_access_token | :scoped_naming_convention | false | 'INTERNAL' | :guest | :accept | :ok
+ :personal_access_token | :scoped_naming_convention | false | 'INTERNAL' | :reporter | :accept | :ok
+ :personal_access_token | :non_existing | true | 'INTERNAL' | :guest | :redirect | :redirected
+ :personal_access_token | :non_existing | true | 'INTERNAL' | :reporter | :redirect | :redirected
+ :personal_access_token | :non_existing | false | 'INTERNAL' | :guest | :reject | :not_found
+ :personal_access_token | :non_existing | false | 'INTERNAL' | :reporter | :reject | :not_found
+
+ :job_token | :scoped_naming_convention | true | 'PUBLIC' | :developer | :accept | :ok
+ :job_token | :scoped_naming_convention | false | 'PUBLIC' | :developer | :accept | :ok
+ :job_token | :non_existing | true | 'PUBLIC' | :developer | :redirect | :redirected
+ :job_token | :non_existing | false | 'PUBLIC' | :developer | :reject | :not_found
+ :job_token | :scoped_naming_convention | true | 'PRIVATE' | :developer | :accept | :ok
+ :job_token | :scoped_naming_convention | false | 'PRIVATE' | :developer | :accept | :ok
+ :job_token | :non_existing | true | 'PRIVATE' | :developer | :redirect | :redirected
+ :job_token | :non_existing | false | 'PRIVATE' | :developer | :reject | :not_found
+ :job_token | :scoped_naming_convention | true | 'INTERNAL' | :developer | :accept | :ok
+ :job_token | :scoped_naming_convention | false | 'INTERNAL' | :developer | :accept | :ok
+ :job_token | :non_existing | true | 'INTERNAL' | :developer | :redirect | :redirected
+ :job_token | :non_existing | false | 'INTERNAL' | :developer | :reject | :not_found
+
+ :deploy_token | :scoped_naming_convention | true | 'PUBLIC' | nil | :accept | :ok
+ :deploy_token | :scoped_naming_convention | false | 'PUBLIC' | nil | :accept | :ok
+ :deploy_token | :non_existing | true | 'PUBLIC' | nil | :redirect | :redirected
+ :deploy_token | :non_existing | false | 'PUBLIC' | nil | :reject | :not_found
+ :deploy_token | :scoped_naming_convention | true | 'PRIVATE' | nil | :accept | :ok
+ :deploy_token | :scoped_naming_convention | false | 'PRIVATE' | nil | :accept | :ok
+ :deploy_token | :non_existing | true | 'PRIVATE' | nil | :redirect | :redirected
+ :deploy_token | :non_existing | false | 'PRIVATE' | nil | :reject | :not_found
+ :deploy_token | :scoped_naming_convention | true | 'INTERNAL' | nil | :accept | :ok
+ :deploy_token | :scoped_naming_convention | false | 'INTERNAL' | nil | :accept | :ok
+ :deploy_token | :non_existing | true | 'INTERNAL' | nil | :redirect | :redirected
+ :deploy_token | :non_existing | false | 'INTERNAL' | nil | :reject | :not_found
+ end
- expect_a_valid_package_response
+ with_them do
+ include_context 'set package name from package name type'
+
+ let(:headers) do
+ case auth
+ when :oauth
+ build_token_auth_header(token.token)
+ when :personal_access_token
+ build_token_auth_header(personal_access_token.token)
+ when :job_token
+ build_token_auth_header(job.token)
+ when :deploy_token
+ build_token_auth_header(deploy_token.token)
+ else
+ {}
end
+ end
- it 'denies request without running job token' do
- job.update!(status: :success)
+ before do
+ project.send("add_#{user_role}", user) if user_role
+ project.update!(visibility: Gitlab::VisibilityLevel.const_get(visibility, false))
+ package.update!(name: package_name) unless package_name == 'non-existing-package'
+ stub_application_setting(npm_package_requests_forwarding: request_forward)
+ end
- subject
+ example_name = "#{params[:expected_result]} metadata request"
+ status = params[:expected_status]
- expect(response).to have_gitlab_http_status(:unauthorized)
+ if scope == :instance && params[:package_name_type] != :scoped_naming_convention
+ if params[:request_forward]
+ example_name = 'redirect metadata request'
+ status = :redirected
+ else
+ example_name = 'reject metadata request'
+ status = :not_found
end
end
- context 'with deploy token' do
- let(:headers) { build_token_auth_header(deploy_token.token) }
+ it_behaves_like example_name, status: status
+ end
- it 'returns the package info with deploy token' do
- subject
+ context 'with a developer' do
+ let(:headers) { build_token_auth_header(personal_access_token.token) }
- expect_a_valid_package_response
- end
+ before do
+ project.add_developer(user)
end
- end
-
- context 'a public project' do
- it_behaves_like 'returning the npm package info'
context 'project path with a dot' do
before do
project.update!(path: 'foo.bar')
end
- it_behaves_like 'returning the npm package info'
+ it_behaves_like 'accept metadata request', status: :ok
end
- context 'with request forward disabled' do
+ context 'with a job token' do
+ let(:headers) { build_token_auth_header(job.token) }
+
before do
- stub_application_setting(npm_package_requests_forwarding: false)
+ job.update!(status: :success)
end
- it_behaves_like 'returning the npm package info'
+ it_behaves_like 'reject metadata request', status: :unauthorized
+ end
+ end
+end
+
+RSpec.shared_examples 'handling get dist tags requests' do |scope: :project|
+ using RSpec::Parameterized::TableSyntax
+ include_context 'set package name from package name type'
- context 'with unknown package' do
- let(:package_name) { 'unknown' }
+ let_it_be(:package_tag1) { create(:packages_tag, package: package) }
+ let_it_be(:package_tag2) { create(:packages_tag, package: package) }
- it 'returns the proper response' do
- subject
+ let(:headers) { {} }
- expect(response).to have_gitlab_http_status(:not_found)
- end
- end
+ subject { get(url, headers: headers) }
+
+ shared_examples 'reject package tags request' do |status:|
+ before do
+ package.update!(name: package_name) unless package_name == 'non-existing-package'
end
- context 'with request forward enabled' do
- before do
- stub_application_setting(npm_package_requests_forwarding: true)
- end
+ it_behaves_like 'returning response status', status
+ end
- it_behaves_like 'returning the npm package info'
+ shared_examples 'handling different package names, visibilities and user roles' do
+ where(:package_name_type, :visibility, :user_role, :expected_result, :expected_status) do
+ :scoped_naming_convention | 'PUBLIC' | :anonymous | :accept | :ok
+ :scoped_naming_convention | 'PUBLIC' | :guest | :accept | :ok
+ :scoped_naming_convention | 'PUBLIC' | :reporter | :accept | :ok
+ :non_existing | 'PUBLIC' | :anonymous | :reject | :not_found
+ :non_existing | 'PUBLIC' | :guest | :reject | :not_found
+ :non_existing | 'PUBLIC' | :reporter | :reject | :not_found
+
+ :scoped_naming_convention | 'PRIVATE' | :anonymous | :reject | :not_found
+ :scoped_naming_convention | 'PRIVATE' | :guest | :reject | :forbidden
+ :scoped_naming_convention | 'PRIVATE' | :reporter | :accept | :ok
+ :non_existing | 'PRIVATE' | :anonymous | :reject | :not_found
+ :non_existing | 'PRIVATE' | :guest | :reject | :forbidden
+ :non_existing | 'PRIVATE' | :reporter | :reject | :not_found
+
+ :scoped_naming_convention | 'INTERNAL' | :anonymous | :reject | :not_found
+ :scoped_naming_convention | 'INTERNAL' | :guest | :accept | :ok
+ :scoped_naming_convention | 'INTERNAL' | :reporter | :accept | :ok
+ :non_existing | 'INTERNAL' | :anonymous | :reject | :not_found
+ :non_existing | 'INTERNAL' | :guest | :reject | :not_found
+ :non_existing | 'INTERNAL' | :reporter | :reject | :not_found
+ end
+
+ with_them do
+ let(:anonymous) { user_role == :anonymous }
- context 'with unknown package' do
- let(:package_name) { 'unknown' }
+ subject { get(url, headers: anonymous ? {} : headers) }
- it 'returns a redirect' do
- subject
+ before do
+ project.send("add_#{user_role}", user) unless anonymous
+ project.update!(visibility: Gitlab::VisibilityLevel.const_get(visibility, false))
+ end
- expect(response).to have_gitlab_http_status(:found)
- expect(response.headers['Location']).to eq('https://registry.npmjs.org/unknown')
- end
+ example_name = "#{params[:expected_result]} package tags request"
+ status = params[:expected_status]
- it_behaves_like 'a gitlab tracking event', described_class.name, 'npm_request_forward'
+ if scope == :instance && params[:package_name_type] != :scoped_naming_convention
+ example_name = 'reject package tags request'
+ status = :not_found
end
+
+ it_behaves_like example_name, status: status
end
end
- context 'internal project' do
- before do
- project.update!(visibility_level: Gitlab::VisibilityLevel::INTERNAL)
- end
+ context 'with oauth token' do
+ let(:headers) { build_token_auth_header(token.token) }
- it_behaves_like 'a package that requires auth'
+ it_behaves_like 'handling different package names, visibilities and user roles'
end
- context 'private project' do
- before do
- project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
- end
+ context 'with personal access token' do
+ let(:headers) { build_token_auth_header(personal_access_token.token) }
- it_behaves_like 'a package that requires auth'
+ it_behaves_like 'handling different package names, visibilities and user roles'
+ end
+end
- context 'with guest' do
- let(:params) { { access_token: token.token } }
+RSpec.shared_examples 'handling create dist tag requests' do |scope: :project|
+ using RSpec::Parameterized::TableSyntax
+ include_context 'set package name from package name type'
- it 'denies request when not enough permissions' do
- project.add_guest(user)
+ let_it_be(:tag_name) { 'test' }
- subject
+ let(:params) { {} }
+ let(:version) { package.version }
+ let(:env) { { 'api.request.body': version } }
+ let(:headers) { {} }
- expect(response).to have_gitlab_http_status(:forbidden)
- end
+ shared_examples 'reject create package tag request' do |status:|
+ before do
+ package.update!(name: package_name) unless package_name == 'non-existing-package'
end
+
+ it_behaves_like 'returning response status', status
end
- def expect_a_valid_package_response
- expect(response).to have_gitlab_http_status(:ok)
- expect(response.media_type).to eq('application/json')
- expect(response).to match_response_schema('public_api/v4/packages/npm_package')
- expect(json_response['name']).to eq(package.name)
- expect(json_response['versions'][package.version]).to match_schema('public_api/v4/packages/npm_package_version')
- ::Packages::Npm::PackagePresenter::NPM_VALID_DEPENDENCY_TYPES.each do |dependency_type|
- expect(json_response.dig('versions', package.version, dependency_type.to_s)).to be_any
+ shared_examples 'handling different package names, visibilities and user roles' do
+ where(:package_name_type, :visibility, :user_role, :expected_result, :expected_status) do
+ :scoped_naming_convention | 'PUBLIC' | :anonymous | :reject | :forbidden
+ :scoped_naming_convention | 'PUBLIC' | :guest | :reject | :forbidden
+ :scoped_naming_convention | 'PUBLIC' | :developer | :accept | :ok
+ :non_existing | 'PUBLIC' | :anonymous | :reject | :forbidden
+ :non_existing | 'PUBLIC' | :guest | :reject | :forbidden
+ :non_existing | 'PUBLIC' | :developer | :reject | :not_found
+
+ :scoped_naming_convention | 'PRIVATE' | :anonymous | :reject | :not_found
+ :scoped_naming_convention | 'PRIVATE' | :guest | :reject | :forbidden
+ :scoped_naming_convention | 'PRIVATE' | :developer | :accept | :ok
+ :non_existing | 'PRIVATE' | :anonymous | :reject | :not_found
+ :non_existing | 'PRIVATE' | :guest | :reject | :forbidden
+ :non_existing | 'PRIVATE' | :developer | :reject | :not_found
+
+ :scoped_naming_convention | 'INTERNAL' | :anonymous | :reject | :forbidden
+ :scoped_naming_convention | 'INTERNAL' | :guest | :reject | :forbidden
+ :scoped_naming_convention | 'INTERNAL' | :developer | :accept | :ok
+ :non_existing | 'INTERNAL' | :anonymous | :reject | :forbidden
+ :non_existing | 'INTERNAL' | :guest | :reject | :forbidden
+ :non_existing | 'INTERNAL' | :developer | :reject | :not_found
end
- expect(json_response['dist-tags']).to match_schema('public_api/v4/packages/npm_package_tags')
- end
-end
-RSpec.shared_examples 'handling get dist tags requests' do
- let_it_be(:package_tag1) { create(:packages_tag, package: package) }
- let_it_be(:package_tag2) { create(:packages_tag, package: package) }
+ with_them do
+ let(:anonymous) { user_role == :anonymous }
- let(:params) { {} }
+ subject { put(url, env: env, headers: headers) }
- subject { get(url, params: params) }
+ before do
+ project.send("add_#{user_role}", user) unless anonymous
+ project.update!(visibility: Gitlab::VisibilityLevel.const_get(visibility, false))
+ end
- context 'with public project' do
- context 'with authenticated user' do
- let(:params) { { private_token: personal_access_token.token } }
+ example_name = "#{params[:expected_result]} create package tag request"
+ status = params[:expected_status]
- it_behaves_like 'returns package tags', :maintainer
- it_behaves_like 'returns package tags', :developer
- it_behaves_like 'returns package tags', :reporter
- it_behaves_like 'returns package tags', :guest
- end
+ if scope == :instance && params[:package_name_type] != :scoped_naming_convention
+ example_name = 'reject create package tag request'
+ status = :not_found
+ end
- context 'with unauthenticated user' do
- it_behaves_like 'returns package tags', :no_type
+ it_behaves_like example_name, status: status
end
end
- context 'with private project' do
- before do
- project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
- end
+ context 'with oauth token' do
+ let(:headers) { build_token_auth_header(token.token) }
- context 'with authenticated user' do
- let(:params) { { private_token: personal_access_token.token } }
+ it_behaves_like 'handling different package names, visibilities and user roles'
+ end
- it_behaves_like 'returns package tags', :maintainer
- it_behaves_like 'returns package tags', :developer
- it_behaves_like 'returns package tags', :reporter
- it_behaves_like 'rejects package tags access', :guest, :forbidden
- end
+ context 'with personal access token' do
+ let(:headers) { build_token_auth_header(personal_access_token.token) }
- context 'with unauthenticated user' do
- it_behaves_like 'rejects package tags access', :no_type, :not_found
- end
+ it_behaves_like 'handling different package names, visibilities and user roles'
end
end
-RSpec.shared_examples 'handling create dist tag requests' do
- let_it_be(:tag_name) { 'test' }
+RSpec.shared_examples 'handling delete dist tag requests' do |scope: :project|
+ using RSpec::Parameterized::TableSyntax
+ include_context 'set package name from package name type'
- let(:params) { {} }
- let(:env) { {} }
- let(:version) { package.version }
-
- subject { put(url, env: env, params: params) }
+ let_it_be(:package_tag) { create(:packages_tag, package: package) }
- context 'with public project' do
- context 'with authenticated user' do
- let(:params) { { private_token: personal_access_token.token } }
- let(:env) { { 'api.request.body': version } }
+ let(:tag_name) { package_tag.name }
+ let(:headers) { {} }
- it_behaves_like 'create package tag', :maintainer
- it_behaves_like 'create package tag', :developer
- it_behaves_like 'rejects package tags access', :reporter, :forbidden
- it_behaves_like 'rejects package tags access', :guest, :forbidden
+ shared_examples 'reject delete package tag request' do |status:|
+ before do
+ package.update!(name: package_name) unless package_name == 'non-existing-package'
end
- context 'with unauthenticated user' do
- it_behaves_like 'rejects package tags access', :no_type, :unauthorized
- end
+ it_behaves_like 'returning response status', status
end
-end
-RSpec.shared_examples 'handling delete dist tag requests' do
- let_it_be(:package_tag) { create(:packages_tag, package: package) }
+ shared_examples 'handling different package names, visibilities and user roles' do
+ where(:package_name_type, :visibility, :user_role, :expected_result, :expected_status) do
+ :scoped_naming_convention | 'PUBLIC' | :anonymous | :reject | :forbidden
+ :scoped_naming_convention | 'PUBLIC' | :guest | :reject | :forbidden
+ :scoped_naming_convention | 'PUBLIC' | :maintainer | :accept | :ok
+ :non_existing | 'PUBLIC' | :anonymous | :reject | :forbidden
+ :non_existing | 'PUBLIC' | :guest | :reject | :forbidden
+ :non_existing | 'PUBLIC' | :maintainer | :reject | :not_found
+
+ :scoped_naming_convention | 'PRIVATE' | :anonymous | :reject | :not_found
+ :scoped_naming_convention | 'PRIVATE' | :guest | :reject | :forbidden
+ :scoped_naming_convention | 'PRIVATE' | :maintainer | :accept | :ok
+ :non_existing | 'INTERNAL' | :anonymous | :reject | :forbidden
+ :non_existing | 'INTERNAL' | :guest | :reject | :forbidden
+ :non_existing | 'INTERNAL' | :maintainer | :reject | :not_found
+ end
- let(:params) { {} }
- let(:tag_name) { package_tag.name }
+ with_them do
+ let(:anonymous) { user_role == :anonymous }
+
+ subject { delete(url, headers: headers) }
- subject { delete(url, params: params) }
+ before do
+ project.send("add_#{user_role}", user) unless anonymous
+ project.update!(visibility: Gitlab::VisibilityLevel.const_get(visibility, false))
+ end
- context 'with public project' do
- context 'with authenticated user' do
- let(:params) { { private_token: personal_access_token.token } }
+ example_name = "#{params[:expected_result]} delete package tag request"
+ status = params[:expected_status]
- it_behaves_like 'delete package tag', :maintainer
- it_behaves_like 'rejects package tags access', :developer, :forbidden
- it_behaves_like 'rejects package tags access', :reporter, :forbidden
- it_behaves_like 'rejects package tags access', :guest, :forbidden
- end
+ if scope == :instance && params[:package_name_type] != :scoped_naming_convention
+ example_name = 'reject delete package tag request'
+ status = :not_found
+ end
- context 'with unauthenticated user' do
- it_behaves_like 'rejects package tags access', :no_type, :unauthorized
+ it_behaves_like example_name, status: status
end
end
- context 'with private project' do
- before do
- project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
- end
+ context 'with oauth token' do
+ let(:headers) { build_token_auth_header(token.token) }
- context 'with authenticated user' do
- let(:params) { { private_token: personal_access_token.token } }
+ it_behaves_like 'handling different package names, visibilities and user roles'
+ end
- it_behaves_like 'delete package tag', :maintainer
- it_behaves_like 'rejects package tags access', :developer, :forbidden
- it_behaves_like 'rejects package tags access', :reporter, :forbidden
- it_behaves_like 'rejects package tags access', :guest, :forbidden
- end
+ context 'with personal access token' do
+ let(:headers) { build_token_auth_header(personal_access_token.token) }
- context 'with unauthenticated user' do
- it_behaves_like 'rejects package tags access', :no_type, :unauthorized
- end
+ it_behaves_like 'handling different package names, visibilities and user roles'
end
end
diff --git a/spec/support/shared_examples/requests/api/npm_packages_tags_shared_examples.rb b/spec/support/shared_examples/requests/api/npm_packages_tags_shared_examples.rb
new file mode 100644
index 00000000000..e6b3dc74b74
--- /dev/null
+++ b/spec/support/shared_examples/requests/api/npm_packages_tags_shared_examples.rb
@@ -0,0 +1,190 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'rejects package tags access' do |status:|
+ before do
+ package.update!(name: package_name) unless package_name == 'non-existing-package'
+ end
+
+ it_behaves_like 'returning response status', status
+end
+
+RSpec.shared_examples 'accept package tags request' do |status:|
+ using RSpec::Parameterized::TableSyntax
+
+ before do
+ stub_application_setting(npm_package_requests_forwarding: false)
+ end
+
+ context 'with valid package name' do
+ before do
+ package.update!(name: package_name) unless package_name == 'non-existing-package'
+ end
+
+ it_behaves_like 'returning response status', status
+
+ it 'returns a valid json response' do
+ subject
+
+ expect(response.media_type).to eq('application/json')
+ expect(json_response).to be_a(Hash)
+ end
+
+ it 'returns two package tags' do
+ subject
+
+ expect(json_response).to match_schema('public_api/v4/packages/npm_package_tags')
+ expect(json_response.length).to eq(3) # two tags + latest (auto added)
+ expect(json_response[package_tag1.name]).to eq(package.version)
+ expect(json_response[package_tag2.name]).to eq(package.version)
+ expect(json_response['latest']).to eq(package.version)
+ end
+ end
+
+ context 'with invalid package name' do
+ where(:package_name, :status) do
+ '%20' | :bad_request
+ nil | :not_found
+ end
+
+ with_them do
+ it_behaves_like 'returning response status', params[:status]
+ end
+ end
+end
+
+RSpec.shared_examples 'accept create package tag request' do |user_type|
+ using RSpec::Parameterized::TableSyntax
+
+ context 'with valid package name' do
+ before do
+ package.update!(name: package_name) unless package_name == 'non-existing-package'
+ end
+
+ it_behaves_like 'returning response status', :no_content
+
+ it 'creates the package tag' do
+ expect { subject }.to change { Packages::Tag.count }.by(1)
+
+ last_tag = Packages::Tag.last
+ expect(last_tag.name).to eq(tag_name)
+ expect(last_tag.package).to eq(package)
+ end
+
+ it 'returns a valid response' do
+ subject
+
+ expect(response.body).to be_empty
+ end
+
+ context 'with already existing tag' do
+ let_it_be(:package2) { create(:npm_package, project: project, name: package.name, version: '5.5.55') }
+ let_it_be(:tag) { create(:packages_tag, package: package2, name: tag_name) }
+
+ it_behaves_like 'returning response status', :no_content
+
+ it 'reuses existing tag' do
+ expect(package.tags).to be_empty
+ expect(package2.tags).to eq([tag])
+ expect { subject }.to not_change { Packages::Tag.count }
+ expect(package.reload.tags).to eq([tag])
+ expect(package2.reload.tags).to be_empty
+ end
+
+ it 'returns a valid response' do
+ subject
+
+ expect(response.body).to be_empty
+ end
+ end
+ end
+
+ context 'with invalid package name' do
+ where(:package_name, :status) do
+ 'unknown' | :not_found
+ '' | :not_found
+ '%20' | :bad_request
+ end
+
+ with_them do
+ it_behaves_like 'returning response status', params[:status]
+ end
+ end
+
+ context 'with invalid tag name' do
+ where(:tag_name, :status) do
+ '' | :not_found
+ '%20' | :bad_request
+ end
+
+ with_them do
+ it_behaves_like 'returning response status', params[:status]
+ end
+ end
+
+ context 'with invalid version' do
+ where(:version, :status) do
+ ' ' | :bad_request
+ '' | :bad_request
+ nil | :bad_request
+ end
+
+ with_them do
+ it_behaves_like 'returning response status', params[:status]
+ end
+ end
+end
+
+RSpec.shared_examples 'accept delete package tag request' do |user_type|
+ using RSpec::Parameterized::TableSyntax
+
+ context 'with valid package name' do
+ before do
+ package.update!(name: package_name) unless package_name == 'non-existing-package'
+ end
+
+ it_behaves_like 'returning response status', :no_content
+
+ it 'returns a valid response' do
+ subject
+
+ expect(response.body).to be_empty
+ end
+
+ it 'destroy the package tag' do
+ expect(package.tags).to eq([package_tag])
+ expect { subject }.to change { Packages::Tag.count }.by(-1)
+ expect(package.reload.tags).to be_empty
+ end
+
+ context 'with tag from other package' do
+ let(:package2) { create(:npm_package, project: project) }
+ let(:package_tag) { create(:packages_tag, package: package2) }
+
+ it_behaves_like 'returning response status', :not_found
+ end
+ end
+
+ context 'with invalid package name' do
+ where(:package_name, :status) do
+ 'unknown' | :not_found
+ '' | :not_found
+ '%20' | :bad_request
+ end
+
+ with_them do
+ it_behaves_like 'returning response status', params[:status]
+ end
+ end
+
+ context 'with invalid tag name' do
+ where(:tag_name, :status) do
+ 'unknown' | :not_found
+ '' | :not_found
+ '%20' | :bad_request
+ end
+
+ with_them do
+ it_behaves_like 'returning response status', params[:status]
+ end
+ end
+end
diff --git a/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb
index 8b60857cdaf..617fdecbb5b 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
@@ -123,7 +123,7 @@ RSpec.shared_examples 'process nuget workhorse authorization' do |user_type, sta
context 'with a request that bypassed gitlab-workhorse' do
let(:headers) do
basic_auth_header(user.username, personal_access_token.token)
- .merge(workhorse_header)
+ .merge(workhorse_headers)
.tap { |h| h.delete(Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER) }
end
diff --git a/spec/support/shared_examples/requests/api/packages_shared_examples.rb b/spec/support/shared_examples/requests/api/packages_shared_examples.rb
index 3833604e304..15976eed021 100644
--- a/spec/support/shared_examples/requests/api/packages_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/packages_shared_examples.rb
@@ -24,7 +24,7 @@ end
RSpec.shared_examples 'deploy token for package uploads' do
context 'with deploy token headers' do
- let(:headers) { basic_auth_header(deploy_token.username, deploy_token.token).merge(workhorse_header) }
+ let(:headers) { basic_auth_header(deploy_token.username, deploy_token.token).merge(workhorse_headers) }
before do
project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
@@ -35,7 +35,7 @@ RSpec.shared_examples 'deploy token for package uploads' do
end
context 'invalid token' do
- let(:headers) { basic_auth_header(deploy_token.username, 'bar').merge(workhorse_header) }
+ let(:headers) { basic_auth_header(deploy_token.username, 'bar').merge(workhorse_headers) }
it_behaves_like 'returning response status', :unauthorized
end
@@ -102,7 +102,7 @@ end
RSpec.shared_examples 'job token for package uploads' do
context 'with job token headers' do
- let(:headers) { basic_auth_header(::Gitlab::Auth::CI_JOB_USER, job.token).merge(workhorse_header) }
+ let(:headers) { basic_auth_header(::Gitlab::Auth::CI_JOB_USER, job.token).merge(workhorse_headers) }
before do
project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
@@ -114,13 +114,13 @@ RSpec.shared_examples 'job token for package uploads' do
end
context 'invalid token' do
- let(:headers) { basic_auth_header(::Gitlab::Auth::CI_JOB_USER, 'bar').merge(workhorse_header) }
+ let(:headers) { basic_auth_header(::Gitlab::Auth::CI_JOB_USER, 'bar').merge(workhorse_headers) }
it_behaves_like 'returning response status', :unauthorized
end
context 'invalid user' do
- let(:headers) { basic_auth_header('foo', job.token).merge(workhorse_header) }
+ let(:headers) { basic_auth_header('foo', job.token).merge(workhorse_headers) }
it_behaves_like 'returning response status', :unauthorized
end
diff --git a/spec/support/shared_examples/requests/api/packages_tags_shared_examples.rb b/spec/support/shared_examples/requests/api/packages_tags_shared_examples.rb
deleted file mode 100644
index 2c203dc096e..00000000000
--- a/spec/support/shared_examples/requests/api/packages_tags_shared_examples.rb
+++ /dev/null
@@ -1,185 +0,0 @@
-# frozen_string_literal: true
-
-RSpec.shared_examples 'rejects package tags access' do |user_type, status|
- context "for user type #{user_type}" do
- before do
- project.send("add_#{user_type}", user) unless user_type == :no_type
- end
-
- it_behaves_like 'returning response status', status
- end
-end
-
-RSpec.shared_examples 'returns package tags' do |user_type|
- using RSpec::Parameterized::TableSyntax
-
- before do
- stub_application_setting(npm_package_requests_forwarding: false)
- project.send("add_#{user_type}", user) unless user_type == :no_type
- end
-
- it_behaves_like 'returning response status', :success
-
- it 'returns a valid json response' do
- subject
-
- expect(response.media_type).to eq('application/json')
- expect(json_response).to be_a(Hash)
- end
-
- it 'returns two package tags' do
- subject
-
- expect(json_response).to match_schema('public_api/v4/packages/npm_package_tags')
- expect(json_response.length).to eq(3) # two tags + latest (auto added)
- expect(json_response[package_tag1.name]).to eq(package.version)
- expect(json_response[package_tag2.name]).to eq(package.version)
- expect(json_response['latest']).to eq(package.version)
- end
-
- context 'with invalid package name' do
- where(:package_name, :status) do
- '%20' | :bad_request
- nil | :not_found
- end
-
- with_them do
- it_behaves_like 'returning response status', params[:status]
- end
- end
-end
-
-RSpec.shared_examples 'create package tag' do |user_type|
- using RSpec::Parameterized::TableSyntax
-
- before do
- project.send("add_#{user_type}", user) unless user_type == :no_type
- end
-
- it_behaves_like 'returning response status', :no_content
-
- it 'creates the package tag' do
- expect { subject }.to change { Packages::Tag.count }.by(1)
-
- last_tag = Packages::Tag.last
- expect(last_tag.name).to eq(tag_name)
- expect(last_tag.package).to eq(package)
- end
-
- it 'returns a valid response' do
- subject
-
- expect(response.body).to be_empty
- end
-
- context 'with already existing tag' do
- let_it_be(:package2) { create(:npm_package, project: project, name: package.name, version: '5.5.55') }
- let_it_be(:tag) { create(:packages_tag, package: package2, name: tag_name) }
-
- it_behaves_like 'returning response status', :no_content
-
- it 'reuses existing tag' do
- expect(package.tags).to be_empty
- expect(package2.tags).to eq([tag])
- expect { subject }.to not_change { Packages::Tag.count }
- expect(package.reload.tags).to eq([tag])
- expect(package2.reload.tags).to be_empty
- end
-
- it 'returns a valid response' do
- subject
-
- expect(response.body).to be_empty
- end
- end
-
- context 'with invalid package name' do
- where(:package_name, :status) do
- 'unknown' | :not_found
- '' | :not_found
- '%20' | :bad_request
- end
-
- with_them do
- it_behaves_like 'returning response status', params[:status]
- end
- end
-
- context 'with invalid tag name' do
- where(:tag_name, :status) do
- '' | :not_found
- '%20' | :bad_request
- end
-
- with_them do
- it_behaves_like 'returning response status', params[:status]
- end
- end
-
- context 'with invalid version' do
- where(:version, :status) do
- ' ' | :bad_request
- '' | :bad_request
- nil | :bad_request
- end
-
- with_them do
- it_behaves_like 'returning response status', params[:status]
- end
- end
-end
-
-RSpec.shared_examples 'delete package tag' do |user_type|
- using RSpec::Parameterized::TableSyntax
-
- before do
- project.send("add_#{user_type}", user) unless user_type == :no_type
- end
-
- context "for #{user_type} user" do
- it_behaves_like 'returning response status', :no_content
-
- it 'returns a valid response' do
- subject
-
- expect(response.body).to be_empty
- end
-
- it 'destroy the package tag' do
- expect(package.tags).to eq([package_tag])
- expect { subject }.to change { Packages::Tag.count }.by(-1)
- expect(package.reload.tags).to be_empty
- end
-
- context 'with tag from other package' do
- let(:package2) { create(:npm_package, project: project) }
- let(:package_tag) { create(:packages_tag, package: package2) }
-
- it_behaves_like 'returning response status', :not_found
- end
-
- context 'with invalid package name' do
- where(:package_name, :status) do
- 'unknown' | :not_found
- '' | :not_found
- '%20' | :bad_request
- end
-
- with_them do
- it_behaves_like 'returning response status', params[:status]
- end
- end
-
- context 'with invalid tag name' do
- where(:tag_name, :status) do
- 'unknown' | :not_found
- '' | :not_found
- '%20' | :bad_request
- end
-
- with_them do
- it_behaves_like 'returning response status', params[:status]
- end
- end
- end
-end
diff --git a/spec/support/shared_examples/requests/api/read_user_shared_examples.rb b/spec/support/shared_examples/requests/api/read_user_shared_examples.rb
index 59cd0ab67b4..b9fd997bd2c 100644
--- a/spec/support/shared_examples/requests/api/read_user_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/read_user_shared_examples.rb
@@ -7,21 +7,33 @@ RSpec.shared_examples 'allows the "read_user" scope' do |api_version|
context 'when the requesting token has the "api" scope' do
let(:token) { create(:personal_access_token, scopes: ['api'], user: user) }
- it 'returns a "200" response' do
+ it 'returns a "200" response on get request' do
get api_call.call(path, user, personal_access_token: token, version: version)
expect(response).to have_gitlab_http_status(:ok)
end
+
+ it 'returns a "200" response on head request' do
+ head api_call.call(path, user, personal_access_token: token, version: version)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
end
context 'when the requesting token has the "read_user" scope' do
let(:token) { create(:personal_access_token, scopes: ['read_user'], user: user) }
- it 'returns a "200" response' do
+ it 'returns a "200" response on get request' do
get api_call.call(path, user, personal_access_token: token, version: version)
expect(response).to have_gitlab_http_status(:ok)
end
+
+ it 'returns a "200" response on head request' do
+ head api_call.call(path, user, personal_access_token: token, version: version)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
end
context 'when the requesting token does not have any required scope' do
@@ -45,21 +57,33 @@ RSpec.shared_examples 'allows the "read_user" scope' do |api_version|
context 'when the requesting token has the "api" scope' do
let!(:token) { Doorkeeper::AccessToken.create! application_id: application.id, resource_owner_id: user.id, scopes: "api" }
- it 'returns a "200" response' do
+ it 'returns a "200" response on get request' do
get api_call.call(path, user, oauth_access_token: token)
expect(response).to have_gitlab_http_status(:ok)
end
+
+ it 'returns a "200" response on head request' do
+ head api_call.call(path, user, oauth_access_token: token)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
end
context 'when the requesting token has the "read_user" scope' do
let!(:token) { Doorkeeper::AccessToken.create! application_id: application.id, resource_owner_id: user.id, scopes: "read_user" }
- it 'returns a "200" response' do
+ it 'returns a "200" response on get request' do
get api_call.call(path, user, oauth_access_token: token)
expect(response).to have_gitlab_http_status(:ok)
end
+
+ it 'returns a "200" response on head request' do
+ head api_call.call(path, user, oauth_access_token: token)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
end
context 'when the requesting token does not have any required scope' do
diff --git a/spec/support/shared_examples/requests/api/repository_storage_moves_shared_examples.rb b/spec/support/shared_examples/requests/api/repository_storage_moves_shared_examples.rb
index b2970fd265d..3ca2b9fa6de 100644
--- a/spec/support/shared_examples/requests/api/repository_storage_moves_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/repository_storage_moves_shared_examples.rb
@@ -85,14 +85,37 @@ RSpec.shared_examples 'repository_storage_moves API' do |container_type|
end
describe "GET /#{container_type}/:id/repository_storage_moves" do
- it_behaves_like 'get container repository storage move list' do
- let(:url) { "/#{container_type}/#{container.id}/repository_storage_moves" }
+ let(:container_id) { container.id }
+ let(:url) { "/#{container_type}/#{container_id}/repository_storage_moves" }
+
+ it_behaves_like 'get container repository storage move list'
+
+ context 'non-existent container' do
+ let(:container_id) { non_existing_record_id }
+
+ it 'returns not found' do
+ get api(url, user)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
end
end
describe "GET /#{container_type}/:id/repository_storage_moves/:repository_storage_move_id" do
- it_behaves_like 'get single container repository storage move' do
- let(:url) { "/#{container_type}/#{container.id}/repository_storage_moves/#{repository_storage_move_id}" }
+ let(:container_id) { container.id }
+ let(:url) { "/#{container_type}/#{container_id}/repository_storage_moves/#{repository_storage_move_id}" }
+
+ it_behaves_like 'get single container repository storage move'
+
+ context 'non-existent container' do
+ let(:container_id) { non_existing_record_id }
+ let(:repository_storage_move_id) { storage_move.id }
+
+ it 'returns not found' do
+ get api(url, user)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
end
end
@@ -109,7 +132,8 @@ RSpec.shared_examples 'repository_storage_moves API' do |container_type|
end
describe "POST /#{container_type}/:id/repository_storage_moves" do
- let(:url) { "/#{container_type}/#{container.id}/repository_storage_moves" }
+ let(:container_id) { container.id }
+ let(:url) { "/#{container_type}/#{container_id}/repository_storage_moves" }
let(:destination_storage_name) { 'test_second_storage' }
def create_container_repository_storage_move
@@ -154,6 +178,16 @@ RSpec.shared_examples 'repository_storage_moves API' do |container_type|
expect(json_response['destination_storage_name']).to be_present
end
end
+
+ context 'when container does not exist' do
+ let(:container_id) { non_existing_record_id }
+
+ it 'returns not found' do
+ create_container_repository_storage_move
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
end
describe "POST /#{container_type.singularize}_repository_storage_moves" do
diff --git a/spec/support/shared_examples/requests/api/resolvable_discussions_shared_examples.rb b/spec/support/shared_examples/requests/api/resolvable_discussions_shared_examples.rb
index 460e8d57a2b..b5139bd8c99 100644
--- a/spec/support/shared_examples/requests/api/resolvable_discussions_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/resolvable_discussions_shared_examples.rb
@@ -13,6 +13,9 @@ RSpec.shared_examples 'resolvable discussions API' do |parent_type, noteable_typ
end
it "unresolves discussion if resolved is false" do
+ expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
+ .to receive(:track_unresolve_thread_action).with(user: user)
+
put api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/"\
"discussions/#{note.discussion_id}", user), params: { resolved: false }
diff --git a/spec/support/shared_examples/requests/rack_attack_shared_examples.rb b/spec/support/shared_examples/requests/rack_attack_shared_examples.rb
index 3b039049ca9..926da827e75 100644
--- a/spec/support/shared_examples/requests/rack_attack_shared_examples.rb
+++ b/spec/support/shared_examples/requests/rack_attack_shared_examples.rb
@@ -112,7 +112,7 @@ RSpec.shared_examples 'rate-limited token-authenticated requests' do
expect(response).not_to have_gitlab_http_status(:too_many_requests)
end
- arguments = {
+ arguments = a_hash_including({
message: 'Rack_Attack',
env: :throttle,
remote_ip: '127.0.0.1',
@@ -121,7 +121,7 @@ RSpec.shared_examples 'rate-limited token-authenticated requests' do
user_id: user.id,
'meta.user' => user.username,
matched: throttle_types[throttle_setting_prefix]
- }
+ })
expect(Gitlab::AuthLogger).to receive(:error).with(arguments).once
@@ -278,7 +278,7 @@ RSpec.shared_examples 'rate-limited web authenticated requests' do
expect(response).not_to have_gitlab_http_status(:too_many_requests)
end
- arguments = {
+ arguments = a_hash_including({
message: 'Rack_Attack',
env: :throttle,
remote_ip: '127.0.0.1',
@@ -287,7 +287,7 @@ RSpec.shared_examples 'rate-limited web authenticated requests' do
user_id: user.id,
'meta.user' => user.username,
matched: throttle_types[throttle_setting_prefix]
- }
+ })
expect(Gitlab::AuthLogger).to receive(:error).with(arguments).once
expect { request_authenticated_web_url }.not_to exceed_query_limit(control_count)
diff --git a/spec/support/shared_examples/services/boards/boards_list_service_shared_examples.rb b/spec/support/shared_examples/services/boards/boards_list_service_shared_examples.rb
index 8f7c08ed625..0e2bddc19ab 100644
--- a/spec/support/shared_examples/services/boards/boards_list_service_shared_examples.rb
+++ b/spec/support/shared_examples/services/boards/boards_list_service_shared_examples.rb
@@ -1,32 +1,8 @@
# frozen_string_literal: true
RSpec.shared_examples 'boards list service' do
- context 'when parent does not have a board' do
- it 'creates a new parent board' do
- expect { service.execute }.to change(parent.boards, :count).by(1)
- end
-
- it 'delegates the parent board creation to Boards::CreateService' do
- expect_any_instance_of(Boards::CreateService).to receive(:execute).once
-
- service.execute
- end
-
- context 'when create_default_board is false' do
- it 'does not create a new parent board' do
- expect { service.execute(create_default_board: false) }.not_to change(parent.boards, :count)
- end
- end
- end
-
- context 'when parent has a board' do
- before do
- create(:board, resource_parent: parent)
- end
-
- it 'does not create a new board' do
- expect { service.execute }.not_to change(parent.boards, :count)
- end
+ it 'does not create a new board' do
+ expect { service.execute }.not_to change(parent.boards, :count)
end
it 'returns parent boards' do
diff --git a/spec/support/shared_examples/services/boards/lists_create_service_shared_examples.rb b/spec/support/shared_examples/services/boards/lists_create_service_shared_examples.rb
new file mode 100644
index 00000000000..3be002c2126
--- /dev/null
+++ b/spec/support/shared_examples/services/boards/lists_create_service_shared_examples.rb
@@ -0,0 +1,81 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'board lists create service' do
+ describe '#execute' do
+ let_it_be(:user) { create(:user) }
+
+ before_all do
+ parent.add_developer(user)
+ end
+
+ subject(:service) { described_class.new(parent, user, label_id: label.id) }
+
+ context 'when board lists is empty' do
+ it 'creates a new list at beginning of the list' do
+ response = service.execute(board)
+
+ expect(response.success?).to eq(true)
+ expect(response.payload[:list].position).to eq 0
+ end
+ end
+
+ context 'when board lists has the done list' do
+ it 'creates a new list at beginning of the list' do
+ response = service.execute(board)
+
+ expect(response.success?).to eq(true)
+ expect(response.payload[:list].position).to eq 0
+ end
+ end
+
+ context 'when board lists has labels lists' do
+ it 'creates a new list at end of the lists' do
+ create_list(position: 0)
+ create_list(position: 1)
+
+ response = service.execute(board)
+
+ expect(response.success?).to eq(true)
+ expect(response.payload[:list].position).to eq 2
+ end
+ end
+
+ context 'when board lists has label and done lists' do
+ it 'creates a new list at end of the label lists' do
+ list1 = create_list(position: 0)
+
+ list2 = service.execute(board).payload[:list]
+
+ expect(list1.reload.position).to eq 0
+ expect(list2.reload.position).to eq 1
+ end
+ end
+
+ context 'when provided label does not belong to the parent' do
+ it 'returns an error' do
+ label = create(:label, name: 'in-development')
+ service = described_class.new(parent, user, label_id: label.id)
+
+ response = service.execute(board)
+
+ expect(response.success?).to eq(false)
+ expect(response.errors).to include('Label not found')
+ end
+ end
+
+ context 'when backlog param is sent' do
+ it 'creates one and only one backlog list' do
+ service = described_class.new(parent, user, 'backlog' => true)
+ list = service.execute(board).payload[:list]
+
+ expect(list.list_type).to eq('backlog')
+ expect(list.position).to be_nil
+ expect(list).to be_valid
+
+ another_backlog = service.execute(board).payload[:list]
+
+ expect(another_backlog).to eq list
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/services/issuable_shared_examples.rb b/spec/support/shared_examples/services/issuable_shared_examples.rb
index 47c7a1e7356..5b3e0f9e0b9 100644
--- a/spec/support/shared_examples/services/issuable_shared_examples.rb
+++ b/spec/support/shared_examples/services/issuable_shared_examples.rb
@@ -18,6 +18,27 @@ RSpec.shared_examples 'updating a single task' do
update_issuable(description: "- [ ] Task 1\n- [ ] Task 2")
end
+ context 'usage counters' do
+ it 'update as expected' do
+ if try(:merge_request)
+ expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
+ .to receive(:track_task_item_status_changed).once.with(user: user)
+ else
+ expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
+ .not_to receive(:track_task_item_status_changed)
+ end
+
+ update_issuable(
+ update_task: {
+ index: 1,
+ checked: true,
+ line_source: '- [ ] Task 1',
+ line_number: 1
+ }
+ )
+ end
+ end
+
context 'when a task is marked as completed' do
before do
update_issuable(update_task: { index: 1, checked: true, line_source: '- [ ] Task 1', line_number: 1 })
diff --git a/spec/support/shared_examples/services/packages_shared_examples.rb b/spec/support/shared_examples/services/packages_shared_examples.rb
index fa307d2a9a6..4e34c191306 100644
--- a/spec/support/shared_examples/services/packages_shared_examples.rb
+++ b/spec/support/shared_examples/services/packages_shared_examples.rb
@@ -40,6 +40,19 @@ RSpec.shared_examples 'assigns the package creator' do
end
end
+RSpec.shared_examples 'assigns status to package' do
+ context 'with status param' do
+ let_it_be(:status) { 'hidden' }
+ let(:params) { super().merge(status: status) }
+
+ it 'assigns the status to the package' do
+ package = subject
+
+ expect(package.status).to eq(status)
+ end
+ end
+end
+
RSpec.shared_examples 'returns packages' do |container_type, user_type|
context "for #{user_type}" do
before do
@@ -190,6 +203,7 @@ RSpec.shared_examples 'filters on each package_type' do |is_project: false|
let_it_be(:package7) { create(:generic_package, project: project) }
let_it_be(:package8) { create(:golang_package, project: project) }
let_it_be(:package9) { create(:debian_package, project: project) }
+ let_it_be(:package9) { create(:rubygems_package, project: project) }
Packages::Package.package_types.keys.each do |package_type|
context "for package type #{package_type}" do
@@ -262,3 +276,41 @@ RSpec.shared_examples 'with versionless packages' do
end
end
end
+
+RSpec.shared_examples 'with status param' do
+ context 'hidden packages' do
+ let!(:hidden_package) { create(:maven_package, :hidden, project: project) }
+
+ shared_examples 'not including the hidden package' do
+ it 'does not return the package' do
+ subject
+
+ expect(json_response.map { |package| package['id'] }).not_to include(hidden_package.id)
+ end
+ end
+
+ context 'no status param' do
+ it_behaves_like 'not including the hidden package'
+ end
+
+ context 'with hidden status param' do
+ let(:params) { super().merge(status: 'hidden') }
+
+ it 'returns the package' do
+ subject
+
+ expect(json_response.map { |package| package['id'] }).to include(hidden_package.id)
+ end
+ end
+ end
+
+ context 'bad status param' do
+ let(:params) { super().merge(status: 'invalid') }
+
+ it 'returns the package' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+ end
+end
diff --git a/spec/support/shared_examples/services/projects/update_repository_storage_service_shared_examples.rb b/spec/support/shared_examples/services/projects/update_repository_storage_service_shared_examples.rb
index f201c7b1780..1fb1b9f79b2 100644
--- a/spec/support/shared_examples/services/projects/update_repository_storage_service_shared_examples.rb
+++ b/spec/support/shared_examples/services/projects/update_repository_storage_service_shared_examples.rb
@@ -71,7 +71,7 @@ RSpec.shared_examples 'moves repository to another storage' do |repository_type|
it 'does not enqueue a GC run' do
expect { subject.execute }
- .not_to change(GitGarbageCollectWorker.jobs, :count)
+ .not_to change(Projects::GitGarbageCollectWorker.jobs, :count)
end
end
@@ -84,24 +84,29 @@ RSpec.shared_examples 'moves repository to another storage' do |repository_type|
stub_application_setting(housekeeping_enabled: false)
expect { subject.execute }
- .not_to change(GitGarbageCollectWorker.jobs, :count)
+ .not_to change(Projects::GitGarbageCollectWorker.jobs, :count)
end
it 'enqueues a GC run' do
expect { subject.execute }
- .to change(GitGarbageCollectWorker.jobs, :count).by(1)
+ .to change(Projects::GitGarbageCollectWorker.jobs, :count).by(1)
end
end
end
context 'when the filesystems are the same' do
- let(:destination) { project.repository_storage }
+ before do
+ expect(Gitlab::GitalyClient).to receive(:filesystem_id).twice.and_return(SecureRandom.uuid)
+ end
- it 'bails out and does nothing' do
+ it 'updates the database without trying to move the repostory', :aggregate_failures do
result = subject.execute
+ project.reload
- expect(result).to be_error
- expect(result.message).to match(/SameFilesystemError/)
+ expect(result).to be_success
+ expect(project).not_to be_repository_read_only
+ expect(project.repository_storage).to eq('test_second_storage')
+ expect(project.project_repository.shard_name).to eq('test_second_storage')
end
end
diff --git a/spec/support/shared_examples/services/repositories/housekeeping_shared_examples.rb b/spec/support/shared_examples/services/repositories/housekeeping_shared_examples.rb
index a174ae94b75..4c00faee56b 100644
--- a/spec/support/shared_examples/services/repositories/housekeeping_shared_examples.rb
+++ b/spec/support/shared_examples/services/repositories/housekeeping_shared_examples.rb
@@ -3,16 +3,16 @@
RSpec.shared_examples 'housekeeps repository' do
subject { described_class.new(resource) }
- context 'with a clean redis state', :clean_gitlab_redis_shared_state do
+ context 'with a clean redis state', :clean_gitlab_redis_shared_state, :aggregate_failures do
describe '#execute' do
it 'enqueues a sidekiq job' do
expect(subject).to receive(:try_obtain_lease).and_return(:the_uuid)
expect(subject).to receive(:lease_key).and_return(:the_lease_key)
expect(subject).to receive(:task).and_return(:incremental_repack)
- expect(GitGarbageCollectWorker).to receive(:perform_async).with(resource.id, :incremental_repack, :the_lease_key, :the_uuid).and_call_original
+ expect(resource.git_garbage_collect_worker_klass).to receive(:perform_async).with(resource.id, :incremental_repack, :the_lease_key, :the_uuid).and_call_original
Sidekiq::Testing.fake! do
- expect { subject.execute }.to change(GitGarbageCollectWorker.jobs, :size).by(1)
+ expect { subject.execute }.to change(resource.git_garbage_collect_worker_klass.jobs, :size).by(1)
end
end
@@ -38,7 +38,7 @@ RSpec.shared_examples 'housekeeps repository' do
end
it 'does not enqueue a job' do
- expect(GitGarbageCollectWorker).not_to receive(:perform_async)
+ expect(resource.git_garbage_collect_worker_klass).not_to receive(:perform_async)
expect { subject.execute }.to raise_error(Repositories::HousekeepingService::LeaseTaken)
end
@@ -63,16 +63,16 @@ RSpec.shared_examples 'housekeeps repository' do
allow(subject).to receive(:lease_key).and_return(:the_lease_key)
# At push 200
- expect(GitGarbageCollectWorker).to receive(:perform_async).with(resource.id, :gc, :the_lease_key, :the_uuid)
+ expect(resource.git_garbage_collect_worker_klass).to receive(:perform_async).with(resource.id, :gc, :the_lease_key, :the_uuid)
.once
# At push 50, 100, 150
- expect(GitGarbageCollectWorker).to receive(:perform_async).with(resource.id, :full_repack, :the_lease_key, :the_uuid)
+ expect(resource.git_garbage_collect_worker_klass).to receive(:perform_async).with(resource.id, :full_repack, :the_lease_key, :the_uuid)
.exactly(3).times
# At push 10, 20, ... (except those above)
- expect(GitGarbageCollectWorker).to receive(:perform_async).with(resource.id, :incremental_repack, :the_lease_key, :the_uuid)
+ expect(resource.git_garbage_collect_worker_klass).to receive(:perform_async).with(resource.id, :incremental_repack, :the_lease_key, :the_uuid)
.exactly(16).times
# At push 6, 12, 18, ... (except those above)
- expect(GitGarbageCollectWorker).to receive(:perform_async).with(resource.id, :pack_refs, :the_lease_key, :the_uuid)
+ expect(resource.git_garbage_collect_worker_klass).to receive(:perform_async).with(resource.id, :pack_refs, :the_lease_key, :the_uuid)
.exactly(27).times
201.times do
@@ -90,7 +90,7 @@ RSpec.shared_examples 'housekeeps repository' do
allow(housekeeping).to receive(:try_obtain_lease).and_return(:gc_uuid)
allow(housekeeping).to receive(:lease_key).and_return(:gc_lease_key)
- expect(GitGarbageCollectWorker).to receive(:perform_async).with(resource.id, :gc, :gc_lease_key, :gc_uuid).twice
+ expect(resource.git_garbage_collect_worker_klass).to receive(:perform_async).with(resource.id, :gc, :gc_lease_key, :gc_uuid).twice
2.times do
housekeeping.execute
diff --git a/spec/support/shared_examples/services/resource_events/change_milestone_service_shared_examples.rb b/spec/support/shared_examples/services/resource_events/change_milestone_service_shared_examples.rb
index d70ed707822..fac9f1d6253 100644
--- a/spec/support/shared_examples/services/resource_events/change_milestone_service_shared_examples.rb
+++ b/spec/support/shared_examples/services/resource_events/change_milestone_service_shared_examples.rb
@@ -3,8 +3,12 @@
RSpec.shared_examples 'timebox(milestone or iteration) resource events creator' do |timebox_event_class|
let_it_be(:user) { create(:user) }
+ before do
+ resource.system_note_timestamp = created_at_time
+ end
+
context 'when milestone/iteration is added' do
- let(:service) { described_class.new(resource, user, add_timebox_args) }
+ let(:service) { described_class.new(resource, user, **add_timebox_args) }
before do
set_timebox(timebox_event_class, timebox)
@@ -18,7 +22,7 @@ RSpec.shared_examples 'timebox(milestone or iteration) resource events creator'
end
context 'when milestone/iteration is removed' do
- let(:service) { described_class.new(resource, user, remove_timebox_args) }
+ let(:service) { described_class.new(resource, user, **remove_timebox_args) }
before do
set_timebox(timebox_event_class, nil)
diff --git a/spec/support/shared_examples/services/snippets_shared_examples.rb b/spec/support/shared_examples/services/snippets_shared_examples.rb
index 4a08c0d4365..10add3a7299 100644
--- a/spec/support/shared_examples/services/snippets_shared_examples.rb
+++ b/spec/support/shared_examples/services/snippets_shared_examples.rb
@@ -1,42 +1,56 @@
# frozen_string_literal: true
-RSpec.shared_examples 'snippets spam check is performed' do
- shared_examples 'marked as spam' do
- it 'marks a snippet as spam' do
- expect(snippet).to be_spam
- end
+RSpec.shared_examples 'checking spam' do
+ let(:request) { double(:request) }
+ let(:api) { true }
+ let(:captcha_response) { 'abc123' }
+ let(:spam_log_id) { 1 }
+ let(:disable_spam_action_service) { false }
- it 'invalidates the snippet' do
- expect(snippet).to be_invalid
- end
+ let(:extra_opts) do
+ {
+ request: request,
+ api: api,
+ captcha_response: captcha_response,
+ spam_log_id: spam_log_id,
+ disable_spam_action_service: disable_spam_action_service
+ }
+ end
- it 'creates a new spam_log' do
- expect { snippet }
- .to have_spam_log(title: snippet.title, noteable_type: snippet.class.name)
+ before do
+ allow_next_instance_of(UserAgentDetailService) do |instance|
+ allow(instance).to receive(:create)
end
+ end
- it 'assigns a spam_log to an issue' do
- expect(snippet.spam_log).to eq(SpamLog.last)
+ it 'executes SpamActionService' do
+ spam_params = Spam::SpamParams.new(
+ api: api,
+ captcha_response: captcha_response,
+ spam_log_id: spam_log_id
+ )
+ expect_next_instance_of(
+ Spam::SpamActionService,
+ {
+ spammable: kind_of(Snippet),
+ request: request,
+ user: an_instance_of(User),
+ action: action
+ }
+ ) do |instance|
+ expect(instance).to receive(:execute).with(spam_params: spam_params)
end
- end
- let(:extra_opts) do
- { visibility_level: Gitlab::VisibilityLevel::PUBLIC, request: double(:request, env: {}) }
+ subject
end
- before do
- expect_next_instance_of(Spam::AkismetService) do |akismet_service|
- expect(akismet_service).to receive_messages(spam?: true)
- end
- end
+ context 'when spam action service is disabled' do
+ let(:disable_spam_action_service) { true }
- [true, false, nil].each do |allow_possible_spam|
- context "when allow_possible_spam flag is #{allow_possible_spam.inspect}" do
- before do
- stub_feature_flags(allow_possible_spam: allow_possible_spam) unless allow_possible_spam.nil?
- end
+ it 'request parameter is not passed to the service' do
+ expect(Spam::SpamActionService).not_to receive(:new)
- it_behaves_like 'marked as spam'
+ subject
end
end
end
diff --git a/spec/support/shared_examples/workers/concerns/git_garbage_collect_methods_shared_examples.rb b/spec/support/shared_examples/workers/concerns/git_garbage_collect_methods_shared_examples.rb
new file mode 100644
index 00000000000..f2314793cb4
--- /dev/null
+++ b/spec/support/shared_examples/workers/concerns/git_garbage_collect_methods_shared_examples.rb
@@ -0,0 +1,320 @@
+# frozen_string_literal: true
+
+require 'fileutils'
+
+RSpec.shared_examples 'can collect git garbage' do |update_statistics: true|
+ include GitHelpers
+
+ let!(:lease_uuid) { SecureRandom.uuid }
+ let!(:lease_key) { "resource_housekeeping:#{resource.id}" }
+ let(:params) { [resource.id, task, lease_key, lease_uuid] }
+ let(:shell) { Gitlab::Shell.new }
+ let(:repository) { resource.repository }
+ let(:statistics_service_klass) { nil }
+
+ subject { described_class.new }
+
+ before do
+ allow(subject).to receive(:find_resource).and_return(resource)
+ end
+
+ shared_examples 'it calls Gitaly' do
+ specify do
+ repository_service = instance_double(Gitlab::GitalyClient::RepositoryService)
+
+ expect(subject).to receive(:get_gitaly_client).with(task, repository.raw_repository).and_return(repository_service)
+ expect(repository_service).to receive(gitaly_task)
+
+ subject.perform(*params)
+ end
+ end
+
+ shared_examples 'it updates the resource statistics' do
+ it 'updates the resource statistics' do
+ expect_next_instance_of(statistics_service_klass, anything, nil, statistics: statistics_keys) do |service|
+ expect(service).to receive(:execute)
+ end
+
+ subject.perform(*params)
+ end
+
+ it 'does nothing if the database is read-only' do
+ allow(Gitlab::Database).to receive(:read_only?) { true }
+
+ expect(statistics_service_klass).not_to receive(:new)
+
+ subject.perform(*params)
+ end
+ end
+
+ describe '#perform', :aggregate_failures do
+ let(:gitaly_task) { :garbage_collect }
+ let(:task) { :gc }
+
+ context 'with active lease_uuid' do
+ before do
+ allow(subject).to receive(:get_lease_uuid).and_return(lease_uuid)
+ end
+
+ it_behaves_like 'it calls Gitaly'
+ it_behaves_like 'it updates the resource statistics' if update_statistics
+
+ it "flushes ref caches when the task if 'gc'" do
+ expect(subject).to receive(:renew_lease).with(lease_key, lease_uuid).and_call_original
+ expect(repository).to receive(:expire_branches_cache).and_call_original
+ expect(repository).to receive(:branch_names).and_call_original
+ expect(repository).to receive(:has_visible_content?).and_call_original
+ expect(repository.raw_repository).to receive(:has_visible_content?).and_call_original
+
+ subject.perform(*params)
+ end
+
+ it 'handles gRPC errors' do
+ allow_next_instance_of(Gitlab::GitalyClient::RepositoryService, repository.raw_repository) do |instance|
+ allow(instance).to receive(:garbage_collect).and_raise(GRPC::NotFound)
+ end
+
+ expect { subject.perform(*params) }.to raise_exception(Gitlab::Git::Repository::NoRepository)
+ end
+ end
+
+ context 'with different lease than the active one' do
+ before do
+ allow(subject).to receive(:get_lease_uuid).and_return(SecureRandom.uuid)
+ end
+
+ it 'returns silently' do
+ expect(repository).not_to receive(:expire_branches_cache).and_call_original
+ expect(repository).not_to receive(:branch_names).and_call_original
+ expect(repository).not_to receive(:has_visible_content?).and_call_original
+
+ subject.perform(*params)
+ end
+ end
+
+ context 'with no active lease' do
+ let(:params) { [resource.id] }
+
+ before do
+ allow(subject).to receive(:get_lease_uuid).and_return(false)
+ end
+
+ context 'when is able to get the lease' do
+ before do
+ allow(subject).to receive(:try_obtain_lease).and_return(SecureRandom.uuid)
+ end
+
+ it_behaves_like 'it calls Gitaly'
+ it_behaves_like 'it updates the resource statistics' if update_statistics
+
+ it "flushes ref caches when the task if 'gc'" do
+ expect(subject).to receive(:get_lease_uuid).with("git_gc:#{task}:#{expected_default_lease}").and_return(false)
+ expect(repository).to receive(:expire_branches_cache).and_call_original
+ expect(repository).to receive(:branch_names).and_call_original
+ expect(repository).to receive(:has_visible_content?).and_call_original
+ expect(repository.raw_repository).to receive(:has_visible_content?).and_call_original
+
+ subject.perform(*params)
+ end
+ end
+
+ context 'when no lease can be obtained' do
+ it 'returns silently' do
+ expect(subject).to receive(:try_obtain_lease).and_return(false)
+
+ expect(subject).not_to receive(:command)
+ expect(repository).not_to receive(:expire_branches_cache).and_call_original
+ expect(repository).not_to receive(:branch_names).and_call_original
+ expect(repository).not_to receive(:has_visible_content?).and_call_original
+
+ subject.perform(*params)
+ end
+ end
+ end
+
+ context 'repack_full' do
+ let(:task) { :full_repack }
+ let(:gitaly_task) { :repack_full }
+
+ before do
+ expect(subject).to receive(:get_lease_uuid).and_return(lease_uuid)
+ end
+
+ it_behaves_like 'it calls Gitaly'
+ it_behaves_like 'it updates the resource statistics' if update_statistics
+ end
+
+ context 'pack_refs' do
+ let(:task) { :pack_refs }
+ let(:gitaly_task) { :pack_refs }
+
+ before do
+ expect(subject).to receive(:get_lease_uuid).and_return(lease_uuid)
+ end
+
+ it 'calls Gitaly' do
+ repository_service = instance_double(Gitlab::GitalyClient::RefService)
+
+ expect(subject).to receive(:get_gitaly_client).with(task, repository.raw_repository).and_return(repository_service)
+ expect(repository_service).to receive(gitaly_task)
+
+ subject.perform(*params)
+ end
+
+ it 'does not update the resource statistics' do
+ expect(statistics_service_klass).not_to receive(:new)
+
+ subject.perform(*params)
+ end
+ end
+
+ context 'repack_incremental' do
+ let(:task) { :incremental_repack }
+ let(:gitaly_task) { :repack_incremental }
+
+ before do
+ expect(subject).to receive(:get_lease_uuid).and_return(lease_uuid)
+ end
+
+ it_behaves_like 'it calls Gitaly'
+ it_behaves_like 'it updates the resource statistics' if update_statistics
+ end
+
+ shared_examples 'gc tasks' do
+ before do
+ allow(subject).to receive(:get_lease_uuid).and_return(lease_uuid)
+ allow(subject).to receive(:bitmaps_enabled?).and_return(bitmaps_enabled)
+ end
+
+ it 'incremental repack adds a new packfile' do
+ create_objects(resource)
+ before_packs = packs(resource)
+
+ expect(before_packs.count).to be >= 1
+
+ subject.perform(resource.id, 'incremental_repack', lease_key, lease_uuid)
+ after_packs = packs(resource)
+
+ # Exactly one new pack should have been created
+ expect(after_packs.count).to eq(before_packs.count + 1)
+
+ # Previously existing packs are still around
+ expect(before_packs & after_packs).to eq(before_packs)
+ end
+
+ it 'full repack consolidates into 1 packfile' do
+ create_objects(resource)
+ subject.perform(resource.id, 'incremental_repack', lease_key, lease_uuid)
+ before_packs = packs(resource)
+
+ expect(before_packs.count).to be >= 2
+
+ subject.perform(resource.id, 'full_repack', lease_key, lease_uuid)
+ after_packs = packs(resource)
+
+ expect(after_packs.count).to eq(1)
+
+ # Previously existing packs should be gone now
+ expect(after_packs - before_packs).to eq(after_packs)
+
+ expect(File.exist?(bitmap_path(after_packs.first))).to eq(bitmaps_enabled)
+ end
+
+ it 'gc consolidates into 1 packfile and updates packed-refs' do
+ create_objects(resource)
+ before_packs = packs(resource)
+ before_packed_refs = packed_refs(resource)
+
+ expect(before_packs.count).to be >= 1
+
+ # It's quite difficult to use `expect_next_instance_of` in this place
+ # because the RepositoryService is instantiated several times to do
+ # some repository calls like `exists?`, `create_repository`, ... .
+ # Therefore, since we're instantiating the object several times,
+ # RSpec has troubles figuring out which instance is the next and which
+ # one we want to mock.
+ # Besides, at this point, we actually want to perform the call to Gitaly,
+ # otherwise we would just use `instance_double` like in other parts of the
+ # spec file.
+ expect_any_instance_of(Gitlab::GitalyClient::RepositoryService) # rubocop:disable RSpec/AnyInstanceOf
+ .to receive(:garbage_collect)
+ .with(bitmaps_enabled, prune: false)
+ .and_call_original
+
+ subject.perform(resource.id, 'gc', lease_key, lease_uuid)
+ after_packed_refs = packed_refs(resource)
+ after_packs = packs(resource)
+
+ expect(after_packs.count).to eq(1)
+
+ # Previously existing packs should be gone now
+ expect(after_packs - before_packs).to eq(after_packs)
+
+ # The packed-refs file should have been updated during 'git gc'
+ expect(before_packed_refs).not_to eq(after_packed_refs)
+
+ expect(File.exist?(bitmap_path(after_packs.first))).to eq(bitmaps_enabled)
+ end
+
+ it 'cleans up repository after finishing' do
+ expect(resource).to receive(:cleanup).and_call_original
+
+ subject.perform(resource.id, 'gc', lease_key, lease_uuid)
+ end
+
+ it 'prune calls garbage_collect with the option prune: true' do
+ repository_service = instance_double(Gitlab::GitalyClient::RepositoryService)
+
+ expect(subject).to receive(:get_gitaly_client).with(:prune, repository.raw_repository).and_return(repository_service)
+ expect(repository_service).to receive(:garbage_collect).with(bitmaps_enabled, prune: true)
+
+ subject.perform(resource.id, 'prune', lease_key, lease_uuid)
+ end
+
+ # Create a new commit on a random new branch
+ def create_objects(resource)
+ rugged = rugged_repo(resource.repository)
+ old_commit = rugged.branches.first.target
+ new_commit_sha = Rugged::Commit.create(
+ rugged,
+ message: "hello world #{SecureRandom.hex(6)}",
+ author: { email: 'foo@bar', name: 'baz' },
+ committer: { email: 'foo@bar', name: 'baz' },
+ tree: old_commit.tree,
+ parents: [old_commit]
+ )
+ rugged.references.create("refs/heads/#{SecureRandom.hex(6)}", new_commit_sha)
+ end
+
+ def packs(resource)
+ Dir["#{path_to_repo}/objects/pack/*.pack"]
+ end
+
+ def packed_refs(resource)
+ path = File.join(path_to_repo, 'packed-refs')
+ FileUtils.touch(path)
+ File.read(path)
+ end
+
+ def path_to_repo
+ @path_to_repo ||= File.join(TestEnv.repos_path, resource.repository.relative_path)
+ end
+
+ def bitmap_path(pack)
+ pack.sub(/\.pack\z/, '.bitmap')
+ end
+ end
+
+ context 'with bitmaps enabled' do
+ let(:bitmaps_enabled) { true }
+
+ include_examples 'gc tasks'
+ end
+
+ context 'with bitmaps disabled' do
+ let(:bitmaps_enabled) { false }
+
+ include_examples 'gc tasks'
+ end
+ end
+end