diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-02-18 10:34:06 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-02-18 10:34:06 +0000 |
commit | 859a6fb938bb9ee2a317c46dfa4fcc1af49608f0 (patch) | |
tree | d7f2700abe6b4ffcb2dcfc80631b2d87d0609239 /spec/support/shared_examples | |
parent | 446d496a6d000c73a304be52587cd9bbc7493136 (diff) | |
download | gitlab-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')
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 |