diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-12-20 14:22:11 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-12-20 14:22:11 +0000 |
commit | 0c872e02b2c822e3397515ec324051ff540f0cd5 (patch) | |
tree | ce2fb6ce7030e4dad0f4118d21ab6453e5938cdd /spec/support/shared_examples/requests | |
parent | f7e05a6853b12f02911494c4b3fe53d9540d74fc (diff) | |
download | gitlab-ce-f7d6ced4f57b4920a666336f5a44d098faade2ea.tar.gz |
Add latest changes from gitlab-org/gitlab@15-7-stable-eev15.7.0-rc42
Diffstat (limited to 'spec/support/shared_examples/requests')
14 files changed, 760 insertions, 103 deletions
diff --git a/spec/support/shared_examples/requests/api/conan_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/conan_packages_shared_examples.rb index 629d93676eb..c76bc7c3107 100644 --- a/spec/support/shared_examples/requests/api/conan_packages_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/conan_packages_shared_examples.rb @@ -593,7 +593,7 @@ RSpec.shared_examples 'delete package endpoint' do project.add_maintainer(user) end - it_behaves_like 'a gitlab tracking event', 'API::ConanPackages', 'delete_package' + it_behaves_like 'a package tracking event', 'API::ConanPackages', 'delete_package' it 'deletes a package' do expect { subject }.to change { Packages::Package.count }.from(2).to(1) @@ -708,7 +708,7 @@ RSpec.shared_examples 'package file download endpoint' do context 'tracking the conan_package.tgz download' do let(:package_file) { package.package_files.find_by(file_name: ::Packages::Conan::FileMetadatum::PACKAGE_BINARY) } - it_behaves_like 'a gitlab tracking event', 'API::ConanPackages', 'pull_package' + it_behaves_like 'a package tracking event', 'API::ConanPackages', 'pull_package' end end @@ -781,7 +781,7 @@ RSpec.shared_examples 'workhorse package file upload endpoint' do context 'tracking the conan_package.tgz upload' do let(:file_name) { ::Packages::Conan::FileMetadatum::PACKAGE_BINARY } - it_behaves_like 'a gitlab tracking event', 'API::ConanPackages', 'push_package' + it_behaves_like 'a package tracking event', 'API::ConanPackages', 'push_package' end end @@ -849,12 +849,6 @@ RSpec.shared_examples 'uploads a package file' do expect(package_file.file_name).to eq(params[:file].original_filename) end - it "doesn't attempt to migrate file to object storage" do - expect(ObjectStorage::BackgroundMoveWorker).not_to receive(:perform_async) - - subject - end - context 'with existing package' do let!(:existing_package) { create(:conan_package, name: 'foo', version: 'bar', project: project) } @@ -936,8 +930,6 @@ RSpec.shared_examples 'uploads a package file' do end end end - - it_behaves_like 'background upload schedules a file migration' end end diff --git a/spec/support/shared_examples/requests/api/graphql/issue_list_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/issue_list_shared_examples.rb index 5469fd80a4f..d4479e462af 100644 --- a/spec/support/shared_examples/requests/api/graphql/issue_list_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/graphql/issue_list_shared_examples.rb @@ -1,6 +1,15 @@ # frozen_string_literal: true RSpec.shared_examples 'graphql issue list request spec' do + let(:issue_ids) { graphql_dig_at(issues_data, :id) } + let(:fields) do + <<~QUERY + nodes { + #{all_graphql_fields_for('issues'.classify)} + } + QUERY + end + it_behaves_like 'a working graphql query' do before do post_query @@ -109,10 +118,57 @@ RSpec.shared_examples 'graphql issue list request spec' do let(:ids) { issue_ids } end end + + context 'when filtering by confidentiality' do + context 'when fetching confidential issues' do + let(:issue_filter_params) { { confidential: true } } + + it 'returns only confidential issues' do + post_query + + expect(issue_ids).to match_array(to_gid_list(confidential_issues)) + end + + context 'when user cannot see confidential issues' do + it 'returns an empty list' do + post_query(external_user) + + expect(issue_ids).to be_empty + end + end + end + + context 'when fetching non-confidential issues' do + let(:issue_filter_params) { { confidential: false } } + + it 'returns only non-confidential issues' do + post_query + + expect(issue_ids).to match_array(to_gid_list(non_confidential_issues)) + end + + context 'when user cannot see confidential issues' do + it 'returns an empty list' do + post_query(external_user) + + expect(issue_ids).to match_array(to_gid_list(public_non_confidential_issues)) + end + end + end + end end describe 'sorting and pagination' do context 'when sorting by severity' do + let(:expected_severity_sorted_asc) { [issue_c, issue_a, issue_b, issue_e, issue_d] } + + before_all do + create(:issuable_severity, issue: issue_a, severity: :unknown) + create(:issuable_severity, issue: issue_b, severity: :low) + create(:issuable_severity, issue: issue_d, severity: :critical) + create(:issuable_severity, issue: issue_e, severity: :high) + end + context 'when ascending' do it_behaves_like 'sorted paginated query' do let(:sort_param) { :SEVERITY_ASC } @@ -147,6 +203,459 @@ RSpec.shared_examples 'graphql issue list request spec' do end end end + + context 'when sorting by due date' do + context 'when ascending' do + it_behaves_like 'sorted paginated query' do + let(:sort_param) { :DUE_DATE_ASC } + let(:first_param) { 2 } + let(:all_records) { to_gid_list(expected_due_date_sorted_asc) } + end + end + + context 'when descending' do + it_behaves_like 'sorted paginated query' do + let(:sort_param) { :DUE_DATE_DESC } + let(:first_param) { 2 } + let(:all_records) { to_gid_list(expected_due_date_sorted_desc) } + end + end + end + + context 'when sorting by relative position' do + context 'when ascending' do + it_behaves_like 'sorted paginated query', is_reversible: true do + let(:sort_param) { :RELATIVE_POSITION_ASC } + let(:first_param) { 2 } + let(:all_records) { to_gid_list(expected_relative_position_sorted_asc) } + end + end + end + + context 'when sorting by label priority' do + context 'when ascending' do + it_behaves_like 'sorted paginated query' do + let(:sort_param) { :LABEL_PRIORITY_ASC } + let(:first_param) { 2 } + let(:all_records) { to_gid_list(expected_label_priority_sorted_asc) } + end + end + + context 'when descending' do + it_behaves_like 'sorted paginated query' do + let(:sort_param) { :LABEL_PRIORITY_DESC } + let(:first_param) { 2 } + let(:all_records) { to_gid_list(expected_label_priority_sorted_desc) } + end + end + end + + context 'when sorting by milestone due date' do + context 'when ascending' do + it_behaves_like 'sorted paginated query' do + let(:sort_param) { :MILESTONE_DUE_ASC } + let(:first_param) { 2 } + let(:all_records) { to_gid_list(expected_milestone_sorted_asc) } + end + end + + context 'when descending' do + it_behaves_like 'sorted paginated query' do + let(:sort_param) { :MILESTONE_DUE_DESC } + let(:first_param) { 2 } + let(:all_records) { to_gid_list(expected_milestone_sorted_desc) } + end + end + end + end + + describe 'N+1 query checks' do + let(:extra_iid_for_second_query) { issue_b.iid.to_s } + let(:search_params) { { iids: [issue_a.iid.to_s] } } + let(:issue_filter_params) { search_params } + let(:fields) do + <<~QUERY + nodes { + id + #{requested_fields} + } + QUERY + end + + def execute_query + post_query + end + + context 'when requesting `user_notes_count` and `user_discussions_count`' do + let(:requested_fields) { 'userNotesCount userDiscussionsCount' } + + before do + create_list(:note_on_issue, 2, noteable: issue_a, project: issue_a.project) + create(:note_on_issue, noteable: issue_b, project: issue_b.project) + end + + include_examples 'N+1 query check' + end + + context 'when requesting `merge_requests_count`' do + let(:requested_fields) { 'mergeRequestsCount' } + + before do + create_list(:merge_requests_closing_issues, 2, issue: issue_a) + create_list(:merge_requests_closing_issues, 3, issue: issue_b) + end + + include_examples 'N+1 query check' + end + + context 'when requesting `timelogs`' do + let(:requested_fields) { 'timelogs { nodes { timeSpent } }' } + + before do + create_list(:issue_timelog, 2, issue: issue_a) + create(:issue_timelog, issue: issue_b) + end + + include_examples 'N+1 query check' + end + + context 'when requesting `closed_as_duplicate_of`' do + let(:requested_fields) { 'closedAsDuplicateOf { id }' } + let(:issue_a_dup) { create(:issue, project: issue_a.project) } + let(:issue_b_dup) { create(:issue, project: issue_b.project) } + + before do + issue_a.update!(duplicated_to_id: issue_a_dup) + issue_b.update!(duplicated_to_id: issue_a_dup) + end + + include_examples 'N+1 query check' + end + + context 'when award emoji votes' do + let(:requested_fields) { 'upvotes downvotes' } + + before do + create_list(:award_emoji, 2, name: 'thumbsup', awardable: issue_a) + create_list(:award_emoji, 2, name: 'thumbsdown', awardable: issue_b) + end + + include_examples 'N+1 query check' + end + + context 'when requesting participants' do + let(:search_params) { { iids: [issue_a.iid.to_s, issue_c.iid.to_s] } } + let(:requested_fields) { 'participants { nodes { name } }' } + + before do + create(:award_emoji, :upvote, awardable: issue_a) + create(:award_emoji, :upvote, awardable: issue_b) + create(:award_emoji, :upvote, awardable: issue_c) + + note_with_emoji_a = create(:note_on_issue, noteable: issue_a, project: issue_a.project) + note_with_emoji_b = create(:note_on_issue, noteable: issue_b, project: issue_b.project) + note_with_emoji_c = create(:note_on_issue, noteable: issue_c, project: issue_c.project) + + create(:award_emoji, :upvote, awardable: note_with_emoji_a) + create(:award_emoji, :upvote, awardable: note_with_emoji_b) + create(:award_emoji, :upvote, awardable: note_with_emoji_c) + end + + # Executes 3 extra queries to fetch participant_attrs + include_examples 'N+1 query check', threshold: 3 + end + + context 'when requesting labels', :use_sql_query_cache do + let(:requested_fields) { 'labels { nodes { id } }' } + let(:extra_iid_for_second_query) { same_project_issue2.iid.to_s } + let(:search_params) { { iids: [same_project_issue1.iid.to_s] } } + + before do + current_project = same_project_issue1.project + project_labels = create_list(:label, 2, project: current_project) + group_labels = create_list(:group_label, 2, group: current_project.group) + + same_project_issue1.update!(labels: [project_labels.first, group_labels.first].flatten) + same_project_issue2.update!(labels: [project_labels, group_labels].flatten) + end + + include_examples 'N+1 query check', skip_cached: false + end + end + + context 'when confidential issues exist' do + context 'when user can see confidential issues' do + it 'includes confidential issues' do + post_query + + all_issues = confidential_issues + non_confidential_issues + + expect(issue_ids).to match_array(to_gid_list(all_issues)) + expect(issues_data.pluck('confidential')).to match_array(all_issues.map(&:confidential)) + end + end + + context 'when user cannot see confidential issues' do + let(:current_user) { external_user } + + it 'does not include confidential issues' do + post_query + + expect(issue_ids).to match_array(to_gid_list(public_non_confidential_issues)) + end + end + end + + context 'when limiting the number of results' do + let(:issue_limit) { 1 } + let(:issue_filter_params) { { first: issue_limit } } + + it_behaves_like 'a working graphql query' do + before do + post_query + end + + it 'only returns N issues' do + expect(issues_data.size).to eq(issue_limit) + end + end + + context 'when no limit is provided' do + let(:issue_limit) { nil } + + it 'returns all issues' do + post_query + + expect(issues_data.size).to be > 1 + end + end + + it 'is expected to check permissions on the first issue only' do + allow(Ability).to receive(:allowed?).and_call_original + # Newest first, we only want to see the newest checked + expect(Ability).not_to receive(:allowed?).with(current_user, :read_issue, issues.first) + + post_query + end + end + + context 'when the user does not have access to the issue' do + let(:current_user) { external_user } + + it 'returns no issues' do + public_projects.each do |public_project| + public_project.project_feature.update!(issues_access_level: ProjectFeature::PRIVATE) + end + + post_query + + expect(issues_data).to eq([]) + end + end + + context 'when fetching escalation status' do + let_it_be(:escalation_status) { create(:incident_management_issuable_escalation_status, issue: issue_a) } + + let(:fields) do + <<~QUERY + nodes { + id + escalationStatus + } + QUERY + end + + before do + issue_a.update_columns(issue_type: Issue.issue_types[:incident]) + end + + it 'returns the escalation status values' do + post_query + + statuses = issues_data.pluck('escalationStatus') + + expect(statuses).to contain_exactly(escalation_status.status_name.upcase.to_s, nil, nil, nil, nil) + end + + it 'avoids N+1 queries', :aggregate_failures do + control = ActiveRecord::QueryRecorder.new { run_with_clean_state(query, context: { current_user: current_user }) } + + new_incident = create(:incident, project: public_projects.first) + create(:incident_management_issuable_escalation_status, issue: new_incident) + + expect { run_with_clean_state(query, context: { current_user: current_user }) }.not_to exceed_query_limit(control) + end + end + + context 'when fetching alert management alert' do + let(:fields) do + <<~QUERY + nodes { + iid + alertManagementAlert { + title + } + alertManagementAlerts { + nodes { + title + } + } + } + QUERY + end + + it 'avoids N+1 queries' do + control = ActiveRecord::QueryRecorder.new { post_query } + + create(:alert_management_alert, :with_incident, project: public_projects.first) + + expect { post_query }.not_to exceed_query_limit(control) + end + + it 'returns the alert data' do + post_query + + alert_titles = issues_data.map { |issue| issue.dig('alertManagementAlert', 'title') } + expected_titles = issues.map { |issue| issue.alert_management_alerts.first&.title } + + expect(alert_titles).to contain_exactly(*expected_titles) + end + + it 'returns the alerts data' do + post_query + + alert_titles = issues_data.map { |issue| issue.dig('alertManagementAlerts', 'nodes') } + expected_titles = issues.map do |issue| + issue.alert_management_alerts.map { |alert| { 'title' => alert.title } } + end + + expect(alert_titles).to contain_exactly(*expected_titles) + end + end + + context 'when fetching customer_relations_contacts' do + let(:fields) do + <<~QUERY + nodes { + id + customerRelationsContacts { + nodes { + firstName + } + } + } + QUERY + end + + def clean_state_query + run_with_clean_state(query, context: { current_user: current_user }) + end + + it 'avoids N+1 queries' do + create(:issue_customer_relations_contact, :for_issue, issue: issue_a) + + control = ActiveRecord::QueryRecorder.new(skip_cached: false) { clean_state_query } + + create(:issue_customer_relations_contact, :for_issue, issue: issue_a) + + expect { clean_state_query }.not_to exceed_all_query_limit(control) + end + end + + context 'when fetching labels' do + let(:fields) do + <<~QUERY + nodes { + id + labels { + nodes { + id + } + } + } + QUERY + end + + before do + issues.each do |issue| + # create a label for each issue we have to properly test N+1 + label = create(:label, project: issue.project) + issue.update!(labels: [label]) + end + end + + def response_label_ids(response_data) + response_data.map do |node| + node['labels']['nodes'].pluck('id') + end.flatten + end + + def labels_as_global_ids(issues) + issues.map(&:labels).flatten.map(&:to_global_id).map(&:to_s) + end + + it 'avoids N+1 queries', :aggregate_failures do + control = ActiveRecord::QueryRecorder.new { post_query } + expect(issues_data.count).to eq(5) + expect(response_label_ids(issues_data)).to match_array(labels_as_global_ids(issues)) + + public_project = public_projects.first + new_issues = issues + [ + create(:issue, project: public_project, labels: [create(:label, project: public_project)]) + ] + + expect { post_query }.not_to exceed_query_limit(control) + + expect(issues_data.count).to eq(6) + expect(response_label_ids(issues_data)).to match_array(labels_as_global_ids(new_issues)) + end + end + + context 'when fetching assignees' do + let(:fields) do + <<~QUERY + nodes { + id + assignees { + nodes { + id + } + } + } + QUERY + end + + before do + issues.each do |issue| + # create an assignee for each issue we have to properly test N+1 + assignee = create(:user) + issue.update!(assignees: [assignee]) + end + end + + def response_assignee_ids(response_data) + response_data.map do |node| + node['assignees']['nodes'].pluck('id') + end.flatten + end + + def assignees_as_global_ids(issues) + issues.map(&:assignees).flatten.map(&:to_global_id).map(&:to_s) + end + + it 'avoids N+1 queries', :aggregate_failures do + control = ActiveRecord::QueryRecorder.new { post_query } + expect(issues_data.count).to eq(5) + expect(response_assignee_ids(issues_data)).to match_array(assignees_as_global_ids(issues)) + + public_project = public_projects.first + new_issues = issues + [create(:issue, project: public_project, assignees: [create(:user)])] + + expect { post_query }.not_to exceed_query_limit(control) + + expect(issues_data.count).to eq(6) + expect(response_assignee_ids(issues_data)).to match_array(assignees_as_global_ids(new_issues)) + end end it 'includes a web_url' do @@ -167,4 +676,8 @@ RSpec.shared_examples 'graphql issue list request spec' do def to_gid_list(instance_list) instance_list.map { |instance| instance.to_gid.to_s } end + + def issues_data + graphql_data.dig(*issue_nodes_path) + end end diff --git a/spec/support/shared_examples/requests/api/graphql/packages/group_and_project_packages_list_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/packages/group_and_project_packages_list_shared_examples.rb index fb4aacfd7a9..f5835460a77 100644 --- a/spec/support/shared_examples/requests/api/graphql/packages/group_and_project_packages_list_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/graphql/packages/group_and_project_packages_list_shared_examples.rb @@ -62,6 +62,21 @@ RSpec.shared_examples 'group and project packages query' do it 'returns the count of the packages' do expect(packages_count).to eq(4) end + + context '_links' do + let_it_be(:errored_package) { create(:maven_package, :error, project: project1) } + + let(:package_web_paths) { graphql_data_at(resource_type, :packages, :nodes, :_links, :web_path) } + + it 'does not contain the web path of errored package' do + expect(package_web_paths.compact).to contain_exactly( + "/#{project1.full_path}/-/packages/#{npm_package.id}", + "/#{project1.full_path}/-/packages/#{maven_package.id}", + "/#{project2.full_path}/-/packages/#{debian_package.id}", + "/#{project2.full_path}/-/packages/#{composer_package.id}" + ) + end + end end context 'when the user does not have access to the resource' do @@ -139,7 +154,7 @@ RSpec.shared_examples 'group and project packages query' do end it 'throws an error' do - expect_graphql_errors_to_include(/Argument \'sort\' on Field \'packages\' has an invalid value/) + expect_graphql_errors_to_include(/Argument 'sort' on Field 'packages' has an invalid value/) end end diff --git a/spec/support/shared_examples/requests/api/graphql/projects/branch_protections/access_level_request_examples.rb b/spec/support/shared_examples/requests/api/graphql/projects/branch_protections/access_level_request_examples.rb index 54cc13fac94..6b4d8cae2ce 100644 --- a/spec/support/shared_examples/requests/api/graphql/projects/branch_protections/access_level_request_examples.rb +++ b/spec/support/shared_examples/requests/api/graphql/projects/branch_protections/access_level_request_examples.rb @@ -1,13 +1,13 @@ # frozen_string_literal: true -RSpec.shared_examples 'perform graphql requests for AccessLevel type objects' do |access_level_kind| +RSpec.shared_examples 'a GraphQL query for access levels' do |access_level_kind| include GraphqlHelpers let_it_be(:project) { create(:project) } let_it_be(:current_user) { create(:user, maintainer_projects: [project]) } let_it_be(:variables) { { path: project.full_path } } - let(:fields) { all_graphql_fields_for("#{access_level_kind.to_s.classify}AccessLevel", max_depth: 2) } + let(:fields) { all_graphql_fields_for("#{access_level_kind.to_s.classify}AccessLevel") } let(:access_levels) { protected_branch.public_send("#{access_level_kind}_access_levels") } let(:access_levels_count) { access_levels.size } let(:maintainer_access_level) { access_levels.for_role.first } @@ -61,17 +61,35 @@ RSpec.shared_examples 'perform graphql requests for AccessLevel type objects' do create(:protected_branch, "maintainers_can_#{access_level_kind}", project: project) end - before do - post_graphql(query, current_user: current_user, variables: variables) + describe 'query' do + it 'avoids N+1 queries' do + control = ActiveRecord::QueryRecorder.new do + post_graphql(query, current_user: current_user, variables: variables) + end + expect_graphql_errors_to_be_empty + + create("protected_branch_#{access_level_kind}_access_level", protected_branch: protected_branch) + + expect do + post_graphql(query, current_user: current_user, variables: variables) + end.not_to exceed_all_query_limit(control) + expect_graphql_errors_to_be_empty + end end - it_behaves_like 'a working graphql query' + describe 'response' do + before do + post_graphql(query, current_user: current_user, variables: variables) + end + + it_behaves_like 'a working graphql query' - it 'returns all the access level attributes' do - expect(maintainer_access_level_data['accessLevel']).to eq(maintainer_access_level.access_level) - expect(maintainer_access_level_data['accessLevelDescription']).to eq(maintainer_access_level.humanize) - expect(maintainer_access_level_data.dig('group', 'name')).to be_nil - expect(maintainer_access_level_data.dig('user', 'name')).to be_nil + it 'returns all the access level attributes' do + expect(maintainer_access_level_data['accessLevel']).to eq(maintainer_access_level.access_level) + expect(maintainer_access_level_data['accessLevelDescription']).to eq(maintainer_access_level.humanize) + expect(maintainer_access_level_data.dig('group', 'name')).to be_nil + expect(maintainer_access_level_data.dig('user', 'name')).to be_nil + end end end end diff --git a/spec/support/shared_examples/requests/api/helm_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/helm_packages_shared_examples.rb index 8bf6b162508..7803f0ff04d 100644 --- a/spec/support/shared_examples/requests/api/helm_packages_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/helm_packages_shared_examples.rb @@ -156,25 +156,13 @@ RSpec.shared_examples 'process helm upload' do |user_type, status| end context 'and direct upload disabled' do - context 'and background upload disabled' do - let(:fog_connection) do - stub_package_file_object_storage(direct_upload: false, background_upload: false) - end - - it_behaves_like 'creates helm package files' + let(:fog_connection) do + stub_package_file_object_storage(direct_upload: false) end - context 'and background upload enabled' do - let(:fog_connection) do - stub_package_file_object_storage(direct_upload: false, background_upload: true) - end - - it_behaves_like 'creates helm package files' - end + it_behaves_like 'creates helm package files' end end - - it_behaves_like 'background upload schedules a file migration' 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 11f9565989f..efe5ed3bcf9 100644 --- a/spec/support/shared_examples/requests/api/notes_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/notes_shared_examples.rb @@ -159,7 +159,7 @@ RSpec.shared_examples 'noteable API' do |parent_type, noteable_type, id_name| expect do post api(uri, user), params: { body: 'hi!' } - end.to change(Event, :count).by(1) + end.to change { Event.count }.by(1) end context 'setting created_at' 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 85ac2b5e1ea..b55639a6b82 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 @@ -323,6 +323,171 @@ RSpec.shared_examples 'handling get metadata requests' do |scope: :project| end end +RSpec.shared_examples 'handling audit request' do |path:, scope: :project| + using RSpec::Parameterized::TableSyntax + + let(:headers) { {} } + let(:params) do + ActiveSupport::Gzip.compress( + Gitlab::Json.dump({ + '@gitlab-org/npm-test': ['1.0.6'], + 'call-bind': ['1.0.2'] + }) + ) + end + + let(:default_headers) do + { 'HTTP_CONTENT_ENCODING' => 'gzip', 'CONTENT_TYPE' => 'application/json' } + end + + subject { post(url, headers: headers.merge(default_headers), params: params) } + + shared_examples 'accept audit request' do |status:| + it 'accepts the audit request' do + subject + + expect(response).to have_gitlab_http_status(status) + expect(response.media_type).to eq('application/json') + expect(json_response).to eq([]) + end + end + + shared_examples 'reject audit request' do |status:| + it 'rejects the audit request' do + subject + + expect(response).to have_gitlab_http_status(status) + end + end + + shared_examples 'redirect audit request' do |status:| + it 'redirects audit request' do + subject + + expect(response).to have_gitlab_http_status(status) + expect(response.headers['Location']).to eq("https://registry.npmjs.org/-/npm/v1/security/#{path}") + end + end + + shared_examples 'handling all conditions' do + include_context 'dependency proxy helpers context' + + where(:auth, :request_forward, :visibility, :user_role, :expected_result, :expected_status) do + nil | true | :public | nil | :reject | :unauthorized + nil | false | :public | nil | :reject | :unauthorized + nil | true | :private | nil | :reject | :unauthorized + nil | false | :private | nil | :reject | :unauthorized + nil | true | :internal | nil | :reject | :unauthorized + nil | false | :internal | nil | :reject | :unauthorized + + :oauth | true | :public | :guest | :redirect | :temporary_redirect + :oauth | true | :public | :reporter | :redirect | :temporary_redirect + :oauth | false | :public | :guest | :accept | :ok + :oauth | false | :public | :reporter | :accept | :ok + :oauth | true | :private | :reporter | :redirect | :temporary_redirect + :oauth | false | :private | :guest | :reject | :forbidden + :oauth | false | :private | :reporter | :accept | :ok + :oauth | true | :private | :guest | :redirect | :temporary_redirect + :oauth | true | :internal | :guest | :redirect | :temporary_redirect + :oauth | true | :internal | :reporter | :redirect | :temporary_redirect + :oauth | false | :internal | :guest | :accept | :ok + :oauth | false | :internal | :reporter | :accept | :ok + + :personal_access_token | true | :public | :guest | :redirect | :temporary_redirect + :personal_access_token | true | :public | :reporter | :redirect | :temporary_redirect + :personal_access_token | false | :public | :guest | :accept | :ok + :personal_access_token | false | :public | :reporter | :accept | :ok + :personal_access_token | true | :private | :guest | :redirect | :temporary_redirect + :personal_access_token | true | :private | :reporter | :redirect | :temporary_redirect + :personal_access_token | false | :private | :guest | :reject | :forbidden # instance might fail + :personal_access_token | false | :private | :reporter | :accept | :ok + :personal_access_token | true | :internal | :guest | :redirect | :temporary_redirect + :personal_access_token | true | :internal | :reporter | :redirect | :temporary_redirect + :personal_access_token | false | :internal | :guest | :accept | :ok + :personal_access_token | false | :internal | :reporter | :accept | :ok + + :job_token | true | :public | :developer | :redirect | :temporary_redirect + :job_token | false | :public | :developer | :accept | :ok + :job_token | true | :private | :developer | :redirect | :temporary_redirect + :job_token | false | :private | :developer | :accept | :ok + :job_token | true | :internal | :developer | :redirect | :temporary_redirect + :job_token | false | :internal | :developer | :accept | :ok + + :deploy_token | true | :public | nil | :redirect | :temporary_redirect + :deploy_token | false | :public | nil | :accept | :ok + :deploy_token | true | :private | nil | :redirect | :temporary_redirect + :deploy_token | false | :private | nil | :accept | :ok + :deploy_token | true | :internal | nil | :redirect | :temporary_redirect + :deploy_token | false | :internal | nil | :accept | :ok + end + + with_them do + let(:headers) do + case auth + when :oauth + build_token_auth_header(token.plaintext_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 + + before do + project.send("add_#{user_role}", user) if user_role + project.update!(visibility: visibility.to_s) + + if scope == :instance + allow_fetch_application_setting(attribute: "npm_package_requests_forwarding", return_value: request_forward) + else + allow_fetch_cascade_application_setting(attribute: "npm_package_requests_forwarding", return_value: request_forward) + end + end + + example_name = "#{params[:expected_result]} audit request" + status = params[:expected_status] + + if scope == :instance && params[:expected_status] != :unauthorized + if params[:request_forward] + example_name = 'redirect audit request' + status = :temporary_redirect + else + example_name = 'reject audit request' + status = :not_found + end + end + + it_behaves_like example_name, status: status + end + end + + context 'with a group namespace' do + it_behaves_like 'handling all conditions' + end + + context 'with a developer' do + let(:headers) { build_token_auth_header(personal_access_token.token) } + + before do + project.add_developer(user) + end + + context 'with a job token' do + let(:headers) { build_token_auth_header(job.token) } + + before do + job.update!(status: :success) + end + + it_behaves_like 'reject audit 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' 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 fdd55893deb..bace570e47a 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 @@ -224,25 +224,13 @@ RSpec.shared_examples 'process nuget upload' do |user_type, status, add_member = end context 'and direct upload disabled' do - context 'and background upload disabled' do - let(:fog_connection) do - stub_package_file_object_storage(direct_upload: false, background_upload: false) - end - - it_behaves_like 'creates nuget package files' + let(:fog_connection) do + stub_package_file_object_storage(direct_upload: false) end - context 'and background upload enabled' do - let(:fog_connection) do - stub_package_file_object_storage(direct_upload: false, background_upload: true) - end - - it_behaves_like 'creates nuget package files' - end + it_behaves_like 'creates nuget package files' end end - - it_behaves_like 'background upload schedules a file migration' end end @@ -507,7 +495,7 @@ RSpec.shared_examples 'nuget upload endpoint' do |symbol_package: false| let(:token) { user_token ? personal_access_token.token : 'wrong' } let(:user_headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) } let(:headers) { user_headers.merge(workhorse_headers) } - let(:snowplow_gitlab_standard_context) { { project: project, user: user, namespace: project.namespace } } + let(:snowplow_gitlab_standard_context) { { project: project, user: user, namespace: project.namespace, property: 'i_package_nuget_user' } } before do update_visibility_to(Gitlab::VisibilityLevel.const_get(visibility_level, false)) 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 860cb1b1d86..98264baa61d 100644 --- a/spec/support/shared_examples/requests/api/packages_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/packages_shared_examples.rb @@ -142,15 +142,26 @@ RSpec.shared_examples 'job token for package uploads' do |authorize_endpoint: fa end end -RSpec.shared_examples 'a package tracking event' do |category, action| +RSpec.shared_examples 'a package tracking event' do |category, action, service_ping_context = true| before do stub_feature_flags(collect_package_events: true) end + let(:context) do + [Gitlab::Tracking::ServicePingContext.new(data_source: :redis_hll, + event: snowplow_gitlab_standard_context[:property]).to_h] + end + it "creates a gitlab tracking event #{action}", :snowplow, :aggregate_failures do expect { subject }.to change { Packages::Event.count }.by(1) - expect_snowplow_event(category: category, action: action, **snowplow_gitlab_standard_context) + if service_ping_context + expect_snowplow_event(category: category, action: action, + label: "redis_hll_counters.user_packages.user_packages_total_unique_counts_monthly", + context: context, **snowplow_gitlab_standard_context) + else + expect_snowplow_event(category: category, action: action, **snowplow_gitlab_standard_context) + end end end diff --git a/spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb index a9b44015206..a267476b7cb 100644 --- a/spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb @@ -95,25 +95,13 @@ RSpec.shared_examples 'PyPI package creation' do |user_type, status, add_member end context 'and direct upload disabled' do - context 'and background upload disabled' do - let(:fog_connection) do - stub_package_file_object_storage(direct_upload: false, background_upload: false) - end - - it_behaves_like 'creating pypi package files' + let(:fog_connection) do + stub_package_file_object_storage(direct_upload: false) end - context 'and background upload enabled' do - let(:fog_connection) do - stub_package_file_object_storage(direct_upload: false, background_upload: true) - end - - it_behaves_like 'creating pypi package files' - end + it_behaves_like 'creating pypi package files' end end - - it_behaves_like 'background upload schedules a file migration' end end @@ -285,7 +273,7 @@ RSpec.shared_examples 'pypi simple API endpoint' do let(:url) { "/projects/#{project.id}/packages/pypi/simple/my-package" } let(:headers) { basic_auth_header(user.username, personal_access_token.token) } - let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace } } + let(:snowplow_gitlab_standard_context) { { project: project, namespace: group, property: 'i_package_pypi_user' } } it_behaves_like 'PyPI package versions', :developer, :success end 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 2d036cb2aa3..2154a76d765 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 @@ -71,11 +71,7 @@ RSpec.shared_examples 'repository_storage_moves API' do |container_type| get_container_repository_storage_moves json_ids = json_response.map { |storage_move| storage_move['id'] } - expect(json_ids).to eq([ - storage_move.id, - storage_move_middle.id, - storage_move_oldest.id - ]) + expect(json_ids).to eq([storage_move.id, storage_move_middle.id, storage_move_oldest.id]) end describe 'permissions' do diff --git a/spec/support/shared_examples/requests/api/rubygems_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/rubygems_packages_shared_examples.rb index f075927e7bf..da09d70c777 100644 --- a/spec/support/shared_examples/requests/api/rubygems_packages_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/rubygems_packages_shared_examples.rb @@ -122,21 +122,11 @@ RSpec.shared_examples 'process rubygems upload' do |user_type, status, add_membe end context 'and direct upload disabled' do - context 'and background upload disabled' do - let(:fog_connection) do - stub_package_file_object_storage(direct_upload: false, background_upload: false) - end - - it_behaves_like 'creates rubygems package files' + let(:fog_connection) do + stub_package_file_object_storage(direct_upload: false) end - context 'and background upload enabled' do - let(:fog_connection) do - stub_package_file_object_storage(direct_upload: false, background_upload: true) - end - - it_behaves_like 'creates rubygems package files' - end + it_behaves_like 'creates rubygems package files' end end end diff --git a/spec/support/shared_examples/requests/api/terraform/modules/v1/packages_shared_examples.rb b/spec/support/shared_examples/requests/api/terraform/modules/v1/packages_shared_examples.rb index bdff2c65691..ae2855083f6 100644 --- a/spec/support/shared_examples/requests/api/terraform/modules/v1/packages_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/terraform/modules/v1/packages_shared_examples.rb @@ -264,21 +264,11 @@ RSpec.shared_examples 'process terraform module upload' do |user_type, status, a end context 'and direct upload disabled' do - context 'and background upload disabled' do - let(:fog_connection) do - stub_package_file_object_storage(direct_upload: false, background_upload: false) - end - - it_behaves_like 'creates terraform module package files' + let(:fog_connection) do + stub_package_file_object_storage(direct_upload: false) end - context 'and background upload enabled' do - let(:fog_connection) do - stub_package_file_object_storage(direct_upload: false, background_upload: true) - end - - it_behaves_like 'creates terraform module package files' - end + it_behaves_like 'creates terraform module package files' end end end 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 11759b6671f..82ed6eb4c95 100644 --- a/spec/support/shared_examples/requests/rack_attack_shared_examples.rb +++ b/spec/support/shared_examples/requests/rack_attack_shared_examples.rb @@ -68,6 +68,7 @@ RSpec.shared_examples 'rate-limited token requests' do # Set low limits settings_to_set[:"#{throttle_setting_prefix}_requests_per_period"] = requests_per_period settings_to_set[:"#{throttle_setting_prefix}_period_in_seconds"] = period_in_seconds + travel_back end after do @@ -220,6 +221,7 @@ RSpec.shared_examples 'rate-limited web authenticated requests' do # Set low limits settings_to_set[:"#{throttle_setting_prefix}_requests_per_period"] = requests_per_period settings_to_set[:"#{throttle_setting_prefix}_period_in_seconds"] = period_in_seconds + travel_back end after do @@ -436,6 +438,7 @@ RSpec.shared_examples 'rate-limited unauthenticated requests' do # Set low limits settings_to_set[:"#{throttle_setting_prefix}_requests_per_period"] = requests_per_period settings_to_set[:"#{throttle_setting_prefix}_period_in_seconds"] = period_in_seconds + travel_back end context 'when the throttle is enabled' do |