diff options
Diffstat (limited to 'spec/support/shared_examples/requests/api/graphql')
3 files changed, 557 insertions, 11 deletions
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 |