summaryrefslogtreecommitdiff
path: root/spec/support/shared_examples/requests/api/graphql
diff options
context:
space:
mode:
Diffstat (limited to 'spec/support/shared_examples/requests/api/graphql')
-rw-r--r--spec/support/shared_examples/requests/api/graphql/issue_list_shared_examples.rb513
-rw-r--r--spec/support/shared_examples/requests/api/graphql/packages/group_and_project_packages_list_shared_examples.rb17
-rw-r--r--spec/support/shared_examples/requests/api/graphql/projects/branch_protections/access_level_request_examples.rb38
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