Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'spec/requests/api/graphql')
-rw-r--r--spec/requests/api/graphql/ci/groups_spec.rb24
-rw-r--r--spec/requests/api/graphql/ci/job_spec.rb100
-rw-r--r--spec/requests/api/graphql/custom_emoji_query_spec.rb10
-rw-r--r--spec/requests/api/graphql/gitlab_schema_spec.rb22
-rw-r--r--spec/requests/api/graphql/group/milestones_spec.rb36
-rw-r--r--spec/requests/api/graphql/group/timelogs_spec.rb121
-rw-r--r--spec/requests/api/graphql/group_query_spec.rb10
-rw-r--r--spec/requests/api/graphql/mutations/boards/issues/issue_move_list_spec.rb37
-rw-r--r--spec/requests/api/graphql/mutations/merge_requests/set_assignees_spec.rb55
-rw-r--r--spec/requests/api/graphql/mutations/merge_requests/set_labels_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/release_asset_links/delete_spec.rb49
-rw-r--r--spec/requests/api/graphql/mutations/snippets/create_spec.rb4
-rw-r--r--spec/requests/api/graphql/mutations/snippets/update_spec.rb7
-rw-r--r--spec/requests/api/graphql/packages/package_spec.rb121
-rw-r--r--spec/requests/api/graphql/project/alert_management/alert/assignees_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/alert_management/alert/notes_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/alert_management/alert/todos_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/alert_management/integrations_spec.rb98
-rw-r--r--spec/requests/api/graphql/project/container_repositories_spec.rb2
-rw-r--r--spec/requests/api/graphql/project/issues_spec.rb84
-rw-r--r--spec/requests/api/graphql/project/merge_request_spec.rb288
-rw-r--r--spec/requests/api/graphql/project/merge_requests_spec.rb13
-rw-r--r--spec/requests/api/graphql/project/pipeline_spec.rb240
-rw-r--r--spec/requests/api/graphql/project/repository/blobs_spec.rb36
-rw-r--r--spec/requests/api/graphql/user_spec.rb8
25 files changed, 1162 insertions, 211 deletions
diff --git a/spec/requests/api/graphql/ci/groups_spec.rb b/spec/requests/api/graphql/ci/groups_spec.rb
index 9e81358a152..d1a4395d2c9 100644
--- a/spec/requests/api/graphql/ci/groups_spec.rb
+++ b/spec/requests/api/graphql/ci/groups_spec.rb
@@ -4,10 +4,15 @@ require 'spec_helper'
RSpec.describe 'Query.project.pipeline.stages.groups' do
include GraphqlHelpers
- let(:project) { create(:project, :repository, :public) }
- let(:user) { create(:user) }
- let(:pipeline) { create(:ci_pipeline, project: project, user: user) }
- let(:group_graphql_data) { graphql_data.dig('project', 'pipeline', 'stages', 'nodes', 0, 'groups', 'nodes') }
+ let_it_be(:project) { create(:project, :repository, :public) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:pipeline) { create(:ci_pipeline, project: project, user: user) }
+ let(:group_graphql_data) { graphql_data_at(:project, :pipeline, :stages, :nodes, 0, :groups, :nodes) }
+
+ let_it_be(:ref) { 'master' }
+ let_it_be(:job_a) { create(:commit_status, pipeline: pipeline, name: 'rspec 0 2', ref: ref) }
+ let_it_be(:job_b) { create(:ci_build, pipeline: pipeline, name: 'rspec 0 1', ref: ref) }
+ let_it_be(:job_c) { create(:ci_bridge, pipeline: pipeline, name: 'spinach 0 1', ref: ref) }
let(:params) { {} }
@@ -38,18 +43,15 @@ RSpec.describe 'Query.project.pipeline.stages.groups' do
end
before do
- create(:commit_status, pipeline: pipeline, name: 'rspec 0 2')
- create(:commit_status, pipeline: pipeline, name: 'rspec 0 1')
- create(:commit_status, pipeline: pipeline, name: 'spinach 0 1')
post_graphql(query, current_user: user)
end
it_behaves_like 'a working graphql query'
it 'returns a array of jobs belonging to a pipeline' do
- expect(group_graphql_data.map { |g| g.slice('name', 'size') }).to eq([
- { 'name' => 'rspec', 'size' => 2 },
- { 'name' => 'spinach', 'size' => 1 }
- ])
+ expect(group_graphql_data).to contain_exactly(
+ a_hash_including('name' => 'rspec', 'size' => 2),
+ a_hash_including('name' => 'spinach', 'size' => 1)
+ )
end
end
diff --git a/spec/requests/api/graphql/ci/job_spec.rb b/spec/requests/api/graphql/ci/job_spec.rb
new file mode 100644
index 00000000000..78f7d3e149b
--- /dev/null
+++ b/spec/requests/api/graphql/ci/job_spec.rb
@@ -0,0 +1,100 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Query.project(fullPath).pipelines.job(id)' do
+ include GraphqlHelpers
+
+ let_it_be(:user) { create_default(:user) }
+ let_it_be(:project) { create(:project, :repository, :public) }
+ let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
+
+ let_it_be(:prepare_stage) { create(:ci_stage_entity, pipeline: pipeline, project: project, name: 'prepare') }
+ let_it_be(:test_stage) { create(:ci_stage_entity, pipeline: pipeline, project: project, name: 'test') }
+
+ let_it_be(:job_1) { create(:ci_build, pipeline: pipeline, stage: 'prepare', name: 'Job 1') }
+ let_it_be(:job_2) { create(:ci_build, pipeline: pipeline, stage: 'test', name: 'Job 2') }
+ let_it_be(:job_3) { create(:ci_build, pipeline: pipeline, stage: 'test', name: 'Job 3') }
+
+ let(:path_to_job) do
+ [
+ [:project, { full_path: project.full_path }],
+ [:pipelines, { first: 1 }],
+ [:nodes, nil],
+ [:job, { id: global_id_of(job_2) }]
+ ]
+ end
+
+ let(:query) do
+ wrap_fields(query_graphql_path(query_path, all_graphql_fields_for(terminal_type)))
+ end
+
+ describe 'scalar fields' do
+ let(:path) { [:project, :pipelines, :nodes, 0, :job] }
+ let(:query_path) { path_to_job }
+ let(:terminal_type) { 'CiJob' }
+
+ it 'retrieves scalar fields' do
+ post_graphql(query, current_user: user)
+
+ expect(graphql_data_at(*path)).to match a_hash_including(
+ 'id' => global_id_of(job_2),
+ 'name' => job_2.name,
+ 'allowFailure' => job_2.allow_failure,
+ 'duration' => job_2.duration,
+ 'status' => job_2.status.upcase
+ )
+ end
+
+ context 'when fetching by name' do
+ before do
+ query_path.last[1] = { name: job_2.name }
+ end
+
+ it 'retrieves scalar fields' do
+ post_graphql(query, current_user: user)
+
+ expect(graphql_data_at(*path)).to match a_hash_including(
+ 'id' => global_id_of(job_2),
+ 'name' => job_2.name
+ )
+ end
+ end
+ end
+
+ describe '.detailedStatus' do
+ let(:path) { [:project, :pipelines, :nodes, 0, :job, :detailed_status] }
+ let(:query_path) { path_to_job + [:detailed_status] }
+ let(:terminal_type) { 'DetailedStatus' }
+
+ it 'retrieves detailed status' do
+ post_graphql(query, current_user: user)
+
+ expect(graphql_data_at(*path)).to match a_hash_including(
+ 'text' => 'pending',
+ 'label' => 'pending',
+ 'action' => a_hash_including('buttonTitle' => 'Cancel this job', 'icon' => 'cancel')
+ )
+ end
+ end
+
+ describe '.stage' do
+ let(:path) { [:project, :pipelines, :nodes, 0, :job, :stage] }
+ let(:query_path) { path_to_job + [:stage] }
+ let(:terminal_type) { 'CiStage' }
+
+ it 'returns appropriate data' do
+ post_graphql(query, current_user: user)
+
+ expect(graphql_data_at(*path)).to match a_hash_including(
+ 'name' => test_stage.name,
+ 'jobs' => a_hash_including(
+ 'nodes' => contain_exactly(
+ a_hash_including('id' => global_id_of(job_2)),
+ a_hash_including('id' => global_id_of(job_3))
+ )
+ )
+ )
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/custom_emoji_query_spec.rb b/spec/requests/api/graphql/custom_emoji_query_spec.rb
index d5a423d0eba..874357d9eef 100644
--- a/spec/requests/api/graphql/custom_emoji_query_spec.rb
+++ b/spec/requests/api/graphql/custom_emoji_query_spec.rb
@@ -16,7 +16,15 @@ RSpec.describe 'getting custom emoji within namespace' do
describe "Query CustomEmoji on Group" do
def custom_emoji_query(group)
- graphql_query_for('group', 'fullPath' => group.full_path)
+ fields = all_graphql_fields_for('Group')
+ # TODO: Set required timelogs args elsewhere https://gitlab.com/gitlab-org/gitlab/-/issues/325499
+ fields.selection['timelogs(startDate: "2021-03-01" endDate: "2021-03-30")'] = fields.selection.delete('timelogs')
+
+ graphql_query_for(
+ 'group',
+ { fullPath: group.full_path },
+ fields
+ )
end
it 'returns emojis when authorised' do
diff --git a/spec/requests/api/graphql/gitlab_schema_spec.rb b/spec/requests/api/graphql/gitlab_schema_spec.rb
index fe1c7c15de2..b41d851439b 100644
--- a/spec/requests/api/graphql/gitlab_schema_spec.rb
+++ b/spec/requests/api/graphql/gitlab_schema_spec.rb
@@ -125,9 +125,9 @@ RSpec.describe 'GitlabSchema configurations' do
subject do
queries = [
- { query: graphql_query_for('project', { 'fullPath' => '$fullPath' }, %w(id name description)) },
- { query: graphql_query_for('echo', { 'text' => "$test" }, []), variables: { "test" => "Hello world" } },
- { query: graphql_query_for('project', { 'fullPath' => project.full_path }, "userPermissions { createIssue }") }
+ { query: graphql_query_for('project', { 'fullPath' => '$fullPath' }, %w(id name description)) }, # Complexity 4
+ { query: graphql_query_for('echo', { 'text' => "$test" }, []), variables: { "test" => "Hello world" } }, # Complexity 1
+ { query: graphql_query_for('project', { 'fullPath' => project.full_path }, "userPermissions { createIssue }") } # Complexity 3
]
post_multiplex(queries, current_user: current_user)
@@ -139,10 +139,9 @@ RSpec.describe 'GitlabSchema configurations' do
expect(json_response.last['data']['project']).to be_nil
end
- it_behaves_like 'imposing query limits' do
- it 'fails all queries when only one of the queries is too complex' do
- # The `project` query above has a complexity of 5
- allow(GitlabSchema).to receive(:max_query_complexity).and_return 4
+ shared_examples 'query is too complex' do |description, max_complexity|
+ it description, :aggregate_failures do
+ allow(GitlabSchema).to receive(:max_query_complexity).and_return max_complexity
subject
@@ -155,11 +154,17 @@ RSpec.describe 'GitlabSchema configurations' do
# Expect errors for each query
expect(graphql_errors.size).to eq(3)
graphql_errors.each do |single_query_errors|
- expect_graphql_errors_to_include(/which exceeds max complexity of 4/)
+ expect_graphql_errors_to_include(/Query has complexity of 8, which exceeds max complexity of #{max_complexity}/)
end
end
end
+ it_behaves_like 'imposing query limits' do
+ # The total complexity of the multiplex query above is 8
+ it_behaves_like 'query is too complex', 'fails all queries when only one of the queries is too complex', 4
+ it_behaves_like 'query is too complex', 'fails when all queries combined are too complex', 7
+ end
+
context 'authentication' do
let(:current_user) { project.owner }
@@ -191,6 +196,7 @@ RSpec.describe 'GitlabSchema configurations' do
complexity: 181,
depth: 13,
duration_s: 7,
+ operation_name: 'IntrospectionQuery',
used_fields: an_instance_of(Array),
used_deprecated_fields: an_instance_of(Array)
}
diff --git a/spec/requests/api/graphql/group/milestones_spec.rb b/spec/requests/api/graphql/group/milestones_spec.rb
index 380eaea17f8..a5b489d72fd 100644
--- a/spec/requests/api/graphql/group/milestones_spec.rb
+++ b/spec/requests/api/graphql/group/milestones_spec.rb
@@ -9,12 +9,14 @@ RSpec.describe 'Milestones through GroupQuery' do
let_it_be(:now) { Time.now }
describe 'Get list of milestones from a group' do
- let_it_be(:group) { create(:group) }
+ let_it_be(:parent_group) { create(:group) }
+ let_it_be(:group) { create(:group, parent: parent_group) }
let_it_be(:milestone_1) { create(:milestone, group: group) }
let_it_be(:milestone_2) { create(:milestone, group: group, state: :closed, start_date: now, due_date: now + 1.day) }
let_it_be(:milestone_3) { create(:milestone, group: group, start_date: now, due_date: now + 2.days) }
let_it_be(:milestone_4) { create(:milestone, group: group, state: :closed, start_date: now - 2.days, due_date: now - 1.day) }
let_it_be(:milestone_from_other_group) { create(:milestone, group: create(:group)) }
+ let_it_be(:parent_milestone) { create(:milestone, group: parent_group) }
let(:milestone_data) { graphql_data['group']['milestones']['edges'] }
@@ -64,14 +66,32 @@ RSpec.describe 'Milestones through GroupQuery' do
accessible_group.add_developer(user)
end
- it 'returns milestones also from subgroups and subprojects visible to user' do
- fetch_milestones(user, args)
+ context 'when including decendants' do
+ let(:args) { { include_descendants: true } }
+
+ it 'returns milestones also from subgroups and subprojects visible to user' do
+ fetch_milestones(user, args)
+
+ expect_array_response(
+ milestone_1.to_global_id.to_s, milestone_2.to_global_id.to_s,
+ milestone_3.to_global_id.to_s, milestone_4.to_global_id.to_s,
+ submilestone_1.to_global_id.to_s, submilestone_2.to_global_id.to_s
+ )
+ end
+ end
+
+ context 'when including ancestors' do
+ let(:args) { { include_ancestors: true } }
- expect_array_response(
- milestone_1.to_global_id.to_s, milestone_2.to_global_id.to_s,
- milestone_3.to_global_id.to_s, milestone_4.to_global_id.to_s,
- submilestone_1.to_global_id.to_s, submilestone_2.to_global_id.to_s
- )
+ it 'returns milestones from ancestor groups' do
+ fetch_milestones(user, args)
+
+ expect_array_response(
+ milestone_1.to_global_id.to_s, milestone_2.to_global_id.to_s,
+ milestone_3.to_global_id.to_s, milestone_4.to_global_id.to_s,
+ parent_milestone.to_global_id.to_s
+ )
+ end
end
end
diff --git a/spec/requests/api/graphql/group/timelogs_spec.rb b/spec/requests/api/graphql/group/timelogs_spec.rb
new file mode 100644
index 00000000000..6e21a73afa9
--- /dev/null
+++ b/spec/requests/api/graphql/group/timelogs_spec.rb
@@ -0,0 +1,121 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Timelogs through GroupQuery' do
+ include GraphqlHelpers
+
+ describe 'Get list of timelogs from a group issues' do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, :public, group: group) }
+ let_it_be(:milestone) { create(:milestone, group: group) }
+ let_it_be(:issue) { create(:issue, project: project, milestone: milestone) }
+ let_it_be(:timelog1) { create(:timelog, issue: issue, user: user, spent_at: '2019-08-13 14:00:00') }
+ let_it_be(:timelog2) { create(:timelog, issue: issue, user: user, spent_at: '2019-08-10 08:00:00') }
+ let_it_be(:params) { { startTime: '2019-08-10 12:00:00', endTime: '2019-08-21 12:00:00' } }
+
+ let(:timelogs_data) { graphql_data['group']['timelogs']['nodes'] }
+
+ before do
+ group.add_developer(user)
+ end
+
+ context 'when the request is correct' do
+ before do
+ post_graphql(query, current_user: user)
+ end
+
+ it_behaves_like 'a working graphql query'
+
+ it 'returns timelogs successfully' do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(graphql_errors).to be_nil
+ expect(timelog_array.size).to eq 1
+ end
+
+ it 'contains correct data', :aggregate_failures do
+ username = timelog_array.map { |data| data['user']['username'] }
+ spent_at = timelog_array.map { |data| data['spentAt'].to_time }
+ time_spent = timelog_array.map { |data| data['timeSpent'] }
+ issue_title = timelog_array.map { |data| data['issue']['title'] }
+ milestone_title = timelog_array.map { |data| data['issue']['milestone']['title'] }
+
+ expect(username).to eq([user.username])
+ expect(spent_at.first).to be_like_time(timelog1.spent_at)
+ expect(time_spent).to eq([timelog1.time_spent])
+ expect(issue_title).to eq([issue.title])
+ expect(milestone_title).to eq([milestone.title])
+ end
+
+ context 'when arguments with no time are present' do
+ let!(:timelog3) { create(:timelog, issue: issue, user: user, spent_at: '2019-08-10 15:00:00') }
+ let!(:timelog4) { create(:timelog, issue: issue, user: user, spent_at: '2019-08-21 15:00:00') }
+ let(:params) { { startDate: '2019-08-10', endDate: '2019-08-21' } }
+
+ it 'sets times as start of day and end of day' do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(timelog_array.size).to eq 2
+ end
+ end
+ end
+
+ context 'when requests has errors' do
+ context 'when there are no timelogs present' do
+ before do
+ Timelog.delete_all
+ end
+
+ it 'returns empty result' do
+ post_graphql(query, current_user: user)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(graphql_errors).to be_nil
+ expect(timelogs_data).to be_empty
+ end
+ end
+
+ context 'when user has no permission to read group timelogs' do
+ it 'returns empty result' do
+ guest = create(:user)
+ group.add_guest(guest)
+ post_graphql(query, current_user: guest)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(graphql_errors).to be_nil
+ expect(timelogs_data).to be_empty
+ end
+ end
+ end
+ end
+
+ def timelog_array(extract_attribute = nil)
+ timelogs_data.map do |item|
+ extract_attribute ? item[extract_attribute] : item
+ end
+ end
+
+ def query(timelog_params = params)
+ timelog_nodes = <<~NODE
+ nodes {
+ spentAt
+ timeSpent
+ user {
+ username
+ }
+ issue {
+ title
+ milestone {
+ title
+ }
+ }
+ }
+ NODE
+
+ graphql_query_for(
+ :group,
+ { full_path: group.full_path },
+ query_graphql_field(:timelogs, timelog_params, timelog_nodes)
+ )
+ end
+end
diff --git a/spec/requests/api/graphql/group_query_spec.rb b/spec/requests/api/graphql/group_query_spec.rb
index 391bae4cfcf..8e4f808f794 100644
--- a/spec/requests/api/graphql/group_query_spec.rb
+++ b/spec/requests/api/graphql/group_query_spec.rb
@@ -17,7 +17,15 @@ RSpec.describe 'getting group information' do
# similar to the API "GET /groups/:id"
describe "Query group(fullPath)" do
def group_query(group)
- graphql_query_for('group', 'fullPath' => group.full_path)
+ fields = all_graphql_fields_for('Group')
+ # TODO: Set required timelogs args elsewhere https://gitlab.com/gitlab-org/gitlab/-/issues/325499
+ fields.selection['timelogs(startDate: "2021-03-01" endDate: "2021-03-30")'] = fields.selection.delete('timelogs')
+
+ graphql_query_for(
+ 'group',
+ { fullPath: group.full_path },
+ fields
+ )
end
it_behaves_like 'a working graphql query' do
diff --git a/spec/requests/api/graphql/mutations/boards/issues/issue_move_list_spec.rb b/spec/requests/api/graphql/mutations/boards/issues/issue_move_list_spec.rb
index e24ab0b07f2..46ec22e7ef8 100644
--- a/spec/requests/api/graphql/mutations/boards/issues/issue_move_list_spec.rb
+++ b/spec/requests/api/graphql/mutations/boards/issues/issue_move_list_spec.rb
@@ -21,7 +21,8 @@ RSpec.describe 'Reposition and move issue within board lists' do
let(:mutation_name) { mutation_class.graphql_name }
let(:mutation_result_identifier) { mutation_name.camelize(:lower) }
let(:current_user) { user }
- let(:params) { { board_id: board.to_global_id.to_s, project_path: project.full_path, iid: issue1.iid.to_s } }
+ let(:board_id) { global_id_of(board) }
+ let(:params) { { board_id: board_id, project_path: project.full_path, iid: issue1.iid.to_s } }
let(:issue_move_params) do
{
from_list_id: list1.id,
@@ -34,16 +35,44 @@ RSpec.describe 'Reposition and move issue within board lists' do
end
shared_examples 'returns an error' do
- it 'fails with error' do
- message = "The resource that you are attempting to access does not exist or you don't have "\
- "permission to perform this action"
+ let(:message) do
+ "The resource that you are attempting to access does not exist or you don't have " \
+ "permission to perform this action"
+ end
+ it 'fails with error' do
post_graphql_mutation(mutation(params), current_user: current_user)
expect(graphql_errors).to include(a_hash_including('message' => message))
end
end
+ context 'when the board_id is not a board' do
+ let(:board_id) { global_id_of(project) }
+ let(:issue_move_params) do
+ { move_after_id: existing_issue1.id, move_before_id: existing_issue2.id }
+ end
+
+ it_behaves_like 'returns an error' do
+ let(:message) { include('does not represent an instance of') }
+ end
+ end
+
+ # This test aims to distinguish between the failures to authorize
+ # :read_issue_board and :update_issue
+ context 'when the user cannot read the issue board' do
+ let(:issue_move_params) do
+ { move_after_id: existing_issue1.id, move_before_id: existing_issue2.id }
+ end
+
+ before do
+ allow(Ability).to receive(:allowed?).with(any_args).and_return(true)
+ allow(Ability).to receive(:allowed?).with(current_user, :read_issue_board, board).and_return(false)
+ end
+
+ it_behaves_like 'returns an error'
+ end
+
context 'when user has access to resources' do
context 'when repositioning an issue' do
let(:issue_move_params) { { move_after_id: existing_issue1.id, move_before_id: existing_issue2.id } }
diff --git a/spec/requests/api/graphql/mutations/merge_requests/set_assignees_spec.rb b/spec/requests/api/graphql/mutations/merge_requests/set_assignees_spec.rb
index 97873b01338..bcede4d37dd 100644
--- a/spec/requests/api/graphql/mutations/merge_requests/set_assignees_spec.rb
+++ b/spec/requests/api/graphql/mutations/merge_requests/set_assignees_spec.rb
@@ -5,11 +5,12 @@ require 'spec_helper'
RSpec.describe 'Setting assignees of a merge request' do
include GraphqlHelpers
- let(:current_user) { create(:user) }
- let(:merge_request) { create(:merge_request) }
- let(:project) { merge_request.project }
- let(:assignee) { create(:user) }
- let(:assignee2) { create(:user) }
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:current_user) { create(:user, developer_projects: [project]) }
+ let_it_be(:assignee) { create(:user) }
+ let_it_be(:assignee2) { create(:user) }
+ let_it_be_with_reload(:merge_request) { create(:merge_request, source_project: project) }
+
let(:input) { { assignee_usernames: [assignee.username] } }
let(:expected_result) do
[{ 'username' => assignee.username }]
@@ -44,10 +45,19 @@ RSpec.describe 'Setting assignees of a merge request' do
mutation_response['mergeRequest']['assignees']['nodes']
end
+ def run_mutation!
+ recorder = ActiveRecord::QueryRecorder.new do
+ post_graphql_mutation(mutation, current_user: current_user)
+ end
+
+ expect(recorder.count).to be <= db_query_limit
+ end
+
before do
- project.add_developer(current_user)
project.add_developer(assignee)
project.add_developer(assignee2)
+
+ merge_request.update!(assignees: [])
end
it 'returns an error if the user is not allowed to update the merge request' do
@@ -56,23 +66,29 @@ RSpec.describe 'Setting assignees of a merge request' do
expect(graphql_errors).not_to be_empty
end
- it 'does not allow members without the right permission to add assignees' do
- user = create(:user)
- project.add_guest(user)
+ context 'when the current user does not have permission to add assignees' do
+ let(:current_user) { create(:user) }
+ let(:db_query_limit) { 27 }
- post_graphql_mutation(mutation, current_user: user)
+ it 'does not change the assignees' do
+ project.add_guest(current_user)
- expect(graphql_errors).not_to be_empty
+ expect { run_mutation! }.not_to change { merge_request.reset.assignees.pluck(:id) }
+
+ expect(graphql_errors).not_to be_empty
+ end
end
context 'with assignees already assigned' do
+ let(:db_query_limit) { 39 }
+
before do
merge_request.assignees = [assignee2]
merge_request.save!
end
it 'replaces the assignee' do
- post_graphql_mutation(mutation, current_user: current_user)
+ run_mutation!
expect(response).to have_gitlab_http_status(:success)
expect(mutation_assignee_nodes).to match_array(expected_result)
@@ -80,6 +96,7 @@ RSpec.describe 'Setting assignees of a merge request' do
end
context 'when passing an empty list of assignees' do
+ let(:db_query_limit) { 31 }
let(:input) { { assignee_usernames: [] } }
before do
@@ -88,7 +105,7 @@ RSpec.describe 'Setting assignees of a merge request' do
end
it 'removes assignee' do
- post_graphql_mutation(mutation, current_user: current_user)
+ run_mutation!
expect(response).to have_gitlab_http_status(:success)
expect(mutation_assignee_nodes).to eq([])
@@ -96,7 +113,9 @@ RSpec.describe 'Setting assignees of a merge request' do
end
context 'when passing append as true' do
- let(:input) { { assignee_usernames: [assignee2.username], operation_mode: Types::MutationOperationModeEnum.enum[:append] } }
+ let(:mode) { Types::MutationOperationModeEnum.enum[:append] }
+ let(:input) { { assignee_usernames: [assignee2.username], operation_mode: mode } }
+ let(:db_query_limit) { 20 }
before do
# In CE, APPEND is a NOOP as you can't have multiple assignees
@@ -108,7 +127,7 @@ RSpec.describe 'Setting assignees of a merge request' do
end
it 'does not replace the assignee in CE' do
- post_graphql_mutation(mutation, current_user: current_user)
+ run_mutation!
expect(response).to have_gitlab_http_status(:success)
expect(mutation_assignee_nodes).to match_array(expected_result)
@@ -116,7 +135,9 @@ RSpec.describe 'Setting assignees of a merge request' do
end
context 'when passing remove as true' do
- let(:input) { { assignee_usernames: [assignee.username], operation_mode: Types::MutationOperationModeEnum.enum[:remove] } }
+ let(:db_query_limit) { 31 }
+ let(:mode) { Types::MutationOperationModeEnum.enum[:remove] }
+ let(:input) { { assignee_usernames: [assignee.username], operation_mode: mode } }
let(:expected_result) { [] }
before do
@@ -125,7 +146,7 @@ RSpec.describe 'Setting assignees of a merge request' do
end
it 'removes the users in the list, while adding none' do
- post_graphql_mutation(mutation, current_user: current_user)
+ run_mutation!
expect(response).to have_gitlab_http_status(:success)
expect(mutation_assignee_nodes).to match_array(expected_result)
diff --git a/spec/requests/api/graphql/mutations/merge_requests/set_labels_spec.rb b/spec/requests/api/graphql/mutations/merge_requests/set_labels_spec.rb
index 34d347c76fd..0d0cc66c52a 100644
--- a/spec/requests/api/graphql/mutations/merge_requests/set_labels_spec.rb
+++ b/spec/requests/api/graphql/mutations/merge_requests/set_labels_spec.rb
@@ -52,7 +52,7 @@ RSpec.describe 'Setting labels of a merge request' do
end
it 'sets the merge request labels, removing existing ones' do
- merge_request.update(labels: [label2])
+ merge_request.update!(labels: [label2])
post_graphql_mutation(mutation, current_user: current_user)
diff --git a/spec/requests/api/graphql/mutations/release_asset_links/delete_spec.rb b/spec/requests/api/graphql/mutations/release_asset_links/delete_spec.rb
new file mode 100644
index 00000000000..57489c82ec2
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/release_asset_links/delete_spec.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Deletes a release asset link' do
+ include GraphqlHelpers
+
+ let_it_be(:project) { create(:project, :private, :repository) }
+ let_it_be(:release) { create(:release, project: project) }
+ let_it_be(:maintainer) { create(:user).tap { |u| project.add_maintainer(u) } }
+ let_it_be(:release_link) { create(:release_link, release: release) }
+
+ let(:current_user) { maintainer }
+ let(:mutation_name) { :release_asset_link_delete }
+ let(:mutation_arguments) { { id: release_link.to_global_id.to_s } }
+
+ let(:mutation) do
+ graphql_mutation(mutation_name, mutation_arguments, <<~FIELDS)
+ link {
+ id
+ name
+ url
+ linkType
+ directAssetUrl
+ external
+ }
+ errors
+ FIELDS
+ end
+
+ let(:delete_link) { post_graphql_mutation(mutation, current_user: current_user) }
+ let(:mutation_response) { graphql_mutation_response(mutation_name)&.with_indifferent_access }
+
+ it 'deletes the release asset link and returns the deleted link', :aggregate_failures do
+ delete_link
+
+ expected_response = {
+ id: release_link.to_global_id.to_s,
+ name: release_link.name,
+ url: release_link.url,
+ linkType: release_link.link_type.upcase,
+ directAssetUrl: end_with(release_link.filepath),
+ external: true
+ }.with_indifferent_access
+
+ expect(mutation_response[:link]).to match(expected_response)
+ expect(mutation_response[:errors]).to eq([])
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/snippets/create_spec.rb b/spec/requests/api/graphql/mutations/snippets/create_spec.rb
index 1c2260070ec..d944c9e9e57 100644
--- a/spec/requests/api/graphql/mutations/snippets/create_spec.rb
+++ b/spec/requests/api/graphql/mutations/snippets/create_spec.rb
@@ -211,5 +211,9 @@ RSpec.describe 'Creating a Snippet' do
end
end
end
+
+ it_behaves_like 'has spam protection' do
+ let(:mutation_class) { ::Mutations::Snippets::Create }
+ end
end
end
diff --git a/spec/requests/api/graphql/mutations/snippets/update_spec.rb b/spec/requests/api/graphql/mutations/snippets/update_spec.rb
index 43dc8d8bc44..28ab593526a 100644
--- a/spec/requests/api/graphql/mutations/snippets/update_spec.rb
+++ b/spec/requests/api/graphql/mutations/snippets/update_spec.rb
@@ -157,6 +157,9 @@ RSpec.describe 'Updating a Snippet' do
it_behaves_like 'graphql update actions'
it_behaves_like 'when the snippet is not found'
it_behaves_like 'snippet edit usage data counters'
+ it_behaves_like 'has spam protection' do
+ let(:mutation_class) { ::Mutations::Snippets::Update }
+ end
end
describe 'ProjectSnippet' do
@@ -201,6 +204,10 @@ RSpec.describe 'Updating a Snippet' do
end
it_behaves_like 'snippet edit usage data counters'
+
+ it_behaves_like 'has spam protection' do
+ let(:mutation_class) { ::Mutations::Snippets::Update }
+ end
end
it_behaves_like 'when the snippet is not found'
diff --git a/spec/requests/api/graphql/packages/package_spec.rb b/spec/requests/api/graphql/packages/package_spec.rb
index 654215041cb..a0131c7733e 100644
--- a/spec/requests/api/graphql/packages/package_spec.rb
+++ b/spec/requests/api/graphql/packages/package_spec.rb
@@ -2,33 +2,47 @@
require 'spec_helper'
RSpec.describe 'package details' do
- using RSpec::Parameterized::TableSyntax
include GraphqlHelpers
let_it_be(:project) { create(:project) }
- let_it_be(:package) { create(:composer_package, project: project) }
+ let_it_be(:composer_package) { create(:composer_package, project: project) }
let_it_be(:composer_json) { { name: 'name', type: 'type', license: 'license', version: 1 } }
let_it_be(:composer_metadatum) do
# we are forced to manually create the metadatum, without using the factory to force the sha to be a string
# and avoid an error where gitaly can't find the repository
- create(:composer_metadatum, package: package, target_sha: 'foo_sha', composer_json: composer_json)
+ create(:composer_metadatum, package: composer_package, target_sha: 'foo_sha', composer_json: composer_json)
end
let(:depth) { 3 }
- let(:excluded) { %w[metadata apiFuzzingCiConfiguration pipeline] }
+ let(:excluded) { %w[metadata apiFuzzingCiConfiguration pipeline packageFiles] }
+ let(:metadata) { query_graphql_fragment('ComposerMetadata') }
+ let(:package_files) {all_graphql_fields_for('PackageFile')}
+ let(:package_files_metadata) {query_graphql_fragment('ConanFileMetadata')}
let(:query) do
graphql_query_for(:package, { id: package_global_id }, <<~FIELDS)
- #{all_graphql_fields_for('Package', max_depth: depth, excluded: excluded)}
+ #{all_graphql_fields_for('PackageDetailsType', max_depth: depth, excluded: excluded)}
metadata {
- #{query_graphql_fragment('ComposerMetadata')}
+ #{metadata}
+ }
+ packageFiles {
+ nodes {
+ #{package_files}
+ fileMetadata {
+ #{package_files_metadata}
+ }
+ }
}
FIELDS
end
let(:user) { project.owner }
- let(:package_global_id) { global_id_of(package) }
+ let(:package_global_id) { global_id_of(composer_package) }
let(:package_details) { graphql_data_at(:package) }
+ let(:metadata_response) { graphql_data_at(:package, :metadata) }
+ let(:package_files_response) { graphql_data_at(:package, :package_files, :nodes) }
+ let(:first_file_response) { graphql_data_at(:package, :package_files, :nodes, 0)}
+ let(:first_file_response_metadata) { graphql_data_at(:package, :package_files, :nodes, 0, :file_metadata)}
subject { post_graphql(query, current_user: user) }
@@ -40,15 +54,68 @@ RSpec.describe 'package details' do
it 'matches the JSON schema' do
expect(package_details).to match_schema('graphql/packages/package_details')
end
+ end
+
+ describe 'Packages Metadata' do
+ before do
+ subject
+ end
- it 'includes the fields of the correct package' do
- expect(package_details).to include(
- 'id' => package_global_id,
- 'metadata' => {
+ describe 'Composer' do
+ it 'has the correct metadata' do
+ expect(metadata_response).to include(
'targetSha' => 'foo_sha',
'composerJson' => composer_json.transform_keys(&:to_s).transform_values(&:to_s)
- }
- )
+ )
+ end
+
+ it 'does not have files' do
+ expect(package_files_response).to be_empty
+ end
+ end
+
+ describe 'Conan' do
+ let_it_be(:conan_package) { create(:conan_package, project: project) }
+
+ let(:package_global_id) { global_id_of(conan_package) }
+ let(:metadata) { query_graphql_fragment('ConanMetadata') }
+ let(:first_file) { conan_package.package_files.find { |f| global_id_of(f) == first_file_response['id'] } }
+
+ it 'has the correct metadata' do
+ expect(metadata_response).to include(
+ 'id' => global_id_of(conan_package.conan_metadatum),
+ 'recipe' => conan_package.conan_metadatum.recipe,
+ 'packageChannel' => conan_package.conan_metadatum.package_channel,
+ 'packageUsername' => conan_package.conan_metadatum.package_username,
+ 'recipePath' => conan_package.conan_metadatum.recipe_path
+ )
+ end
+
+ it 'has the right amount of files' do
+ expect(package_files_response.length).to be(conan_package.package_files.length)
+ end
+
+ it 'has the basic package files data' do
+ expect(first_file_response).to include(
+ 'id' => global_id_of(first_file),
+ 'fileName' => first_file.file_name,
+ 'size' => first_file.size.to_s,
+ 'downloadPath' => first_file.download_path,
+ 'fileSha1' => first_file.file_sha1,
+ 'fileMd5' => first_file.file_md5,
+ 'fileSha256' => first_file.file_sha256
+ )
+ end
+
+ it 'has the correct file metadata' do
+ expect(first_file_response_metadata).to include(
+ 'id' => global_id_of(first_file.conan_file_metadatum),
+ 'packageRevision' => first_file.conan_file_metadatum.package_revision,
+ 'conanPackageReference' => first_file.conan_file_metadatum.conan_package_reference,
+ 'recipeRevision' => first_file.conan_file_metadatum.recipe_revision,
+ 'conanFileType' => first_file.conan_file_metadatum.conan_file_type.upcase
+ )
+ end
end
end
@@ -56,7 +123,7 @@ RSpec.describe 'package details' do
let(:depth) { 3 }
let(:excluded) { %w[metadata project tags pipelines] } # to limit the query complexity
- let_it_be(:siblings) { create_list(:composer_package, 2, project: project, name: package.name) }
+ let_it_be(:siblings) { create_list(:composer_package, 2, project: project, name: composer_package.name) }
it 'includes the sibling versions' do
subject
@@ -73,8 +140,32 @@ RSpec.describe 'package details' do
subject
expect(graphql_data_at(:package, :versions, :nodes, :version)).to be_present
- expect(graphql_data_at(:package, :versions, :nodes, :versions)).not_to be_present
+ expect(graphql_data_at(:package, :versions, :nodes, :versions, :nodes)).to be_empty
end
end
end
+
+ context 'with a batched query' do
+ let_it_be(:conan_package) { create(:conan_package, project: project) }
+
+ let(:batch_query) do
+ <<~QUERY
+ {
+ a: package(id: "#{global_id_of(composer_package)}") { name }
+ b: package(id: "#{global_id_of(conan_package)}") { name }
+ }
+ QUERY
+ end
+
+ let(:a_packages_names) { graphql_data_at(:a, :packages, :nodes, :name) }
+
+ it 'returns an error for the second package and data for the first' do
+ post_graphql(batch_query, current_user: user)
+
+ expect(graphql_data_at(:a, :name)).to eq(composer_package.name)
+
+ expect_graphql_errors_to_include [/Package details can be requested only for one package at a time/]
+ expect(graphql_data_at(:b)).to be(nil)
+ end
+ end
end
diff --git a/spec/requests/api/graphql/project/alert_management/alert/assignees_spec.rb b/spec/requests/api/graphql/project/alert_management/alert/assignees_spec.rb
index 9ab94f1d749..a59402208ec 100644
--- a/spec/requests/api/graphql/project/alert_management/alert/assignees_spec.rb
+++ b/spec/requests/api/graphql/project/alert_management/alert/assignees_spec.rb
@@ -34,7 +34,7 @@ RSpec.describe 'getting Alert Management Alert Assignees' do
end
let(:alerts) { graphql_data.dig('project', 'alertManagementAlerts', 'nodes') }
- let(:assignees) { alerts.map { |alert| [alert['iid'], alert['assignees']['nodes']] }.to_h }
+ let(:assignees) { alerts.to_h { |alert| [alert['iid'], alert['assignees']['nodes']] } }
let(:first_assignees) { assignees[first_alert.iid.to_s] }
let(:second_assignees) { assignees[second_alert.iid.to_s] }
diff --git a/spec/requests/api/graphql/project/alert_management/alert/notes_spec.rb b/spec/requests/api/graphql/project/alert_management/alert/notes_spec.rb
index 5d46f370756..72d185144ef 100644
--- a/spec/requests/api/graphql/project/alert_management/alert/notes_spec.rb
+++ b/spec/requests/api/graphql/project/alert_management/alert/notes_spec.rb
@@ -38,7 +38,7 @@ RSpec.describe 'getting Alert Management Alert Notes' do
end
let(:alerts_result) { graphql_data.dig('project', 'alertManagementAlerts', 'nodes') }
- let(:notes_result) { alerts_result.map { |alert| [alert['iid'], alert['notes']['nodes']] }.to_h }
+ let(:notes_result) { alerts_result.to_h { |alert| [alert['iid'], alert['notes']['nodes']] } }
let(:first_notes_result) { notes_result[first_alert.iid.to_s] }
let(:second_notes_result) { notes_result[second_alert.iid.to_s] }
diff --git a/spec/requests/api/graphql/project/alert_management/alert/todos_spec.rb b/spec/requests/api/graphql/project/alert_management/alert/todos_spec.rb
index 3a9077061ad..ca58079fdfe 100644
--- a/spec/requests/api/graphql/project/alert_management/alert/todos_spec.rb
+++ b/spec/requests/api/graphql/project/alert_management/alert/todos_spec.rb
@@ -34,7 +34,7 @@ RSpec.describe 'getting Alert Management Alert Assignees' do
end
let(:gql_alerts) { graphql_data.dig('project', 'alertManagementAlerts', 'nodes') }
- let(:gql_todos) { gql_alerts.map { |gql_alert| [gql_alert['iid'], gql_alert['todos']['nodes']] }.to_h }
+ let(:gql_todos) { gql_alerts.to_h { |gql_alert| [gql_alert['iid'], gql_alert['todos']['nodes']] } }
let(:gql_alert_todo) { gql_todos[alert.iid.to_s].first }
let(:gql_other_alert_todo) { gql_todos[other_alert.iid.to_s].first }
diff --git a/spec/requests/api/graphql/project/alert_management/integrations_spec.rb b/spec/requests/api/graphql/project/alert_management/integrations_spec.rb
index b13805a61ce..0e029aee9e8 100644
--- a/spec/requests/api/graphql/project/alert_management/integrations_spec.rb
+++ b/spec/requests/api/graphql/project/alert_management/integrations_spec.rb
@@ -13,6 +13,8 @@ RSpec.describe 'getting Alert Management Integrations' do
let_it_be(:inactive_http_integration) { create(:alert_management_http_integration, :inactive, project: project) }
let_it_be(:other_project_http_integration) { create(:alert_management_http_integration) }
+ let(:params) { {} }
+
let(:fields) do
<<~QUERY
nodes {
@@ -25,7 +27,7 @@ RSpec.describe 'getting Alert Management Integrations' do
graphql_query_for(
'project',
{ 'fullPath' => project.full_path },
- query_graphql_field('alertManagementIntegrations', {}, fields)
+ query_graphql_field('alertManagementIntegrations', params, fields)
)
end
@@ -50,34 +52,78 @@ RSpec.describe 'getting Alert Management Integrations' do
post_graphql(query, current_user: current_user)
end
- let(:http_integration) { integrations.first }
- let(:prometheus_integration) { integrations.second }
+ context 'when no extra params given' do
+ let(:http_integration) { integrations.first }
+ let(:prometheus_integration) { integrations.second }
- it_behaves_like 'a working graphql query'
+ it_behaves_like 'a working graphql query'
+
+ it { expect(integrations.size).to eq(2) }
+
+ it 'returns the correct properties of the integrations' do
+ expect(http_integration).to include(
+ 'id' => global_id_of(active_http_integration),
+ 'type' => 'HTTP',
+ 'name' => active_http_integration.name,
+ 'active' => active_http_integration.active,
+ 'token' => active_http_integration.token,
+ 'url' => active_http_integration.url,
+ 'apiUrl' => nil
+ )
- it { expect(integrations.size).to eq(2) }
-
- it 'returns the correct properties of the integrations' do
- expect(http_integration).to include(
- 'id' => GitlabSchema.id_from_object(active_http_integration).to_s,
- 'type' => 'HTTP',
- 'name' => active_http_integration.name,
- 'active' => active_http_integration.active,
- 'token' => active_http_integration.token,
- 'url' => active_http_integration.url,
- 'apiUrl' => nil
- )
-
- expect(prometheus_integration).to include(
- 'id' => GitlabSchema.id_from_object(prometheus_service).to_s,
- 'type' => 'PROMETHEUS',
- 'name' => 'Prometheus',
- 'active' => prometheus_service.manual_configuration?,
- 'token' => project_alerting_setting.token,
- 'url' => "http://localhost/#{project.full_path}/prometheus/alerts/notify.json",
- 'apiUrl' => prometheus_service.api_url
- )
+ expect(prometheus_integration).to include(
+ 'id' => global_id_of(prometheus_service),
+ 'type' => 'PROMETHEUS',
+ 'name' => 'Prometheus',
+ 'active' => prometheus_service.manual_configuration?,
+ 'token' => project_alerting_setting.token,
+ 'url' => "http://localhost/#{project.full_path}/prometheus/alerts/notify.json",
+ 'apiUrl' => prometheus_service.api_url
+ )
+ end
end
+
+ context 'when HTTP Integration ID is given' do
+ let(:params) { { id: global_id_of(active_http_integration) } }
+
+ it_behaves_like 'a working graphql query'
+
+ it { expect(integrations).to be_one }
+
+ it 'returns the correct properties of the HTTP integration' do
+ expect(integrations.first).to include(
+ 'id' => global_id_of(active_http_integration),
+ 'type' => 'HTTP',
+ 'name' => active_http_integration.name,
+ 'active' => active_http_integration.active,
+ 'token' => active_http_integration.token,
+ 'url' => active_http_integration.url,
+ 'apiUrl' => nil
+ )
+ end
+ end
+
+ context 'when Prometheus Integration ID is given' do
+ let(:params) { { id: global_id_of(prometheus_service) } }
+
+ it_behaves_like 'a working graphql query'
+
+ it { expect(integrations).to be_one }
+
+ it 'returns the correct properties of the Prometheus Integration' do
+ expect(integrations.first).to include(
+ 'id' => global_id_of(prometheus_service),
+ 'type' => 'PROMETHEUS',
+ 'name' => 'Prometheus',
+ 'active' => prometheus_service.manual_configuration?,
+ 'token' => project_alerting_setting.token,
+ 'url' => "http://localhost/#{project.full_path}/prometheus/alerts/notify.json",
+ 'apiUrl' => prometheus_service.api_url
+ )
+ end
+ end
+
+ it_behaves_like 'GraphQL query with several integrations requested', graphql_query_name: 'alertManagementIntegrations'
end
end
end
diff --git a/spec/requests/api/graphql/project/container_repositories_spec.rb b/spec/requests/api/graphql/project/container_repositories_spec.rb
index 5ffd48a7bc4..3ad56223b61 100644
--- a/spec/requests/api/graphql/project/container_repositories_spec.rb
+++ b/spec/requests/api/graphql/project/container_repositories_spec.rb
@@ -16,7 +16,7 @@ RSpec.describe 'getting container repositories in a project' do
<<~GQL
edges {
node {
- #{all_graphql_fields_for('container_repositories'.classify, excluded: ['pipeline'])}
+ #{all_graphql_fields_for('container_repositories'.classify, excluded: %w(pipeline jobs))}
}
}
GQL
diff --git a/spec/requests/api/graphql/project/issues_spec.rb b/spec/requests/api/graphql/project/issues_spec.rb
index 9c915075c42..dd9d44136e5 100644
--- a/spec/requests/api/graphql/project/issues_spec.rb
+++ b/spec/requests/api/graphql/project/issues_spec.rb
@@ -5,14 +5,15 @@ require 'spec_helper'
RSpec.describe 'getting an issue list for a project' do
include GraphqlHelpers
- let(:issues_data) { graphql_data['project']['issues']['edges'] }
-
let_it_be(:project) { create(:project, :repository, :public) }
let_it_be(:current_user) { create(:user) }
let_it_be(:issue_a, reload: true) { create(:issue, project: project, discussion_locked: true) }
let_it_be(:issue_b, reload: true) { create(:issue, :with_alert, project: project) }
let_it_be(:issues, reload: true) { [issue_a, issue_b] }
+ let(:issues_data) { graphql_data['project']['issues']['edges'] }
+ let(:issue_filter_params) { {} }
+
let(:fields) do
<<~QUERY
edges {
@@ -27,7 +28,7 @@ RSpec.describe 'getting an issue list for a project' do
graphql_query_for(
'project',
{ 'fullPath' => project.full_path },
- query_graphql_field('issues', {}, fields)
+ query_graphql_field('issues', issue_filter_params, fields)
)
end
@@ -50,6 +51,16 @@ RSpec.describe 'getting an issue list for a project' do
expect(issues_data[1]['node']['discussionLocked']).to eq(true)
end
+ context 'when both assignee_username filters are provided' do
+ let(:issue_filter_params) { { assignee_username: current_user.username, assignee_usernames: [current_user.username] } }
+
+ it 'returns a mutually exclusive param error' do
+ post_graphql(query, current_user: current_user)
+
+ expect_graphql_errors_to_include('only one of [assigneeUsernames, assigneeUsername] arguments is allowed at the same time.')
+ end
+ end
+
context 'when limiting the number of results' do
let(:query) do
<<~GQL
@@ -76,7 +87,7 @@ RSpec.describe 'getting an issue list for a project' do
end
end
- context 'no limit is provided' do
+ context 'when no limit is provided' do
let(:issue_limit) { nil }
it 'returns all issues' do
@@ -143,13 +154,15 @@ RSpec.describe 'getting an issue list for a project' do
let_it_be(:data_path) { [:project, :issues] }
def pagination_query(params)
- graphql_query_for(:project, { full_path: sort_project.full_path },
+ graphql_query_for(
+ :project,
+ { full_path: sort_project.full_path },
query_graphql_field(:issues, params, "#{page_info} nodes { iid }")
)
end
def pagination_results_data(data)
- data.map { |issue| issue.dig('iid').to_i }
+ data.map { |issue| issue['iid'].to_i }
end
context 'when sorting by due date' do
@@ -189,27 +202,38 @@ RSpec.describe 'getting an issue list for a project' do
it_behaves_like 'sorted paginated query' do
let(:sort_param) { :RELATIVE_POSITION_ASC }
let(:first_param) { 2 }
- let(:expected_results) { [relative_issue5.iid, relative_issue3.iid, relative_issue1.iid, relative_issue4.iid, relative_issue2.iid] }
+ let(:expected_results) do
+ [
+ relative_issue5.iid, relative_issue3.iid, relative_issue1.iid,
+ relative_issue4.iid, relative_issue2.iid
+ ]
+ end
end
end
end
context 'when sorting by priority' do
let_it_be(:sort_project) { create(:project, :public) }
- let_it_be(:early_milestone) { create(:milestone, project: sort_project, due_date: 10.days.from_now) }
- let_it_be(:late_milestone) { create(:milestone, project: sort_project, due_date: 30.days.from_now) }
- let_it_be(:priority_label1) { create(:label, project: sort_project, priority: 1) }
- let_it_be(:priority_label2) { create(:label, project: sort_project, priority: 5) }
- let_it_be(:priority_issue1) { create(:issue, project: sort_project, labels: [priority_label1], milestone: late_milestone) }
- let_it_be(:priority_issue2) { create(:issue, project: sort_project, labels: [priority_label2]) }
- let_it_be(:priority_issue3) { create(:issue, project: sort_project, milestone: early_milestone) }
- let_it_be(:priority_issue4) { create(:issue, project: sort_project) }
+ let_it_be(:on_project) { { project: sort_project } }
+ let_it_be(:early_milestone) { create(:milestone, **on_project, due_date: 10.days.from_now) }
+ let_it_be(:late_milestone) { create(:milestone, **on_project, due_date: 30.days.from_now) }
+ let_it_be(:priority_1) { create(:label, **on_project, priority: 1) }
+ let_it_be(:priority_2) { create(:label, **on_project, priority: 5) }
+ let_it_be(:priority_issue1) { create(:issue, **on_project, labels: [priority_1], milestone: late_milestone) }
+ let_it_be(:priority_issue2) { create(:issue, **on_project, labels: [priority_2]) }
+ let_it_be(:priority_issue3) { create(:issue, **on_project, milestone: early_milestone) }
+ let_it_be(:priority_issue4) { create(:issue, **on_project) }
context 'when ascending' do
it_behaves_like 'sorted paginated query' do
let(:sort_param) { :PRIORITY_ASC }
let(:first_param) { 2 }
- let(:expected_results) { [priority_issue3.iid, priority_issue1.iid, priority_issue2.iid, priority_issue4.iid] }
+ let(:expected_results) do
+ [
+ priority_issue3.iid, priority_issue1.iid,
+ priority_issue2.iid, priority_issue4.iid
+ ]
+ end
end
end
@@ -217,7 +241,9 @@ RSpec.describe 'getting an issue list for a project' do
it_behaves_like 'sorted paginated query' do
let(:sort_param) { :PRIORITY_DESC }
let(:first_param) { 2 }
- let(:expected_results) { [priority_issue1.iid, priority_issue3.iid, priority_issue2.iid, priority_issue4.iid] }
+ let(:expected_results) do
+ [priority_issue1.iid, priority_issue3.iid, priority_issue2.iid, priority_issue4.iid]
+ end
end
end
end
@@ -275,7 +301,7 @@ RSpec.describe 'getting an issue list for a project' do
end
end
- context 'fetching alert management alert' do
+ context 'when fetching alert management alert' do
let(:fields) do
<<~QUERY
edges {
@@ -297,7 +323,7 @@ RSpec.describe 'getting an issue list for a project' do
it 'avoids N+1 queries' do
control = ActiveRecord::QueryRecorder.new { post_graphql(query, current_user: current_user) }
- create(:alert_management_alert, :with_issue, project: project )
+ create(:alert_management_alert, :with_issue, project: project)
expect { post_graphql(query, current_user: current_user) }.not_to exceed_query_limit(control)
end
@@ -312,7 +338,7 @@ RSpec.describe 'getting an issue list for a project' do
end
end
- context 'fetching labels' do
+ context 'when fetching labels' do
let(:fields) do
<<~QUERY
edges {
@@ -362,7 +388,7 @@ RSpec.describe 'getting an issue list for a project' do
end
end
- context 'fetching assignees' do
+ context 'when fetching assignees' do
let(:fields) do
<<~QUERY
edges {
@@ -420,9 +446,10 @@ RSpec.describe 'getting an issue list for a project' do
query = graphql_query_for(
:project,
{ full_path: project.full_path },
- query_graphql_field(:issues, search_params, [
+ query_graphql_field(
+ :issues, search_params,
query_graphql_field(:nodes, nil, requested_fields)
- ])
+ )
)
post_graphql(query, current_user: current_user)
end
@@ -448,5 +475,16 @@ RSpec.describe 'getting an issue list for a project' do
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
end
end
diff --git a/spec/requests/api/graphql/project/merge_request_spec.rb b/spec/requests/api/graphql/project/merge_request_spec.rb
index e32899c600e..15551005502 100644
--- a/spec/requests/api/graphql/project/merge_request_spec.rb
+++ b/spec/requests/api/graphql/project/merge_request_spec.rb
@@ -5,21 +5,25 @@ require 'spec_helper'
RSpec.describe 'getting merge request information nested in a project' do
include GraphqlHelpers
- let(:project) { create(:project, :repository, :public) }
- let(:current_user) { create(:user) }
- let(:merge_request_graphql_data) { graphql_data['project']['mergeRequest'] }
- let!(:merge_request) { create(:merge_request, source_project: project) }
- let(:mr_fields) { all_graphql_fields_for('MergeRequest', excluded: ['pipeline']) }
+ let_it_be(:project) { create(:project, :repository, :public) }
+ let_it_be(:current_user) { create(:user) }
+ let_it_be_with_reload(:merge_request) { create(:merge_request, source_project: project) }
+
+ let(:merge_request_graphql_data) { graphql_data_at(:project, :merge_request) }
+ let(:mr_fields) { all_graphql_fields_for('MergeRequest', max_depth: 1) }
let(:query) do
graphql_query_for(
- 'project',
- { 'fullPath' => project.full_path },
- query_graphql_field('mergeRequest', { iid: merge_request.iid.to_s }, mr_fields)
+ :project,
+ { full_path: project.full_path },
+ query_graphql_field(:merge_request, { iid: merge_request.iid.to_s }, mr_fields)
)
end
it_behaves_like 'a working graphql query' do
+ # we exclude Project.pipeline because it needs arguments
+ let(:mr_fields) { all_graphql_fields_for('MergeRequest', excluded: %w[jobs pipeline]) }
+
before do
post_graphql(query, current_user: current_user)
end
@@ -38,13 +42,17 @@ RSpec.describe 'getting merge request information nested in a project' do
expect(merge_request_graphql_data['webUrl']).to be_present
end
- it 'includes author' do
- post_graphql(query, current_user: current_user)
+ context 'when selecting author' do
+ let(:mr_fields) { 'author { username }' }
- expect(merge_request_graphql_data['author']['username']).to eq(merge_request.author.username)
+ it 'includes author' do
+ post_graphql(query, current_user: current_user)
+
+ expect(merge_request_graphql_data['author']['username']).to eq(merge_request.author.username)
+ end
end
- context 'the merge_request has reviewers' do
+ context 'when the merge_request has reviewers' do
let(:mr_fields) do
<<~SELECT
reviewers { nodes { id username } }
@@ -68,63 +76,76 @@ RSpec.describe 'getting merge request information nested in a project' do
end
end
- it 'includes diff stats' do
- be_natural = an_instance_of(Integer).and(be >= 0)
-
- post_graphql(query, current_user: current_user)
-
- sums = merge_request_graphql_data['diffStats'].reduce([0, 0, 0]) do |(a, d, c), node|
- a_, d_ = node.values_at('additions', 'deletions')
- [a + a_, d + d_, c + a_ + d_]
+ describe 'diffStats' do
+ let(:mr_fields) do
+ <<~FIELDS
+ diffStats { #{all_graphql_fields_for('DiffStats')} }
+ diffStatsSummary { #{all_graphql_fields_for('DiffStatsSummary')} }
+ FIELDS
end
- expect(merge_request_graphql_data).to include(
- 'diffStats' => all(a_hash_including('path' => String, 'additions' => be_natural, 'deletions' => be_natural)),
- 'diffStatsSummary' => a_hash_including(
- 'fileCount' => merge_request.diff_stats.count,
- 'additions' => be_natural,
- 'deletions' => be_natural,
- 'changes' => be_natural
- )
- )
+ it 'includes diff stats' do
+ be_natural = an_instance_of(Integer).and(be >= 0)
- # diff_stats is consistent with summary
- expect(merge_request_graphql_data['diffStatsSummary']
- .values_at('additions', 'deletions', 'changes')).to eq(sums)
-
- # diff_stats_summary is internally consistent
- expect(merge_request_graphql_data['diffStatsSummary']
- .values_at('additions', 'deletions').sum)
- .to eq(merge_request_graphql_data.dig('diffStatsSummary', 'changes'))
- .and be_positive
- end
+ post_graphql(query, current_user: current_user)
- context 'requesting a specific diff stat' do
- let(:diff_stat) { merge_request.diff_stats.first }
+ sums = merge_request_graphql_data['diffStats'].reduce([0, 0, 0]) do |(a, d, c), node|
+ a_, d_ = node.values_at('additions', 'deletions')
+ [a + a_, d + d_, c + a_ + d_]
+ end
- let(:query) do
- graphql_query_for(:project, { full_path: project.full_path },
- query_graphql_field(:merge_request, { iid: merge_request.iid.to_s }, [
- query_graphql_field(:diff_stats, { path: diff_stat.path }, all_graphql_fields_for('DiffStats'))
- ])
+ expect(merge_request_graphql_data).to include(
+ 'diffStats' => all(a_hash_including('path' => String, 'additions' => be_natural, 'deletions' => be_natural)),
+ 'diffStatsSummary' => a_hash_including(
+ 'fileCount' => merge_request.diff_stats.count,
+ 'additions' => be_natural,
+ 'deletions' => be_natural,
+ 'changes' => be_natural
+ )
)
+
+ # diff_stats is consistent with summary
+ expect(merge_request_graphql_data['diffStatsSummary']
+ .values_at('additions', 'deletions', 'changes')).to eq(sums)
+
+ # diff_stats_summary is internally consistent
+ expect(merge_request_graphql_data['diffStatsSummary']
+ .values_at('additions', 'deletions').sum)
+ .to eq(merge_request_graphql_data.dig('diffStatsSummary', 'changes'))
+ .and be_positive
end
- it 'includes only the requested stats' do
- post_graphql(query, current_user: current_user)
+ context 'when requesting a specific diff stat' do
+ let(:diff_stat) { merge_request.diff_stats.first }
- expect(merge_request_graphql_data).to include(
- 'diffStats' => contain_exactly(
- a_hash_including('path' => diff_stat.path, 'additions' => diff_stat.additions, 'deletions' => diff_stat.deletions)
+ let(:mr_fields) do
+ query_graphql_field(
+ :diff_stats,
+ { path: diff_stat.path },
+ all_graphql_fields_for('DiffStats')
)
- )
+ end
+
+ it 'includes only the requested stats' do
+ post_graphql(query, current_user: current_user)
+
+ expect(merge_request_graphql_data).to include(
+ 'diffStats' => contain_exactly(
+ a_hash_including(
+ 'path' => diff_stat.path,
+ 'additions' => diff_stat.additions,
+ 'deletions' => diff_stat.deletions
+ )
+ )
+ )
+ end
end
end
it 'includes correct mergedAt value when merged' do
time = 1.week.ago
merge_request.mark_as_merged
- merge_request.metrics.update_columns(merged_at: time)
+ merge_request.metrics.update!(merged_at: time)
post_graphql(query, current_user: current_user)
retrieved = merge_request_graphql_data['mergedAt']
@@ -139,7 +160,11 @@ RSpec.describe 'getting merge request information nested in a project' do
expect(retrieved).to be_nil
end
- context 'permissions on the merge request' do
+ describe 'permissions on the merge request' do
+ let(:mr_fields) do
+ "userPermissions { #{all_graphql_fields_for('MergeRequestPermissions')} }"
+ end
+
it 'includes the permissions for the current user on a public project' do
expected_permissions = {
'readMergeRequest' => true,
@@ -162,8 +187,6 @@ RSpec.describe 'getting merge request information nested in a project' do
end
context 'when the user does not have access to the merge request' do
- let(:project) { create(:project, :public, :repository) }
-
it 'returns nil' do
project.project_feature.update!(merge_requests_access_level: ProjectFeature::PRIVATE)
@@ -174,13 +197,23 @@ RSpec.describe 'getting merge request information nested in a project' do
end
context 'when there are pipelines' do
- before do
+ let_it_be(:pipeline) do
create(
:ci_pipeline,
project: merge_request.source_project,
ref: merge_request.source_branch,
sha: merge_request.diff_head_sha
)
+ end
+
+ let(:mr_fields) do
+ <<~FIELDS
+ headPipeline { id }
+ pipelines { nodes { id } }
+ FIELDS
+ end
+
+ before do
merge_request.update_head_pipeline
end
@@ -193,20 +226,12 @@ RSpec.describe 'getting merge request information nested in a project' do
it 'has pipeline connections' do
post_graphql(query, current_user: current_user)
- expect(merge_request_graphql_data['pipelines']['edges'].size).to eq(1)
+ expect(merge_request_graphql_data['pipelines']['nodes']).to be_one
end
end
context 'when limiting the number of results' do
- let(:merge_requests_graphql_data) { graphql_data['project']['mergeRequests']['edges'] }
-
- let!(:merge_requests) do
- [
- create(:merge_request, source_project: project, source_branch: 'branch-1'),
- create(:merge_request, source_project: project, source_branch: 'branch-2'),
- create(:merge_request, source_project: project, source_branch: 'branch-3')
- ]
- end
+ let(:merge_requests_graphql_data) { graphql_data_at(:project, :merge_requests, :edges) }
let(:fields) do
<<~QUERY
@@ -228,6 +253,10 @@ RSpec.describe 'getting merge request information nested in a project' do
end
it 'returns the correct number of results' do
+ create(:merge_request, source_project: project, source_branch: 'branch-1')
+ create(:merge_request, source_project: project, source_branch: 'branch-2')
+ create(:merge_request, source_project: project, source_branch: 'branch-3')
+
post_graphql(query, current_user: current_user)
expect(merge_requests_graphql_data.size).to eq 2
@@ -281,4 +310,129 @@ RSpec.describe 'getting merge request information nested in a project' do
)
end
end
+
+ context 'when requesting information about MR interactions' do
+ let_it_be(:user) { create(:user) }
+
+ let(:selected_fields) { all_graphql_fields_for('UserMergeRequestInteraction') }
+
+ let(:mr_fields) do
+ query_nodes(
+ :reviewers,
+ query_graphql_field(:merge_request_interaction, nil, selected_fields)
+ )
+ end
+
+ def interaction_data
+ graphql_data_at(:project, :merge_request, :reviewers, :nodes, :merge_request_interaction)
+ end
+
+ context 'when the user does not have interactions' do
+ it 'returns null data' do
+ post_graphql(query)
+
+ expect(interaction_data).to be_empty
+ end
+ end
+
+ context 'when the user is a reviewer, but has not reviewed' do
+ before do
+ project.add_guest(user)
+ merge_request.merge_request_reviewers.create!(reviewer: user)
+ end
+
+ it 'returns falsey values' do
+ post_graphql(query)
+
+ expect(interaction_data).to contain_exactly a_hash_including(
+ 'canMerge' => false,
+ 'canUpdate' => false,
+ 'reviewState' => 'UNREVIEWED',
+ 'reviewed' => false,
+ 'approved' => false
+ )
+ end
+ end
+
+ context 'when the user has interacted' do
+ before do
+ project.add_maintainer(user)
+ merge_request.merge_request_reviewers.create!(reviewer: user, state: 'reviewed')
+ merge_request.approved_by_users << user
+ end
+
+ it 'returns appropriate data' do
+ post_graphql(query)
+ enum = ::Types::MergeRequestReviewStateEnum.values['REVIEWED']
+
+ expect(interaction_data).to contain_exactly a_hash_including(
+ 'canMerge' => true,
+ 'canUpdate' => true,
+ 'reviewState' => enum.graphql_name,
+ 'reviewed' => true,
+ 'approved' => true
+ )
+ end
+ end
+
+ describe 'scalability' do
+ let_it_be(:other_users) { create_list(:user, 3) }
+
+ let(:unreviewed) do
+ { 'reviewState' => 'UNREVIEWED' }
+ end
+
+ let(:reviewed) do
+ { 'reviewState' => 'REVIEWED' }
+ end
+
+ shared_examples 'scalable query for interaction fields' do
+ before do
+ ([user] + other_users).each { project.add_guest(_1) }
+ end
+
+ it 'does not suffer from N+1' do
+ merge_request.merge_request_reviewers.create!(reviewer: user, state: 'reviewed')
+
+ baseline = ActiveRecord::QueryRecorder.new do
+ post_graphql(query)
+ end
+
+ expect(interaction_data).to contain_exactly(include(reviewed))
+
+ other_users.each do |user|
+ merge_request.merge_request_reviewers.create!(reviewer: user)
+ end
+
+ expect { post_graphql(query) }.not_to exceed_query_limit(baseline)
+
+ expect(interaction_data).to contain_exactly(
+ include(unreviewed),
+ include(unreviewed),
+ include(unreviewed),
+ include(reviewed)
+ )
+ end
+ end
+
+ context 'when selecting only known scalable fields' do
+ let(:not_scalable) { %w[canUpdate canMerge] }
+ let(:selected_fields) do
+ all_graphql_fields_for('UserMergeRequestInteraction', excluded: not_scalable)
+ end
+
+ it_behaves_like 'scalable query for interaction fields'
+ end
+
+ context 'when selecting all fields' do
+ before do
+ pending "See: https://gitlab.com/gitlab-org/gitlab/-/issues/322549"
+ end
+
+ let(:selected_fields) { all_graphql_fields_for('UserMergeRequestInteraction') }
+
+ it_behaves_like 'scalable query for interaction fields'
+ end
+ end
+ end
end
diff --git a/spec/requests/api/graphql/project/merge_requests_spec.rb b/spec/requests/api/graphql/project/merge_requests_spec.rb
index d97a0ed9399..7fc1ef05fa7 100644
--- a/spec/requests/api/graphql/project/merge_requests_spec.rb
+++ b/spec/requests/api/graphql/project/merge_requests_spec.rb
@@ -47,10 +47,10 @@ RSpec.describe 'getting merge request listings nested in a project' do
end
before do
- # We cannot call the whitelist here, since the transaction does not
+ # We cannot disable SQL query limiting here, since the transaction does not
# begin until we enter the controller.
headers = {
- 'X-GITLAB-QUERY-WHITELIST-ISSUE' => 'https://gitlab.com/gitlab-org/gitlab/-/issues/322979'
+ 'X-GITLAB-DISABLE-SQL-QUERY-LIMIT' => 'https://gitlab.com/gitlab-org/gitlab/-/issues/322979'
}
post_graphql(query, current_user: current_user, headers: headers)
@@ -299,6 +299,7 @@ RSpec.describe 'getting merge request listings nested in a project' do
reviewers { nodes { username } }
participants { nodes { username } }
headPipeline { status }
+ timelogs { nodes { timeSpent } }
SELECT
end
@@ -307,7 +308,7 @@ RSpec.describe 'getting merge request listings nested in a project' do
query($first: Int) {
project(fullPath: "#{project.full_path}") {
mergeRequests(first: $first) {
- nodes { #{mr_fields} }
+ nodes { iid #{mr_fields} }
}
}
}
@@ -324,6 +325,7 @@ RSpec.describe 'getting merge request listings nested in a project' do
mr.assignees << current_user
mr.reviewers << create(:user)
mr.reviewers << current_user
+ mr.timelogs << create(:merge_request_timelog, merge_request: mr)
end
end
@@ -345,7 +347,7 @@ RSpec.describe 'getting merge request listings nested in a project' do
end
def user_collection
- { 'nodes' => all(match(a_hash_including('username' => be_present))) }
+ { 'nodes' => be_present.and(all(match(a_hash_including('username' => be_present)))) }
end
it 'returns appropriate results' do
@@ -358,7 +360,8 @@ RSpec.describe 'getting merge request listings nested in a project' do
'assignees' => user_collection,
'reviewers' => user_collection,
'participants' => user_collection,
- 'headPipeline' => { 'status' => be_present }
+ 'headPipeline' => { 'status' => be_present },
+ 'timelogs' => { 'nodes' => be_one }
)))
end
diff --git a/spec/requests/api/graphql/project/pipeline_spec.rb b/spec/requests/api/graphql/project/pipeline_spec.rb
index cc028ff2ff9..0a5bcc7a965 100644
--- a/spec/requests/api/graphql/project/pipeline_spec.rb
+++ b/spec/requests/api/graphql/project/pipeline_spec.rb
@@ -5,24 +5,28 @@ require 'spec_helper'
RSpec.describe 'getting pipeline information nested in a project' do
include GraphqlHelpers
- let!(:project) { create(:project, :repository, :public) }
- let!(:pipeline) { create(:ci_pipeline, project: project) }
- let!(:current_user) { create(:user) }
- let(:pipeline_graphql_data) { graphql_data['project']['pipeline'] }
-
- let!(:query) do
- %(
- query {
- project(fullPath: "#{project.full_path}") {
- pipeline(iid: "#{pipeline.iid}") {
- configSource
- }
- }
- }
+ let_it_be(:project) { create(:project, :repository, :public) }
+ let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:build_job) { create(:ci_build, :trace_with_sections, name: 'build-a', pipeline: pipeline) }
+ let_it_be(:failed_build) { create(:ci_build, :failed, name: 'failed-build', pipeline: pipeline) }
+ let_it_be(:bridge) { create(:ci_bridge, name: 'ci-bridge-example', pipeline: pipeline) }
+
+ let(:path) { %i[project pipeline] }
+ let(:pipeline_graphql_data) { graphql_data_at(*path) }
+ let(:depth) { 3 }
+ let(:excluded) { %w[job project] } # Project is very expensive, due to the number of fields
+ let(:fields) { all_graphql_fields_for('Pipeline', excluded: excluded, max_depth: depth) }
+
+ let(:query) do
+ graphql_query_for(
+ :project,
+ { full_path: project.full_path },
+ query_graphql_field(:pipeline, { iid: pipeline.iid.to_s }, fields)
)
end
- it_behaves_like 'a working graphql query' do
+ it_behaves_like 'a working graphql query', :use_clean_rails_memory_store_caching, :request_store do
before do
post_graphql(query, current_user: current_user)
end
@@ -37,14 +41,18 @@ RSpec.describe 'getting pipeline information nested in a project' do
it 'contains configSource' do
post_graphql(query, current_user: current_user)
- expect(pipeline_graphql_data.dig('configSource')).to eq('UNKNOWN_SOURCE')
+ expect(pipeline_graphql_data['configSource']).to eq('UNKNOWN_SOURCE')
end
- context 'batching' do
- let!(:pipeline2) { create(:ci_pipeline, project: project, user: current_user, builds: [create(:ci_build, :success)]) }
- let!(:pipeline3) { create(:ci_pipeline, project: project, user: current_user, builds: [create(:ci_build, :success)]) }
+ context 'when batching' do
+ let!(:pipeline2) { successful_pipeline }
+ let!(:pipeline3) { successful_pipeline }
let!(:query) { build_query_to_find_pipeline_shas(pipeline, pipeline2, pipeline3) }
+ def successful_pipeline
+ create(:ci_pipeline, project: project, user: current_user, builds: [create(:ci_build, :success)])
+ end
+
it 'executes the finder once' do
mock = double(Ci::PipelinesFinder)
opts = { iids: [pipeline.iid, pipeline2.iid, pipeline3.iid].map(&:to_s) }
@@ -80,4 +88,198 @@ RSpec.describe 'getting pipeline information nested in a project' do
graphql_query_for('project', { 'fullPath' => project.full_path }, pipeline_fields)
end
+
+ context 'when enough data is requested' do
+ let(:fields) do
+ query_graphql_field(:jobs, nil,
+ query_graphql_field(:nodes, {}, all_graphql_fields_for('CiJob', max_depth: 3)))
+ end
+
+ it 'contains jobs' do
+ post_graphql(query, current_user: current_user)
+
+ expect(graphql_data_at(*path, :jobs, :nodes)).to contain_exactly(
+ a_hash_including(
+ 'name' => build_job.name,
+ 'status' => build_job.status.upcase,
+ 'duration' => build_job.duration
+ ),
+ a_hash_including(
+ 'id' => global_id_of(failed_build),
+ 'status' => failed_build.status.upcase
+ ),
+ a_hash_including(
+ 'id' => global_id_of(bridge),
+ 'status' => bridge.status.upcase
+ )
+ )
+ end
+ end
+
+ context 'when requesting only builds with certain statuses' do
+ let(:variables) do
+ {
+ path: project.full_path,
+ pipelineIID: pipeline.iid.to_s,
+ status: :FAILED
+ }
+ end
+
+ let(:query) do
+ <<~GQL
+ query($path: ID!, $pipelineIID: ID!, $status: CiJobStatus!) {
+ project(fullPath: $path) {
+ pipeline(iid: $pipelineIID) {
+ jobs(statuses: [$status]) {
+ nodes {
+ #{all_graphql_fields_for('CiJob', max_depth: 1)}
+ }
+ }
+ }
+ }
+ }
+ GQL
+ end
+
+ it 'can filter build jobs by status' do
+ post_graphql(query, current_user: current_user, variables: variables)
+
+ expect(graphql_data_at(*path, :jobs, :nodes))
+ .to contain_exactly(a_hash_including('id' => global_id_of(failed_build)))
+ end
+ end
+
+ context 'when requesting a specific job' do
+ let(:variables) do
+ {
+ path: project.full_path,
+ pipelineIID: pipeline.iid.to_s
+ }
+ end
+
+ let(:build_fields) do
+ all_graphql_fields_for('CiJob', max_depth: 1)
+ end
+
+ let(:query) do
+ <<~GQL
+ query($path: ID!, $pipelineIID: ID!, $jobName: String, $jobID: JobID) {
+ project(fullPath: $path) {
+ pipeline(iid: $pipelineIID) {
+ job(id: $jobID, name: $jobName) {
+ #{build_fields}
+ }
+ }
+ }
+ }
+ GQL
+ end
+
+ let(:the_job) do
+ a_hash_including('name' => build_job.name, 'id' => global_id_of(build_job))
+ end
+
+ it 'can request a build by name' do
+ vars = variables.merge(jobName: build_job.name)
+
+ post_graphql(query, current_user: current_user, variables: vars)
+
+ expect(graphql_data_at(*path, :job)).to match(the_job)
+ end
+
+ it 'can request a build by ID' do
+ vars = variables.merge(jobID: global_id_of(build_job))
+
+ post_graphql(query, current_user: current_user, variables: vars)
+
+ expect(graphql_data_at(*path, :job)).to match(the_job)
+ end
+
+ context 'when we request nested fields of the build' do
+ let_it_be(:needy) { create(:ci_build, :dependent, pipeline: pipeline) }
+
+ let(:build_fields) { 'needs { nodes { name } }' }
+ let(:vars) { variables.merge(jobID: global_id_of(needy)) }
+
+ it 'returns the nested data' do
+ post_graphql(query, current_user: current_user, variables: vars)
+
+ expect(graphql_data_at(*path, :job, :needs, :nodes)).to contain_exactly(
+ a_hash_including('name' => needy.needs.first.name)
+ )
+ end
+
+ it 'requires a constant number of queries' do
+ fst_user = create(:user)
+ snd_user = create(:user)
+ path = %i[project pipeline job needs nodes name]
+
+ baseline = ActiveRecord::QueryRecorder.new do
+ post_graphql(query, current_user: fst_user, variables: vars)
+ end
+
+ expect(baseline.count).to be > 0
+ dep_names = graphql_dig_at(graphql_data(fresh_response_data), *path)
+
+ deps = create_list(:ci_build, 3, :unique_name, pipeline: pipeline)
+ deps.each { |d| create(:ci_build_need, build: needy, name: d.name) }
+
+ expect do
+ post_graphql(query, current_user: snd_user, variables: vars)
+ end.not_to exceed_query_limit(baseline)
+
+ more_names = graphql_dig_at(graphql_data(fresh_response_data), *path)
+
+ expect(more_names).to include(*dep_names)
+ expect(more_names.count).to be > dep_names.count
+ end
+ end
+ end
+
+ context 'when requesting a specific test suite' do
+ let_it_be(:pipeline) { create(:ci_pipeline, :with_test_reports, project: project) }
+ let(:suite_name) { 'test' }
+ let_it_be(:build_ids) { pipeline.latest_builds.pluck(:id) }
+
+ let(:variables) do
+ {
+ path: project.full_path,
+ pipelineIID: pipeline.iid.to_s
+ }
+ end
+
+ let(:query) do
+ <<~GQL
+ query($path: ID!, $pipelineIID: ID!, $buildIds: [ID!]!) {
+ project(fullPath: $path) {
+ pipeline(iid: $pipelineIID) {
+ testSuite(buildIds: $buildIds) {
+ name
+ }
+ }
+ }
+ }
+ GQL
+ end
+
+ it 'can request a test suite by an array of build_ids' do
+ vars = variables.merge(buildIds: build_ids)
+
+ post_graphql(query, current_user: current_user, variables: vars)
+
+ expect(graphql_data_at(:project, :pipeline, :testSuite, :name)).to eq(suite_name)
+ end
+
+ context 'when pipeline has no builds that matches the given build_ids' do
+ let_it_be(:build_ids) { [non_existing_record_id] }
+
+ it 'returns nil' do
+ vars = variables.merge(buildIds: build_ids)
+
+ post_graphql(query, current_user: current_user, variables: vars)
+
+ expect(graphql_data_at(*path, :test_suite)).to be_nil
+ end
+ end
+ end
end
diff --git a/spec/requests/api/graphql/project/repository/blobs_spec.rb b/spec/requests/api/graphql/project/repository/blobs_spec.rb
new file mode 100644
index 00000000000..12f6fbd793e
--- /dev/null
+++ b/spec/requests/api/graphql/project/repository/blobs_spec.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe 'getting blobs in a project repository' do
+ include GraphqlHelpers
+
+ let(:project) { create(:project, :repository) }
+ let(:current_user) { project.owner }
+ let(:paths) { ["CONTRIBUTING.md", "README.md"] }
+ let(:ref) { project.default_branch }
+ let(:fields) do
+ <<~QUERY
+ blobs(paths:#{paths.inspect}, ref:#{ref.inspect}) {
+ nodes {
+ #{all_graphql_fields_for('repository_blob'.classify)}
+ }
+ }
+ QUERY
+ end
+
+ let(:query) do
+ graphql_query_for(
+ 'project',
+ { 'fullPath' => project.full_path },
+ query_graphql_field('repository', {}, fields)
+ )
+ end
+
+ subject(:blobs) { graphql_data_at(:project, :repository, :blobs, :nodes) }
+
+ it 'returns the blob' do
+ post_graphql(query, current_user: current_user)
+
+ expect(blobs).to match_array(paths.map { |path| a_hash_including('path' => path) })
+ end
+end
diff --git a/spec/requests/api/graphql/user_spec.rb b/spec/requests/api/graphql/user_spec.rb
index d2d6b1fca66..a3b2b750bc3 100644
--- a/spec/requests/api/graphql/user_spec.rb
+++ b/spec/requests/api/graphql/user_spec.rb
@@ -42,7 +42,13 @@ RSpec.describe 'User' do
end
context 'when username and id parameter are used' do
- let_it_be(:query) { graphql_query_for(:user, { id: current_user.to_global_id.to_s, username: current_user.username }, 'id') }
+ let_it_be(:query) do
+ graphql_query_for(
+ :user,
+ { id: current_user.to_global_id.to_s, username: current_user.username },
+ 'id'
+ )
+ end
it 'displays an error' do
post_graphql(query)