summaryrefslogtreecommitdiff
path: root/spec/requests/api/graphql
diff options
context:
space:
mode:
Diffstat (limited to 'spec/requests/api/graphql')
-rw-r--r--spec/requests/api/graphql/boards/board_lists_query_spec.rb16
-rw-r--r--spec/requests/api/graphql/ci/ci_cd_setting_spec.rb50
-rw-r--r--spec/requests/api/graphql/ci/config_spec.rb91
-rw-r--r--spec/requests/api/graphql/ci/job_artifacts_spec.rb52
-rw-r--r--spec/requests/api/graphql/ci/jobs_spec.rb119
-rw-r--r--spec/requests/api/graphql/group/container_repositories_spec.rb22
-rw-r--r--spec/requests/api/graphql/group/group_members_spec.rb80
-rw-r--r--spec/requests/api/graphql/group_query_spec.rb2
-rw-r--r--spec/requests/api/graphql/issue/issue_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/admin/sidekiq_queues/delete_jobs_spec.rb12
-rw-r--r--spec/requests/api/graphql/mutations/award_emojis/add_spec.rb4
-rw-r--r--spec/requests/api/graphql/mutations/award_emojis/toggle_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/container_repository/destroy_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/container_repository/destroy_tags_spec.rb120
-rw-r--r--spec/requests/api/graphql/mutations/environments/canary_ingress/update_spec.rb45
-rw-r--r--spec/requests/api/graphql/mutations/releases/delete_spec.rb132
-rw-r--r--spec/requests/api/graphql/mutations/releases/update_spec.rb255
-rw-r--r--spec/requests/api/graphql/mutations/snippets/create_spec.rb8
-rw-r--r--spec/requests/api/graphql/mutations/snippets/mark_as_spam_spec.rb10
-rw-r--r--spec/requests/api/graphql/namespace/projects_spec.rb44
-rw-r--r--spec/requests/api/graphql/project/container_repositories_spec.rb22
-rw-r--r--spec/requests/api/graphql/project/issue/design_collection/version_spec.rb3
-rw-r--r--spec/requests/api/graphql/project/issue_spec.rb6
-rw-r--r--spec/requests/api/graphql/project/issues_spec.rb28
-rw-r--r--spec/requests/api/graphql/project/merge_request/pipelines_spec.rb63
-rw-r--r--spec/requests/api/graphql/project/merge_requests_spec.rb47
-rw-r--r--spec/requests/api/graphql/project/pipeline_spec.rb49
-rw-r--r--spec/requests/api/graphql/project/project_members_spec.rb121
-rw-r--r--spec/requests/api/graphql/project/project_pipeline_statistics_spec.rb58
-rw-r--r--spec/requests/api/graphql/project/release_spec.rb10
-rw-r--r--spec/requests/api/graphql/project/terraform/states_spec.rb45
-rw-r--r--spec/requests/api/graphql/project_query_spec.rb35
-rw-r--r--spec/requests/api/graphql/user_query_spec.rb142
-rw-r--r--spec/requests/api/graphql/users_spec.rb14
34 files changed, 1488 insertions, 223 deletions
diff --git a/spec/requests/api/graphql/boards/board_lists_query_spec.rb b/spec/requests/api/graphql/boards/board_lists_query_spec.rb
index 5d5b963fed5..cd94ce91071 100644
--- a/spec/requests/api/graphql/boards/board_lists_query_spec.rb
+++ b/spec/requests/api/graphql/boards/board_lists_query_spec.rb
@@ -66,28 +66,22 @@ RSpec.describe 'get board lists' do
describe 'sorting and pagination' do
let_it_be(:current_user) { user }
- let(:data_path) { [board_parent_type, :boards, :edges, 0, :node, :lists] }
+ let(:data_path) { [board_parent_type, :boards, :nodes, 0, :lists] }
- def pagination_query(params, page_info)
+ def pagination_query(params)
graphql_query_for(
board_parent_type,
{ 'fullPath' => board_parent.full_path },
<<~BOARDS
boards(first: 1) {
- edges {
- node {
- #{query_graphql_field('lists', params, "#{page_info} edges { node { id } }")}
- }
+ nodes {
+ #{query_graphql_field(:lists, params, "#{page_info} nodes { id }")}
}
}
BOARDS
)
end
- def pagination_results_data(data)
- data.map { |list| list.dig('node', 'id') }
- end
-
context 'when using default sorting' do
let!(:label_list) { create(:list, board: board, label: label, position: 10) }
let!(:label_list2) { create(:list, board: board, label: label2, position: 2) }
@@ -99,7 +93,7 @@ RSpec.describe 'get board lists' do
it_behaves_like 'sorted paginated query' do
let(:sort_param) { }
let(:first_param) { 2 }
- let(:expected_results) { lists.map { |list| list.to_global_id.to_s } }
+ let(:expected_results) { lists.map { |list| global_id_of(list) } }
end
end
end
diff --git a/spec/requests/api/graphql/ci/ci_cd_setting_spec.rb b/spec/requests/api/graphql/ci/ci_cd_setting_spec.rb
new file mode 100644
index 00000000000..e086ce02942
--- /dev/null
+++ b/spec/requests/api/graphql/ci/ci_cd_setting_spec.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe 'Getting Ci Cd Setting' do
+ include GraphqlHelpers
+
+ let_it_be_with_reload(:project) { create(:project, :repository) }
+ let_it_be(:current_user) { project.owner }
+
+ let(:fields) do
+ <<~QUERY
+ #{all_graphql_fields_for('ProjectCiCdSetting')}
+ QUERY
+ end
+
+ let(:query) do
+ graphql_query_for(
+ 'project',
+ { 'fullPath' => project.full_path },
+ query_graphql_field('ciCdSettings', {}, fields)
+ )
+ end
+
+ let(:settings_data) { graphql_data['project']['ciCdSettings'] }
+
+ context 'without permissions' do
+ let(:user) { create(:user) }
+
+ before do
+ project.add_reporter(user)
+ post_graphql(query, current_user: user)
+ end
+
+ it_behaves_like 'a working graphql query'
+
+ specify { expect(settings_data).to be nil }
+ end
+
+ context 'with project permissions' do
+ before do
+ post_graphql(query, current_user: current_user)
+ end
+
+ it_behaves_like 'a working graphql query'
+
+ specify { expect(settings_data['mergePipelinesEnabled']).to eql project.ci_cd_settings.merge_pipelines_enabled? }
+ specify { expect(settings_data['mergeTrainsEnabled']).to eql project.ci_cd_settings.merge_trains_enabled? }
+ specify { expect(settings_data['project']['id']).to eql "gid://gitlab/Project/#{project.id}" }
+ end
+end
diff --git a/spec/requests/api/graphql/ci/config_spec.rb b/spec/requests/api/graphql/ci/config_spec.rb
new file mode 100644
index 00000000000..b682470e0a1
--- /dev/null
+++ b/spec/requests/api/graphql/ci/config_spec.rb
@@ -0,0 +1,91 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Query.ciConfig' do
+ include GraphqlHelpers
+
+ subject(:post_graphql_query) { post_graphql(query, current_user: user) }
+
+ let(:user) { create(:user) }
+
+ let_it_be(:content) do
+ File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci_includes.yml'))
+ end
+
+ let(:query) do
+ %(
+ query {
+ ciConfig(content: "#{content}") {
+ status
+ errors
+ stages {
+ name
+ groups {
+ name
+ size
+ jobs {
+ name
+ groupName
+ stage
+ needs {
+ name
+ }
+ }
+ }
+ }
+ }
+ }
+ )
+ end
+
+ before do
+ post_graphql_query
+ end
+
+ it_behaves_like 'a working graphql query'
+
+ it 'returns the correct structure' do
+ expect(graphql_data['ciConfig']).to eq(
+ "status" => "VALID",
+ "errors" => [],
+ "stages" =>
+ [
+ {
+ "name" => "build",
+ "groups" =>
+ [
+ {
+ "name" => "rspec",
+ "size" => 2,
+ "jobs" =>
+ [
+ { "name" => "rspec 0 1", "groupName" => "rspec", "stage" => "build", "needs" => [] },
+ { "name" => "rspec 0 2", "groupName" => "rspec", "stage" => "build", "needs" => [] }
+ ]
+ },
+ {
+ "name" => "spinach", "size" => 1, "jobs" =>
+ [
+ { "name" => "spinach", "groupName" => "spinach", "stage" => "build", "needs" => [] }
+ ]
+ }
+ ]
+ },
+ {
+ "name" => "test",
+ "groups" =>
+ [
+ {
+ "name" => "docker",
+ "size" => 1,
+ "jobs" => [
+ { "name" => "docker", "groupName" => "docker", "stage" => "test", "needs" => [{ "name" => "spinach" }, { "name" => "rspec 0 1" }] }
+ ]
+ }
+ ]
+ }
+ ]
+ )
+ end
+end
diff --git a/spec/requests/api/graphql/ci/job_artifacts_spec.rb b/spec/requests/api/graphql/ci/job_artifacts_spec.rb
new file mode 100644
index 00000000000..df6e398fbe5
--- /dev/null
+++ b/spec/requests/api/graphql/ci/job_artifacts_spec.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Query.project(fullPath).pipelines.jobs.artifacts' do
+ include GraphqlHelpers
+
+ let_it_be(:project) { create(:project, :repository, :public) }
+ let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
+ let_it_be(:user) { create(:user) }
+
+ let_it_be(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ pipelines {
+ nodes {
+ jobs {
+ nodes {
+ artifacts {
+ nodes {
+ downloadPath
+ fileType
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ )
+ end
+
+ it 'returns the fields for the artifacts' do
+ job = create(:ci_build, pipeline: pipeline)
+ create(:ci_job_artifact, :junit, job: job)
+
+ post_graphql(query, current_user: user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+
+ pipelines_data = graphql_data.dig('project', 'pipelines', 'nodes')
+ jobs_data = pipelines_data.first.dig('jobs', 'nodes')
+ artifact_data = jobs_data.first.dig('artifacts', 'nodes').first
+
+ expect(artifact_data['downloadPath']).to eq(
+ "/#{project.full_path}/-/jobs/#{job.id}/artifacts/download?file_type=junit"
+ )
+ expect(artifact_data['fileType']).to eq('JUNIT')
+ end
+end
diff --git a/spec/requests/api/graphql/ci/jobs_spec.rb b/spec/requests/api/graphql/ci/jobs_spec.rb
index 618705e5f94..19954c4e52f 100644
--- a/spec/requests/api/graphql/ci/jobs_spec.rb
+++ b/spec/requests/api/graphql/ci/jobs_spec.rb
@@ -1,41 +1,44 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe 'Query.project.pipeline.stages.groups.jobs' do
+RSpec.describe 'Query.project.pipeline' do
include GraphqlHelpers
- let(:project) { create(:project, :repository, :public) }
- let(:user) { create(:user) }
- let(:pipeline) do
- pipeline = create(:ci_pipeline, project: project, user: user)
- stage = create(:ci_stage_entity, pipeline: pipeline, name: 'first')
- create(:commit_status, stage_id: stage.id, pipeline: pipeline, name: 'my test job')
-
- pipeline
- end
+ let_it_be(:project) { create(:project, :repository, :public) }
+ let_it_be(:user) { create(:user) }
def first(field)
[field.pluralize, 'nodes', 0]
end
- let(:jobs_graphql_data) { graphql_data.dig(*%w[project pipeline], *first('stage'), *first('group'), 'jobs', 'nodes') }
-
- let(:query) do
- %(
- query {
- project(fullPath: "#{project.full_path}") {
- pipeline(iid: "#{pipeline.iid}") {
- stages {
- nodes {
- name
- groups {
- nodes {
- name
- jobs {
- nodes {
- name
- pipeline {
- id
+ describe '.stages.groups.jobs' do
+ let(:pipeline) do
+ pipeline = create(:ci_pipeline, project: project, user: user)
+ stage = create(:ci_stage_entity, pipeline: pipeline, name: 'first')
+ create(:commit_status, stage_id: stage.id, pipeline: pipeline, name: 'my test job')
+
+ pipeline
+ end
+
+ let(:jobs_graphql_data) { graphql_data.dig(*%w[project pipeline], *first('stage'), *first('group'), 'jobs', 'nodes') }
+
+ let(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ pipeline(iid: "#{pipeline.iid}") {
+ stages {
+ nodes {
+ name
+ groups {
+ nodes {
+ name
+ jobs {
+ nodes {
+ name
+ pipeline {
+ id
+ }
}
}
}
@@ -45,17 +48,15 @@ RSpec.describe 'Query.project.pipeline.stages.groups.jobs' do
}
}
}
- }
- )
- end
+ )
+ end
- it 'returns the jobs of a pipeline stage' do
- post_graphql(query, current_user: user)
+ it 'returns the jobs of a pipeline stage' do
+ post_graphql(query, current_user: user)
- expect(jobs_graphql_data).to contain_exactly(a_hash_including('name' => 'my test job'))
- end
+ expect(jobs_graphql_data).to contain_exactly(a_hash_including('name' => 'my test job'))
+ end
- context 'when fetching jobs from the pipeline' do
it 'avoids N+1 queries', :aggregate_failures do
control_count = ActiveRecord::QueryRecorder.new do
post_graphql(query, current_user: user)
@@ -112,4 +113,50 @@ RSpec.describe 'Query.project.pipeline.stages.groups.jobs' do
])
end
end
+
+ describe '.jobs.artifacts' do
+ let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
+
+ let(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ pipeline(iid: "#{pipeline.iid}") {
+ jobs {
+ nodes {
+ artifacts {
+ nodes {
+ downloadPath
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ )
+ end
+
+ context 'when the job is a build' do
+ it "returns the build's artifacts" do
+ create(:ci_build, :artifacts, pipeline: pipeline)
+
+ post_graphql(query, current_user: user)
+
+ job_data = graphql_data.dig('project', 'pipeline', 'jobs', 'nodes').first
+ expect(job_data.dig('artifacts', 'nodes').count).to be(2)
+ end
+ end
+
+ context 'when the job is not a build' do
+ it 'returns nil' do
+ create(:ci_bridge, pipeline: pipeline)
+
+ post_graphql(query, current_user: user)
+
+ job_data = graphql_data.dig('project', 'pipeline', 'jobs', 'nodes').first
+ expect(job_data['artifacts']).to be_nil
+ end
+ end
+ end
end
diff --git a/spec/requests/api/graphql/group/container_repositories_spec.rb b/spec/requests/api/graphql/group/container_repositories_spec.rb
index bcf689a5e8f..4aa775eba0f 100644
--- a/spec/requests/api/graphql/group/container_repositories_spec.rb
+++ b/spec/requests/api/graphql/group/container_repositories_spec.rb
@@ -14,7 +14,7 @@ RSpec.describe 'getting container repositories in a group' do
let_it_be(:container_repositories) { [container_repository, container_repositories_delete_scheduled, container_repositories_delete_failed].flatten }
let_it_be(:container_expiration_policy) { project.container_expiration_policy }
- let(:fields) do
+ let(:container_repositories_fields) do
<<~GQL
edges {
node {
@@ -24,17 +24,25 @@ RSpec.describe 'getting container repositories in a group' do
GQL
end
+ let(:fields) do
+ <<~GQL
+ #{query_graphql_field('container_repositories', {}, container_repositories_fields)}
+ containerRepositoriesCount
+ GQL
+ end
+
let(:query) do
graphql_query_for(
'group',
{ 'fullPath' => group.full_path },
- query_graphql_field('container_repositories', {}, fields)
+ fields
)
end
let(:user) { owner }
let(:variables) { {} }
let(:container_repositories_response) { graphql_data.dig('group', 'containerRepositories', 'edges') }
+ let(:container_repositories_count_response) { graphql_data.dig('group', 'containerRepositoriesCount') }
before do
group.add_owner(owner)
@@ -101,7 +109,7 @@ RSpec.describe 'getting container repositories in a group' do
<<~GQL
query($path: ID!, $n: Int) {
group(fullPath: $path) {
- containerRepositories(first: $n) { #{fields} }
+ containerRepositories(first: $n) { #{container_repositories_fields} }
}
}
GQL
@@ -122,7 +130,7 @@ RSpec.describe 'getting container repositories in a group' do
<<~GQL
query($path: ID!, $name: String) {
group(fullPath: $path) {
- containerRepositories(name: $name) { #{fields} }
+ containerRepositories(name: $name) { #{container_repositories_fields} }
}
}
GQL
@@ -143,4 +151,10 @@ RSpec.describe 'getting container repositories in a group' do
expect(container_repositories_response.first.dig('node', 'id')).to eq(container_repository.to_global_id.to_s)
end
end
+
+ it 'returns the total count of container repositories' do
+ subject
+
+ expect(container_repositories_count_response).to eq(container_repositories.size)
+ end
end
diff --git a/spec/requests/api/graphql/group/group_members_spec.rb b/spec/requests/api/graphql/group/group_members_spec.rb
index 84b2fd63d46..3554e22cdf2 100644
--- a/spec/requests/api/graphql/group/group_members_spec.rb
+++ b/spec/requests/api/graphql/group/group_members_spec.rb
@@ -5,44 +5,95 @@ require 'spec_helper'
RSpec.describe 'getting group members information' do
include GraphqlHelpers
- let_it_be(:group) { create(:group, :public) }
+ let_it_be(:parent_group) { create(:group, :public) }
let_it_be(:user) { create(:user) }
let_it_be(:user_1) { create(:user, username: 'user') }
let_it_be(:user_2) { create(:user, username: 'test') }
let(:member_data) { graphql_data['group']['groupMembers']['edges'] }
- before do
- [user_1, user_2].each { |user| group.add_guest(user) }
+ before_all do
+ [user_1, user_2].each { |user| parent_group.add_guest(user) }
end
context 'when the request is correct' do
it_behaves_like 'a working graphql query' do
- before do
- fetch_members(user)
+ before_all do
+ fetch_members
end
end
it 'returns group members successfully' do
- fetch_members(user)
+ fetch_members
expect(graphql_errors).to be_nil
- expect_array_response(user_1.to_global_id.to_s, user_2.to_global_id.to_s)
+ expect_array_response(user_1, user_2)
end
it 'returns members that match the search query' do
- fetch_members(user, { search: 'test' })
+ fetch_members(args: { search: 'test' })
expect(graphql_errors).to be_nil
- expect_array_response(user_2.to_global_id.to_s)
+ expect_array_response(user_2)
end
end
- def fetch_members(user = nil, args = {})
- post_graphql(members_query(args), current_user: user)
+ context 'member relations' do
+ let_it_be(:child_group) { create(:group, :public, parent: parent_group) }
+ let_it_be(:grandchild_group) { create(:group, :public, parent: child_group) }
+ let_it_be(:child_user) { create(:user) }
+ let_it_be(:grandchild_user) { create(:user) }
+
+ before_all do
+ child_group.add_guest(child_user)
+ grandchild_group.add_guest(grandchild_user)
+ end
+
+ it 'returns direct members' do
+ fetch_members(group: child_group, args: { relations: [:DIRECT] })
+
+ expect(graphql_errors).to be_nil
+ expect_array_response(child_user)
+ end
+
+ it 'returns direct and inherited members' do
+ fetch_members(group: child_group, args: { relations: [:DIRECT, :INHERITED] })
+
+ expect(graphql_errors).to be_nil
+ expect_array_response(child_user, user_1, user_2)
+ end
+
+ it 'returns direct, inherited, and descendant members' do
+ fetch_members(group: child_group, args: { relations: [:DIRECT, :INHERITED, :DESCENDANTS] })
+
+ expect(graphql_errors).to be_nil
+ expect_array_response(child_user, user_1, user_2, grandchild_user)
+ end
+
+ it 'returns an error for an invalid member relation' do
+ fetch_members(group: child_group, args: { relations: [:OBLIQUE] })
+
+ expect(graphql_errors.first)
+ .to include('path' => %w[query group groupMembers relations],
+ 'message' => a_string_including('invalid value ([OBLIQUE])'))
+ end
+ end
+
+ context 'when unauthenticated' do
+ it 'returns nothing' do
+ fetch_members(current_user: nil)
+
+ expect(graphql_errors).to be_nil
+ expect(response).to have_gitlab_http_status(:success)
+ expect(member_data).to be_empty
+ end
+ end
+
+ def fetch_members(group: parent_group, current_user: user, args: {})
+ post_graphql(members_query(group.full_path, args), current_user: current_user)
end
- def members_query(args = {})
+ def members_query(group_path, args = {})
members_node = <<~NODE
edges {
node {
@@ -54,7 +105,7 @@ RSpec.describe 'getting group members information' do
NODE
graphql_query_for("group",
- { full_path: group.full_path },
+ { full_path: group_path },
[query_graphql_field("groupMembers", args, members_node)]
)
end
@@ -62,6 +113,7 @@ RSpec.describe 'getting group members information' do
def expect_array_response(*items)
expect(response).to have_gitlab_http_status(:success)
expect(member_data).to be_an Array
- expect(member_data.map { |node| node["node"]["user"]["id"] }).to match_array(items)
+ expect(member_data.map { |node| node["node"]["user"]["id"] })
+ .to match_array(items.map { |u| global_id_of(u) })
end
end
diff --git a/spec/requests/api/graphql/group_query_spec.rb b/spec/requests/api/graphql/group_query_spec.rb
index 83180c7d7a5..391bae4cfcf 100644
--- a/spec/requests/api/graphql/group_query_spec.rb
+++ b/spec/requests/api/graphql/group_query_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
# Based on spec/requests/api/groups_spec.rb
# Should follow closely in order to ensure all situations are covered
-RSpec.describe 'getting group information', :do_not_mock_admin_mode do
+RSpec.describe 'getting group information' do
include GraphqlHelpers
include UploadHelpers
diff --git a/spec/requests/api/graphql/issue/issue_spec.rb b/spec/requests/api/graphql/issue/issue_spec.rb
index d7fa680d29b..42c8e0cc9c0 100644
--- a/spec/requests/api/graphql/issue/issue_spec.rb
+++ b/spec/requests/api/graphql/issue/issue_spec.rb
@@ -125,7 +125,7 @@ RSpec.describe 'Query.issue(id)' do
let(:issue_params) { { 'id' => confidential_issue.to_global_id.to_s } }
context 'when the user cannot see confidential issues' do
- it 'returns nil ' do
+ it 'returns nil' do
post_graphql(query, current_user: current_user)
expect(issue_data).to be nil
diff --git a/spec/requests/api/graphql/mutations/admin/sidekiq_queues/delete_jobs_spec.rb b/spec/requests/api/graphql/mutations/admin/sidekiq_queues/delete_jobs_spec.rb
index 4ad35e7f0d1..b8cde32877b 100644
--- a/spec/requests/api/graphql/mutations/admin/sidekiq_queues/delete_jobs_spec.rb
+++ b/spec/requests/api/graphql/mutations/admin/sidekiq_queues/delete_jobs_spec.rb
@@ -6,8 +6,9 @@ RSpec.describe 'Deleting Sidekiq jobs', :clean_gitlab_redis_queues do
include GraphqlHelpers
let_it_be(:admin) { create(:admin) }
+ let(:queue) { 'authorized_projects' }
- let(:variables) { { user: admin.username, queue_name: 'authorized_projects' } }
+ let(:variables) { { user: admin.username, queue_name: queue } }
let(:mutation) { graphql_mutation(:admin_sidekiq_queues_delete_jobs, variables) }
def mutation_response
@@ -26,18 +27,19 @@ RSpec.describe 'Deleting Sidekiq jobs', :clean_gitlab_redis_queues do
context 'valid request' do
around do |example|
- Sidekiq::Queue.new('authorized_projects').clear
+ Sidekiq::Queue.new(queue).clear
Sidekiq::Testing.disable!(&example)
- Sidekiq::Queue.new('authorized_projects').clear
+ Sidekiq::Queue.new(queue).clear
end
def add_job(user, args)
Sidekiq::Client.push(
'class' => 'AuthorizedProjectsWorker',
- 'queue' => 'authorized_projects',
+ 'queue' => queue,
'args' => args,
'meta.user' => user.username
)
+ raise 'Not enqueued!' if Sidekiq::Queue.new(queue).size.zero?
end
it 'returns info about the deleted jobs' do
@@ -55,7 +57,7 @@ RSpec.describe 'Deleting Sidekiq jobs', :clean_gitlab_redis_queues do
end
context 'when no required params are provided' do
- let(:variables) { { queue_name: 'authorized_projects' } }
+ let(:variables) { { queue_name: queue } }
it_behaves_like 'a mutation that returns errors in the response',
errors: ['No metadata provided']
diff --git a/spec/requests/api/graphql/mutations/award_emojis/add_spec.rb b/spec/requests/api/graphql/mutations/award_emojis/add_spec.rb
index 3aaebb5095a..b39062f2e71 100644
--- a/spec/requests/api/graphql/mutations/award_emojis/add_spec.rb
+++ b/spec/requests/api/graphql/mutations/award_emojis/add_spec.rb
@@ -56,10 +56,10 @@ RSpec.describe 'Adding an AwardEmoji' do
it_behaves_like 'a mutation that does not create an AwardEmoji'
it_behaves_like 'a mutation that returns top-level errors',
- errors: ['Cannot award emoji to this resource']
+ errors: ['You cannot award emoji to this resource.']
end
- context 'when the given awardable an Awardable' do
+ context 'when the given awardable is an Awardable' do
it 'creates an emoji' do
expect do
post_graphql_mutation(mutation, current_user: current_user)
diff --git a/spec/requests/api/graphql/mutations/award_emojis/toggle_spec.rb b/spec/requests/api/graphql/mutations/award_emojis/toggle_spec.rb
index 6910ad80a11..170e7ff3b44 100644
--- a/spec/requests/api/graphql/mutations/award_emojis/toggle_spec.rb
+++ b/spec/requests/api/graphql/mutations/award_emojis/toggle_spec.rb
@@ -55,7 +55,7 @@ RSpec.describe 'Toggling an AwardEmoji' do
it_behaves_like 'a mutation that does not create or destroy an AwardEmoji'
it_behaves_like 'a mutation that returns top-level errors',
- errors: ['Cannot award emoji to this resource']
+ errors: ['You cannot award emoji to this resource.']
end
context 'when the given awardable is an Awardable' do
diff --git a/spec/requests/api/graphql/mutations/container_repository/destroy_spec.rb b/spec/requests/api/graphql/mutations/container_repository/destroy_spec.rb
index 645edfc2e43..c4121cfed42 100644
--- a/spec/requests/api/graphql/mutations/container_repository/destroy_spec.rb
+++ b/spec/requests/api/graphql/mutations/container_repository/destroy_spec.rb
@@ -22,7 +22,7 @@ RSpec.describe 'Destroying a container repository' do
GQL
end
- let(:params) { { id: container_repository.to_global_id.to_s } }
+ let(:params) { { id: id } }
let(:mutation) { graphql_mutation(:destroy_container_repository, params, query) }
let(:mutation_response) { graphql_mutation_response(:destroyContainerRepository) }
let(:container_repository_mutation_response) { mutation_response['containerRepository'] }
diff --git a/spec/requests/api/graphql/mutations/container_repository/destroy_tags_spec.rb b/spec/requests/api/graphql/mutations/container_repository/destroy_tags_spec.rb
new file mode 100644
index 00000000000..decb2e7bccc
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/container_repository/destroy_tags_spec.rb
@@ -0,0 +1,120 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Destroying a container repository tags' do
+ include_context 'container repository delete tags service shared context'
+ using RSpec::Parameterized::TableSyntax
+
+ include GraphqlHelpers
+
+ let(:id) { repository.to_global_id.to_s }
+ let(:tags) { %w[A C D E] }
+
+ let(:query) do
+ <<~GQL
+ deletedTagNames
+ errors
+ GQL
+ end
+
+ let(:params) { { id: id, tag_names: tags } }
+ let(:mutation) { graphql_mutation(:destroy_container_repository_tags, params, query) }
+ let(:mutation_response) { graphql_mutation_response(:destroyContainerRepositoryTags) }
+ let(:tag_names_response) { mutation_response['deletedTagNames'] }
+ let(:errors_response) { mutation_response['errors'] }
+
+ shared_examples 'destroying the container repository tags' do
+ before do
+ stub_delete_reference_requests(tags)
+ expect_delete_tag_by_names(tags)
+ allow_next_instance_of(ContainerRegistry::Client) do |client|
+ allow(client).to receive(:supports_tag_delete?).and_return(true)
+ end
+ end
+
+ it 'destroys the container repository tags' do
+ expect(Projects::ContainerRepository::DeleteTagsService)
+ .to receive(:new).and_call_original
+ expect { subject }.to change { ::Packages::Event.count }.by(1)
+
+ expect(tag_names_response).to eq(tags)
+ expect(errors_response).to eq([])
+ end
+
+ it_behaves_like 'returning response status', :success
+ end
+
+ shared_examples 'denying the mutation request' do
+ it 'does not destroy the container repository tags' do
+ expect(Projects::ContainerRepository::DeleteTagsService)
+ .not_to receive(:new)
+
+ expect { subject }.not_to change { ::Packages::Event.count }
+
+ expect(mutation_response).to be_nil
+ end
+
+ it_behaves_like 'returning response status', :success
+ end
+
+ describe 'post graphql mutation' do
+ subject { post_graphql_mutation(mutation, current_user: user) }
+
+ context 'with valid id' do
+ where(:user_role, :shared_examples_name) do
+ :maintainer | 'destroying the container repository tags'
+ :developer | 'destroying the container repository tags'
+ :reporter | 'denying the mutation request'
+ :guest | 'denying the mutation request'
+ :anonymous | 'denying the mutation request'
+ end
+
+ with_them do
+ before do
+ project.send("add_#{user_role}", user) unless user_role == :anonymous
+ end
+
+ it_behaves_like params[:shared_examples_name]
+ end
+ end
+
+ context 'with invalid id' do
+ let(:id) { 'gid://gitlab/ContainerRepository/5555' }
+
+ it_behaves_like 'denying the mutation request'
+ end
+
+ context 'with too many tags' do
+ let(:tags) { Array.new(Mutations::ContainerRepositories::DestroyTags::LIMIT + 1, 'x') }
+
+ it 'returns too many tags error' do
+ expect { subject }.not_to change { ::Packages::Event.count }
+
+ explanation = graphql_errors.dig(0, 'extensions', 'problems', 0, 'explanation')
+ expect(explanation).to eq(Mutations::ContainerRepositories::DestroyTags::TOO_MANY_TAGS_ERROR_MESSAGE)
+ end
+ end
+
+ context 'with service error' do
+ before do
+ project.add_maintainer(user)
+ allow_next_instance_of(Projects::ContainerRepository::DeleteTagsService) do |service|
+ allow(service).to receive(:execute).and_return(message: 'could not delete tags', status: :error)
+ end
+ end
+
+ it 'returns an error' do
+ subject
+
+ expect(tag_names_response).to eq([])
+ expect(errors_response).to eq(['could not delete tags'])
+ end
+
+ it 'does not create a package event' do
+ expect(::Packages::CreateEventService).not_to receive(:new)
+ expect { subject }.not_to change { ::Packages::Event.count }
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/environments/canary_ingress/update_spec.rb b/spec/requests/api/graphql/mutations/environments/canary_ingress/update_spec.rb
new file mode 100644
index 00000000000..f25a49291a6
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/environments/canary_ingress/update_spec.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Update Environment Canary Ingress', :clean_gitlab_redis_cache do
+ include GraphqlHelpers
+ include KubernetesHelpers
+
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:cluster) { create(:cluster, :project, projects: [project]) }
+ let_it_be(:service) { create(:cluster_platform_kubernetes, :configured, cluster: cluster) }
+ let_it_be(:environment) { create(:environment, project: project) }
+ let_it_be(:deployment) { create(:deployment, :success, environment: environment, project: project) }
+ let_it_be(:maintainer) { create(:user) }
+ let_it_be(:developer) { create(:user) }
+ let(:environment_id) { environment.to_global_id.to_s }
+ let(:weight) { 25 }
+ let(:actor) { developer }
+
+ let(:mutation) do
+ graphql_mutation(:environments_canary_ingress_update, id: environment_id, weight: weight)
+ end
+
+ before_all do
+ project.add_maintainer(maintainer)
+ project.add_developer(developer)
+ end
+
+ before do
+ stub_kubeclient_ingresses(environment.deployment_namespace, response: kube_ingresses_response(with_canary: true))
+ end
+
+ context 'when kubernetes accepted the patch request' do
+ before do
+ stub_kubeclient_ingresses(environment.deployment_namespace, method: :patch, resource_path: "/production-auto-deploy")
+ end
+
+ it 'updates successfully' do
+ post_graphql_mutation(mutation, current_user: actor)
+
+ expect(graphql_mutation_response(:environments_canary_ingress_update)['errors'])
+ .to be_empty
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/releases/delete_spec.rb b/spec/requests/api/graphql/mutations/releases/delete_spec.rb
new file mode 100644
index 00000000000..3710f118bf4
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/releases/delete_spec.rb
@@ -0,0 +1,132 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Deleting a release' do
+ include GraphqlHelpers
+ include Presentable
+
+ let_it_be(:public_user) { create(:user) }
+ let_it_be(:guest) { create(:user) }
+ let_it_be(:reporter) { create(:user) }
+ let_it_be(:developer) { create(:user) }
+ let_it_be(:maintainer) { create(:user) }
+ let_it_be(:project) { create(:project, :public, :repository) }
+ let_it_be(:tag_name) { 'v1.1.0' }
+ let_it_be(:release) { create(:release, project: project, tag: tag_name) }
+
+ let(:mutation_name) { :release_delete }
+
+ let(:project_path) { project.full_path }
+ let(:mutation_arguments) do
+ {
+ projectPath: project_path,
+ tagName: tag_name
+ }
+ end
+
+ let(:mutation) do
+ graphql_mutation(mutation_name, mutation_arguments, <<~FIELDS)
+ release {
+ tagName
+ }
+ errors
+ FIELDS
+ end
+
+ let(:delete_release) { post_graphql_mutation(mutation, current_user: current_user) }
+ let(:mutation_response) { graphql_mutation_response(mutation_name)&.with_indifferent_access }
+
+ before do
+ project.add_guest(guest)
+ project.add_reporter(reporter)
+ project.add_developer(developer)
+ project.add_maintainer(maintainer)
+ end
+
+ shared_examples 'unauthorized or not found error' do
+ it 'returns a top-level error with message' do
+ delete_release
+
+ expect(mutation_response).to be_nil
+ expect(graphql_errors.count).to eq(1)
+ expect(graphql_errors.first['message']).to eq("The resource that you are attempting to access does not exist or you don't have permission to perform this action")
+ end
+ end
+
+ context 'when the current user has access to update releases' do
+ let(:current_user) { maintainer }
+
+ it 'deletes the release' do
+ expect { delete_release }.to change { Release.count }.by(-1)
+ end
+
+ it 'returns the deleted release' do
+ delete_release
+
+ expected_release = { tagName: tag_name }.with_indifferent_access
+
+ expect(mutation_response[:release]).to eq(expected_release)
+ end
+
+ it 'does not remove the Git tag associated with the deleted release' do
+ expect { delete_release }.not_to change { Project.find_by_id(project.id).repository.tag_count }
+ end
+
+ it 'returns no errors' do
+ delete_release
+
+ expect(mutation_response[:errors]).to eq([])
+ end
+
+ context 'validation' do
+ context 'when the release does not exist' do
+ let_it_be(:tag_name) { 'not-a-real-release' }
+
+ it 'returns the release as null' do
+ delete_release
+
+ expect(mutation_response[:release]).to be_nil
+ end
+
+ it 'returns an errors-at-data message' do
+ delete_release
+
+ expect(mutation_response[:errors]).to eq(['Release does not exist'])
+ end
+ end
+
+ context 'when the project does not exist' do
+ let(:project_path) { 'not/a/real/path' }
+
+ it_behaves_like 'unauthorized or not found error'
+ end
+ end
+ end
+
+ context "when the current user doesn't have access to update releases" do
+ context 'when the current user is a Developer' do
+ let(:current_user) { developer }
+
+ it_behaves_like 'unauthorized or not found error'
+ end
+
+ context 'when the current user is a Reporter' do
+ let(:current_user) { reporter }
+
+ it_behaves_like 'unauthorized or not found error'
+ end
+
+ context 'when the current user is a Guest' do
+ let(:current_user) { guest }
+
+ it_behaves_like 'unauthorized or not found error'
+ end
+
+ context 'when the current user is a public user' do
+ let(:current_user) { public_user }
+
+ it_behaves_like 'unauthorized or not found error'
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/releases/update_spec.rb b/spec/requests/api/graphql/mutations/releases/update_spec.rb
new file mode 100644
index 00000000000..19320c3393c
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/releases/update_spec.rb
@@ -0,0 +1,255 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Updating an existing release' do
+ include GraphqlHelpers
+ include Presentable
+
+ let_it_be(:public_user) { create(:user) }
+ let_it_be(:guest) { create(:user) }
+ let_it_be(:reporter) { create(:user) }
+ let_it_be(:developer) { create(:user) }
+ let_it_be(:project) { create(:project, :public, :repository) }
+ let_it_be(:milestone_12_3) { create(:milestone, project: project, title: '12.3') }
+ let_it_be(:milestone_12_4) { create(:milestone, project: project, title: '12.4') }
+
+ let_it_be(:tag_name) { 'v1.1.0' }
+ let_it_be(:name) { 'Version 7.12.5'}
+ let_it_be(:description) { 'Release 7.12.5 :rocket:' }
+ let_it_be(:released_at) { '2018-12-10' }
+ let_it_be(:created_at) { '2018-11-05' }
+ let_it_be(:milestones) { [milestone_12_3, milestone_12_4] }
+
+ let_it_be(:release) do
+ create(:release, project: project, tag: tag_name, name: name,
+ description: description, released_at: Time.parse(released_at).utc,
+ created_at: Time.parse(created_at).utc, milestones: milestones)
+ end
+
+ let(:mutation_name) { :release_update }
+
+ let(:mutation_arguments) do
+ {
+ projectPath: project.full_path,
+ tagName: tag_name
+ }
+ end
+
+ let(:mutation) do
+ graphql_mutation(mutation_name, mutation_arguments, <<~FIELDS)
+ release {
+ tagName
+ name
+ description
+ releasedAt
+ createdAt
+ milestones {
+ nodes {
+ title
+ }
+ }
+ }
+ errors
+ FIELDS
+ end
+
+ let(:update_release) { post_graphql_mutation(mutation, current_user: current_user) }
+ let(:mutation_response) { graphql_mutation_response(mutation_name)&.with_indifferent_access }
+
+ let(:expected_attributes) do
+ {
+ tagName: tag_name,
+ name: name,
+ description: description,
+ releasedAt: Time.parse(released_at).utc.iso8601,
+ createdAt: Time.parse(created_at).utc.iso8601,
+ milestones: {
+ nodes: milestones.map { |m| { title: m.title } }
+ }
+ }.with_indifferent_access
+ end
+
+ around do |example|
+ freeze_time { example.run }
+ end
+
+ before do
+ project.add_guest(guest)
+ project.add_reporter(reporter)
+ project.add_developer(developer)
+
+ stub_default_url_options(host: 'www.example.com')
+ end
+
+ shared_examples 'no errors' do
+ it 'returns no errors' do
+ update_release
+
+ expect(graphql_errors).not_to be_present
+ end
+ end
+
+ shared_examples 'top-level error with message' do |error_message|
+ it 'returns a top-level error with message' do
+ update_release
+
+ expect(mutation_response).to be_nil
+ expect(graphql_errors.count).to eq(1)
+ expect(graphql_errors.first['message']).to eq(error_message)
+ end
+ end
+
+ shared_examples 'errors-as-data with message' do |error_message|
+ it 'returns an error-as-data with message' do
+ update_release
+
+ expect(mutation_response[:release]).to be_nil
+ expect(mutation_response[:errors].count).to eq(1)
+ expect(mutation_response[:errors].first).to match(error_message)
+ end
+ end
+
+ shared_examples 'updates release fields' do |updates|
+ it_behaves_like 'no errors'
+
+ it 'updates the correct field and returns the release' do
+ update_release
+
+ expect(mutation_response[:release]).to include(expected_attributes.merge(updates).except(:milestones))
+
+ # Right now the milestones are returned in a non-deterministic order.
+ # Because of this, we need to test milestones separately to allow
+ # for them to be returned in any order.
+ # Once https://gitlab.com/gitlab-org/gitlab/-/issues/259012 has been
+ # fixed, this special milestone handling can be removed.
+ expected_milestones = expected_attributes.merge(updates)[:milestones]
+ expect(mutation_response[:release][:milestones][:nodes]).to match_array(expected_milestones[:nodes])
+ end
+ end
+
+ context 'when the current user has access to update releases' do
+ let(:current_user) { developer }
+
+ context 'name' do
+ context 'when a new name is provided' do
+ let(:mutation_arguments) { super().merge(name: 'Updated name') }
+
+ it_behaves_like 'updates release fields', name: 'Updated name'
+ end
+
+ context 'when null is provided' do
+ let(:mutation_arguments) { super().merge(name: nil) }
+
+ it_behaves_like 'updates release fields', name: 'v1.1.0'
+ end
+ end
+
+ context 'description' do
+ context 'when a new description is provided' do
+ let(:mutation_arguments) { super().merge(description: 'Updated description') }
+
+ it_behaves_like 'updates release fields', description: 'Updated description'
+ end
+
+ context 'when null is provided' do
+ let(:mutation_arguments) { super().merge(description: nil) }
+
+ it_behaves_like 'updates release fields', description: nil
+ end
+ end
+
+ context 'releasedAt' do
+ context 'when no time zone is provided' do
+ let(:mutation_arguments) { super().merge(releasedAt: '2015-05-05') }
+
+ it_behaves_like 'updates release fields', releasedAt: Time.parse('2015-05-05').utc.iso8601
+ end
+
+ context 'when a local time zone is provided' do
+ let(:mutation_arguments) { super().merge(releasedAt: Time.parse('2015-05-05').in_time_zone('Hawaii').iso8601) }
+
+ it_behaves_like 'updates release fields', releasedAt: Time.parse('2015-05-05').utc.iso8601
+ end
+
+ context 'when null is provided' do
+ let(:mutation_arguments) { super().merge(releasedAt: nil) }
+
+ it_behaves_like 'top-level error with message', 'if the releasedAt argument is provided, it cannot be null'
+ end
+ end
+
+ context 'milestones' do
+ context 'when a new set of milestones is provided provided' do
+ let(:mutation_arguments) { super().merge(milestones: ['12.3']) }
+
+ it_behaves_like 'updates release fields', milestones: { nodes: [{ title: '12.3' }] }
+ end
+
+ context 'when an empty array is provided' do
+ let(:mutation_arguments) { super().merge(milestones: []) }
+
+ it_behaves_like 'updates release fields', milestones: { nodes: [] }
+ end
+
+ context 'when null is provided' do
+ let(:mutation_arguments) { super().merge(milestones: nil) }
+
+ it_behaves_like 'top-level error with message', 'if the milestones argument is provided, it cannot be null'
+ end
+
+ context 'when a non-existent milestone title is provided' do
+ let(:mutation_arguments) { super().merge(milestones: ['not real']) }
+
+ it_behaves_like 'errors-as-data with message', 'Milestone(s) not found: not real'
+ end
+
+ context 'when a milestone title from a different project is provided' do
+ let(:milestone_in_different_project) { create(:milestone, title: 'milestone in different project') }
+ let(:mutation_arguments) { super().merge(milestones: [milestone_in_different_project.title]) }
+
+ it_behaves_like 'errors-as-data with message', 'Milestone(s) not found: milestone in different project'
+ end
+ end
+
+ context 'validation' do
+ context 'when no updated fields are provided' do
+ it_behaves_like 'errors-as-data with message', 'params is empty'
+ end
+
+ context 'when the tag does not exist' do
+ let(:mutation_arguments) { super().merge(tagName: 'not-a-real-tag') }
+
+ it_behaves_like 'errors-as-data with message', 'Tag does not exist'
+ end
+
+ context 'when the project does not exist' do
+ let(:mutation_arguments) { super().merge(projectPath: 'not/a/real/path') }
+
+ it_behaves_like 'top-level error with message', "The resource that you are attempting to access does not exist or you don't have permission to perform this action"
+ end
+ end
+ end
+
+ context "when the current user doesn't have access to update releases" do
+ expected_error_message = "The resource that you are attempting to access does not exist or you don't have permission to perform this action"
+
+ context 'when the current user is a Reporter' do
+ let(:current_user) { reporter }
+
+ it_behaves_like 'top-level error with message', expected_error_message
+ end
+
+ context 'when the current user is a Guest' do
+ let(:current_user) { guest }
+
+ it_behaves_like 'top-level error with message', expected_error_message
+ end
+
+ context 'when the current user is a public user' do
+ let(:current_user) { public_user }
+
+ it_behaves_like 'top-level error with message', expected_error_message
+ end
+ 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 d2fa3cfc24f..fd0dc98a8d3 100644
--- a/spec/requests/api/graphql/mutations/snippets/create_spec.rb
+++ b/spec/requests/api/graphql/mutations/snippets/create_spec.rb
@@ -163,9 +163,15 @@ RSpec.describe 'Creating a Snippet' do
context 'when there are uploaded files' do
shared_examples 'expected files argument' do |file_value, expected_value|
let(:uploaded_files) { file_value }
+ let(:snippet) { build(:snippet) }
+ let(:creation_response) do
+ ::ServiceResponse.error(message: 'urk', payload: { snippet: snippet })
+ end
it do
- expect(::Snippets::CreateService).to receive(:new).with(nil, user, hash_including(files: expected_value))
+ expect(::Snippets::CreateService).to receive(:new)
+ .with(nil, user, hash_including(files: expected_value))
+ .and_return(double(execute: creation_response))
subject
end
diff --git a/spec/requests/api/graphql/mutations/snippets/mark_as_spam_spec.rb b/spec/requests/api/graphql/mutations/snippets/mark_as_spam_spec.rb
index 97e6ae8fda8..4d499310591 100644
--- a/spec/requests/api/graphql/mutations/snippets/mark_as_spam_spec.rb
+++ b/spec/requests/api/graphql/mutations/snippets/mark_as_spam_spec.rb
@@ -2,8 +2,9 @@
require 'spec_helper'
-RSpec.describe 'Mark snippet as spam', :do_not_mock_admin_mode do
+RSpec.describe 'Mark snippet as spam' do
include GraphqlHelpers
+ include AfterNextHelpers
let_it_be(:admin) { create(:admin) }
let_it_be(:other_user) { create(:user) }
@@ -56,11 +57,12 @@ RSpec.describe 'Mark snippet as spam', :do_not_mock_admin_mode do
end
it 'marks snippet as spam' do
- expect_next_instance_of(Spam::MarkAsSpamService) do |instance|
- expect(instance).to receive(:execute)
- end
+ expect_next(Spam::MarkAsSpamService, target: snippet)
+ .to receive(:execute).and_return(true)
post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(graphql_errors).to be_blank
end
end
end
diff --git a/spec/requests/api/graphql/namespace/projects_spec.rb b/spec/requests/api/graphql/namespace/projects_spec.rb
index 03160719389..3e68503b7fb 100644
--- a/spec/requests/api/graphql/namespace/projects_spec.rb
+++ b/spec/requests/api/graphql/namespace/projects_spec.rb
@@ -80,38 +80,34 @@ RSpec.describe 'getting projects' do
end
describe 'sorting and pagination' do
+ let_it_be(:ns) { create(:group) }
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:project_1) { create(:project, name: 'Project', path: 'project', namespace: ns) }
+ let_it_be(:project_2) { create(:project, name: 'Test Project', path: 'test-project', namespace: ns) }
+ let_it_be(:project_3) { create(:project, name: 'Test', path: 'test', namespace: ns) }
+ let_it_be(:project_4) { create(:project, name: 'Test Project Other', path: 'other-test-project', namespace: ns) }
+
let(:data_path) { [:namespace, :projects] }
- def pagination_query(params, page_info)
- graphql_query_for(
- 'namespace',
- { 'fullPath' => subject.full_path },
- <<~QUERY
- projects(includeSubgroups: #{include_subgroups}, search: "#{search}", #{params}) {
- #{page_info} edges {
- node {
- #{all_graphql_fields_for('Project')}
- }
- }
- }
- QUERY
- )
+ let(:ns_args) { { full_path: ns.full_path } }
+ let(:search) { 'test' }
+
+ before do
+ ns.add_owner(current_user)
end
- def pagination_results_data(data)
- data.map { |project| project.dig('node', 'name') }
+ def pagination_query(params)
+ arguments = params.merge(include_subgroups: include_subgroups, search: search)
+ graphql_query_for(:namespace, ns_args, query_graphql_field(:projects, arguments, <<~GQL))
+ #{page_info}
+ nodes { name }
+ GQL
end
context 'when sorting by similarity' do
- let!(:project_1) { create(:project, name: 'Project', path: 'project', namespace: subject) }
- let!(:project_2) { create(:project, name: 'Test Project', path: 'test-project', namespace: subject) }
- let!(:project_3) { create(:project, name: 'Test', path: 'test', namespace: subject) }
- let!(:project_4) { create(:project, name: 'Test Project Other', path: 'other-test-project', namespace: subject) }
- let(:search) { 'test' }
- let(:current_user) { user }
-
it_behaves_like 'sorted paginated query' do
- let(:sort_param) { 'SIMILARITY' }
+ let(:node_path) { %w[name] }
+ let(:sort_param) { :SIMILARITY }
let(:first_param) { 2 }
let(:expected_results) { [project_3.name, project_2.name, project_4.name] }
end
diff --git a/spec/requests/api/graphql/project/container_repositories_spec.rb b/spec/requests/api/graphql/project/container_repositories_spec.rb
index 7e32f54bf1d..6b1c8689515 100644
--- a/spec/requests/api/graphql/project/container_repositories_spec.rb
+++ b/spec/requests/api/graphql/project/container_repositories_spec.rb
@@ -12,7 +12,7 @@ RSpec.describe 'getting container repositories in a project' do
let_it_be(:container_repositories) { [container_repository, container_repositories_delete_scheduled, container_repositories_delete_failed].flatten }
let_it_be(:container_expiration_policy) { project.container_expiration_policy }
- let(:fields) do
+ let(:container_repositories_fields) do
<<~GQL
edges {
node {
@@ -22,17 +22,25 @@ RSpec.describe 'getting container repositories in a project' do
GQL
end
+ let(:fields) do
+ <<~GQL
+ #{query_graphql_field('container_repositories', {}, container_repositories_fields)}
+ containerRepositoriesCount
+ GQL
+ end
+
let(:query) do
graphql_query_for(
'project',
{ 'fullPath' => project.full_path },
- query_graphql_field('container_repositories', {}, fields)
+ fields
)
end
let(:user) { project.owner }
let(:variables) { {} }
let(:container_repositories_response) { graphql_data.dig('project', 'containerRepositories', 'edges') }
+ let(:container_repositories_count_response) { graphql_data.dig('project', 'containerRepositoriesCount') }
before do
stub_container_registry_config(enabled: true)
@@ -100,7 +108,7 @@ RSpec.describe 'getting container repositories in a project' do
<<~GQL
query($path: ID!, $n: Int) {
project(fullPath: $path) {
- containerRepositories(first: $n) { #{fields} }
+ containerRepositories(first: $n) { #{container_repositories_fields} }
}
}
GQL
@@ -121,7 +129,7 @@ RSpec.describe 'getting container repositories in a project' do
<<~GQL
query($path: ID!, $name: String) {
project(fullPath: $path) {
- containerRepositories(name: $name) { #{fields} }
+ containerRepositories(name: $name) { #{container_repositories_fields} }
}
}
GQL
@@ -142,4 +150,10 @@ RSpec.describe 'getting container repositories in a project' do
expect(container_repositories_response.first.dig('node', 'id')).to eq(container_repository.to_global_id.to_s)
end
end
+
+ it 'returns the total count of container repositories' do
+ subject
+
+ expect(container_repositories_count_response).to eq(container_repositories.size)
+ end
end
diff --git a/spec/requests/api/graphql/project/issue/design_collection/version_spec.rb b/spec/requests/api/graphql/project/issue/design_collection/version_spec.rb
index 4bce3c7fe0f..f544d78ecbb 100644
--- a/spec/requests/api/graphql/project/issue/design_collection/version_spec.rb
+++ b/spec/requests/api/graphql/project/issue/design_collection/version_spec.rb
@@ -42,7 +42,6 @@ RSpec.describe 'Query.project(fullPath).issue(iid).designCollection.version(sha)
describe 'scalar fields' do
let(:path) { path_prefix }
- let(:version_fields) { query_graphql_field(:sha) }
before do
post_query
@@ -50,7 +49,7 @@ RSpec.describe 'Query.project(fullPath).issue(iid).designCollection.version(sha)
{ id: ->(x) { x.to_global_id.to_s }, sha: ->(x) { x.sha } }.each do |field, value|
describe ".#{field}" do
- let(:version_fields) { query_graphql_field(field) }
+ let(:version_fields) { field }
it "retrieves the #{field}" do
expect(data).to match(a_hash_including(field.to_s => value[version]))
diff --git a/spec/requests/api/graphql/project/issue_spec.rb b/spec/requests/api/graphql/project/issue_spec.rb
index 5f368833181..ddf63a8f2c9 100644
--- a/spec/requests/api/graphql/project/issue_spec.rb
+++ b/spec/requests/api/graphql/project/issue_spec.rb
@@ -29,8 +29,8 @@ RSpec.describe 'Query.project(fullPath).issue(iid)' do
let(:design_fields) do
[
- query_graphql_field(:filename),
- query_graphql_field(:project, nil, query_graphql_field(:id))
+ :filename,
+ query_graphql_field(:project, :id)
]
end
@@ -173,7 +173,7 @@ RSpec.describe 'Query.project(fullPath).issue(iid)' do
let(:result_fields) { { 'version' => id_hash(version) } }
let(:object_fields) do
- design_fields + [query_graphql_field(:version, nil, query_graphql_field(:id))]
+ design_fields + [query_graphql_field(:version, :id)]
end
let(:no_argument_error) { missing_required_argument(path, :id) }
diff --git a/spec/requests/api/graphql/project/issues_spec.rb b/spec/requests/api/graphql/project/issues_spec.rb
index 4f27f08bf98..9c915075c42 100644
--- a/spec/requests/api/graphql/project/issues_spec.rb
+++ b/spec/requests/api/graphql/project/issues_spec.rb
@@ -142,16 +142,14 @@ RSpec.describe 'getting an issue list for a project' do
describe 'sorting and pagination' do
let_it_be(:data_path) { [:project, :issues] }
- def pagination_query(params, page_info)
- graphql_query_for(
- 'project',
- { 'fullPath' => sort_project.full_path },
- query_graphql_field('issues', params, "#{page_info} edges { node { iid dueDate} }")
+ def pagination_query(params)
+ 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('node', 'iid').to_i }
+ data.map { |issue| issue.dig('iid').to_i }
end
context 'when sorting by due date' do
@@ -164,7 +162,7 @@ RSpec.describe 'getting an issue list for a project' do
context 'when ascending' do
it_behaves_like 'sorted paginated query' do
- let(:sort_param) { 'DUE_DATE_ASC' }
+ let(:sort_param) { :DUE_DATE_ASC }
let(:first_param) { 2 }
let(:expected_results) { [due_issue3.iid, due_issue5.iid, due_issue1.iid, due_issue4.iid, due_issue2.iid] }
end
@@ -172,7 +170,7 @@ RSpec.describe 'getting an issue list for a project' do
context 'when descending' do
it_behaves_like 'sorted paginated query' do
- let(:sort_param) { 'DUE_DATE_DESC' }
+ let(:sort_param) { :DUE_DATE_DESC }
let(:first_param) { 2 }
let(:expected_results) { [due_issue1.iid, due_issue5.iid, due_issue3.iid, due_issue4.iid, due_issue2.iid] }
end
@@ -189,7 +187,7 @@ RSpec.describe 'getting an issue list for a project' do
context 'when ascending' do
it_behaves_like 'sorted paginated query' do
- let(:sort_param) { 'RELATIVE_POSITION_ASC' }
+ 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] }
end
@@ -209,7 +207,7 @@ RSpec.describe 'getting an issue list for a project' do
context 'when ascending' do
it_behaves_like 'sorted paginated query' do
- let(:sort_param) { 'PRIORITY_ASC' }
+ let(:sort_param) { :PRIORITY_ASC }
let(:first_param) { 2 }
let(:expected_results) { [priority_issue3.iid, priority_issue1.iid, priority_issue2.iid, priority_issue4.iid] }
end
@@ -217,7 +215,7 @@ RSpec.describe 'getting an issue list for a project' do
context 'when descending' do
it_behaves_like 'sorted paginated query' do
- let(:sort_param) { 'PRIORITY_DESC' }
+ let(:sort_param) { :PRIORITY_DESC }
let(:first_param) { 2 }
let(:expected_results) { [priority_issue1.iid, priority_issue3.iid, priority_issue2.iid, priority_issue4.iid] }
end
@@ -236,7 +234,7 @@ RSpec.describe 'getting an issue list for a project' do
context 'when ascending' do
it_behaves_like 'sorted paginated query' do
- let(:sort_param) { 'LABEL_PRIORITY_ASC' }
+ let(:sort_param) { :LABEL_PRIORITY_ASC }
let(:first_param) { 2 }
let(:expected_results) { [label_issue3.iid, label_issue1.iid, label_issue2.iid, label_issue4.iid] }
end
@@ -244,7 +242,7 @@ RSpec.describe 'getting an issue list for a project' do
context 'when descending' do
it_behaves_like 'sorted paginated query' do
- let(:sort_param) { 'LABEL_PRIORITY_DESC' }
+ let(:sort_param) { :LABEL_PRIORITY_DESC }
let(:first_param) { 2 }
let(:expected_results) { [label_issue2.iid, label_issue3.iid, label_issue1.iid, label_issue4.iid] }
end
@@ -261,7 +259,7 @@ RSpec.describe 'getting an issue list for a project' do
context 'when ascending' do
it_behaves_like 'sorted paginated query' do
- let(:sort_param) { 'MILESTONE_DUE_ASC' }
+ let(:sort_param) { :MILESTONE_DUE_ASC }
let(:first_param) { 2 }
let(:expected_results) { [milestone_issue2.iid, milestone_issue3.iid, milestone_issue1.iid] }
end
@@ -269,7 +267,7 @@ RSpec.describe 'getting an issue list for a project' do
context 'when descending' do
it_behaves_like 'sorted paginated query' do
- let(:sort_param) { 'MILESTONE_DUE_DESC' }
+ let(:sort_param) { :MILESTONE_DUE_DESC }
let(:first_param) { 2 }
let(:expected_results) { [milestone_issue3.iid, milestone_issue2.iid, milestone_issue1.iid] }
end
diff --git a/spec/requests/api/graphql/project/merge_request/pipelines_spec.rb b/spec/requests/api/graphql/project/merge_request/pipelines_spec.rb
new file mode 100644
index 00000000000..ac0b18a37d6
--- /dev/null
+++ b/spec/requests/api/graphql/project/merge_request/pipelines_spec.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Query.project.mergeRequests.pipelines' do
+ include GraphqlHelpers
+
+ let_it_be(:project) { create(:project, :public, :repository) }
+ let_it_be(:author) { create(:user) }
+ let_it_be(:merge_requests) do
+ %i[with_diffs with_image_diffs conflict].map do |trait|
+ create(:merge_request, trait, author: author, source_project: project)
+ end
+ end
+
+ describe '.count' do
+ let(:query) do
+ <<~GQL
+ query($path: ID!, $first: Int) {
+ project(fullPath: $path) {
+ mergeRequests(first: $first) {
+ nodes {
+ iid
+ pipelines {
+ count
+ }
+ }
+ }
+ }
+ }
+ GQL
+ end
+
+ def run_query(first = nil)
+ post_graphql(query, current_user: author, variables: { path: project.full_path, first: first })
+ end
+
+ before do
+ merge_requests.each do |mr|
+ shas = mr.all_commits.limit(2).pluck(:sha)
+
+ shas.each do |sha|
+ create(:ci_pipeline, :success, project: project, ref: mr.source_branch, sha: sha)
+ end
+ end
+ end
+
+ it 'produces correct results' do
+ run_query(2)
+
+ p_nodes = graphql_data_at(:project, :merge_requests, :nodes)
+
+ expect(p_nodes).to all(match('iid' => be_present, 'pipelines' => match('count' => 2)))
+ end
+
+ it 'is scalable', :request_store, :use_clean_rails_memory_store_caching do
+ # warm up
+ run_query
+
+ expect { run_query(2) }.to(issue_same_number_of_queries_as { run_query(1) }.ignoring_cached_queries)
+ 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 2b8d537f9fc..c05a620bb62 100644
--- a/spec/requests/api/graphql/project/merge_requests_spec.rb
+++ b/spec/requests/api/graphql/project/merge_requests_spec.rb
@@ -259,29 +259,19 @@ RSpec.describe 'getting merge request listings nested in a project' do
describe 'sorting and pagination' do
let(:data_path) { [:project, :mergeRequests] }
- def pagination_query(params, page_info)
- graphql_query_for(
- :project,
- { full_path: project.full_path },
+ def pagination_query(params)
+ graphql_query_for(:project, { full_path: project.full_path },
<<~QUERY
mergeRequests(#{params}) {
- #{page_info} edges {
- node {
- id
- }
- }
+ #{page_info} nodes { id }
}
QUERY
)
end
- def pagination_results_data(data)
- data.map { |project| project.dig('node', 'id') }
- end
-
context 'when sorting by merged_at DESC' do
it_behaves_like 'sorted paginated query' do
- let(:sort_param) { 'MERGED_AT_DESC' }
+ let(:sort_param) { :MERGED_AT_DESC }
let(:first_param) { 2 }
let(:expected_results) do
@@ -291,7 +281,7 @@ RSpec.describe 'getting merge request listings nested in a project' do
merge_request_c,
merge_request_e,
merge_request_a
- ].map(&:to_gid).map(&:to_s)
+ ].map { |mr| global_id_of(mr) }
end
before do
@@ -304,33 +294,6 @@ RSpec.describe 'getting merge request listings nested in a project' do
merge_request_b.metrics.update!(merged_at: 1.day.ago)
end
-
- context 'when paginating backwards' do
- let(:params) { 'first: 2, sort: MERGED_AT_DESC' }
- let(:page_info) { 'pageInfo { startCursor endCursor }' }
-
- before do
- post_graphql(pagination_query(params, page_info), current_user: current_user)
- end
-
- it 'paginates backwards correctly' do
- # first page
- first_page_response_data = graphql_dig_at(Gitlab::Json.parse(response.body), :data, *data_path, :edges)
- end_cursor = graphql_dig_at(Gitlab::Json.parse(response.body), :data, :project, :mergeRequests, :pageInfo, :endCursor)
-
- # second page
- params = "first: 2, after: \"#{end_cursor}\", sort: MERGED_AT_DESC"
- post_graphql(pagination_query(params, page_info), current_user: current_user)
- start_cursor = graphql_dig_at(Gitlab::Json.parse(response.body), :data, :project, :mergeRequests, :pageInfo, :start_cursor)
-
- # going back to the first page
-
- params = "last: 2, before: \"#{start_cursor}\", sort: MERGED_AT_DESC"
- post_graphql(pagination_query(params, page_info), current_user: current_user)
- backward_paginated_response_data = graphql_dig_at(Gitlab::Json.parse(response.body), :data, *data_path, :edges)
- expect(first_page_response_data).to eq(backward_paginated_response_data)
- end
- end
end
end
end
diff --git a/spec/requests/api/graphql/project/pipeline_spec.rb b/spec/requests/api/graphql/project/pipeline_spec.rb
index fef0e7e160c..6179b43629b 100644
--- a/spec/requests/api/graphql/project/pipeline_spec.rb
+++ b/spec/requests/api/graphql/project/pipeline_spec.rb
@@ -5,12 +5,12 @@ 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!(: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
+ let!(:query) do
graphql_query_for(
'project',
{ 'fullPath' => project.full_path },
@@ -35,4 +35,45 @@ RSpec.describe 'getting pipeline information nested in a project' do
expect(pipeline_graphql_data.dig('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)]) }
+ let!(:query) { build_query_to_find_pipeline_shas(pipeline, pipeline2, pipeline3) }
+
+ it 'executes the finder once' do
+ mock = double(Ci::PipelinesFinder)
+ opts = { iids: [pipeline.iid, pipeline2.iid, pipeline3.iid].map(&:to_s) }
+
+ expect(Ci::PipelinesFinder).to receive(:new).once.with(project, current_user, opts).and_return(mock)
+ expect(mock).to receive(:execute).once.and_return(Ci::Pipeline.none)
+
+ post_graphql(query, current_user: current_user)
+ end
+
+ it 'keeps the queries under the threshold' do
+ control = ActiveRecord::QueryRecorder.new do
+ single_pipeline_query = build_query_to_find_pipeline_shas(pipeline)
+
+ post_graphql(single_pipeline_query, current_user: current_user)
+ end
+
+ aggregate_failures do
+ expect(response).to have_gitlab_http_status(:success)
+ expect do
+ post_graphql(query, current_user: current_user)
+ end.not_to exceed_query_limit(control)
+ end
+ end
+ end
+
+ private
+
+ def build_query_to_find_pipeline_shas(*pipelines)
+ pipeline_fields = pipelines.map.each_with_index do |pipeline, idx|
+ "pipeline#{idx}: pipeline(iid: \"#{pipeline.iid}\") { sha }"
+ end.join(' ')
+
+ graphql_query_for('project', { 'fullPath' => project.full_path }, pipeline_fields)
+ end
end
diff --git a/spec/requests/api/graphql/project/project_members_spec.rb b/spec/requests/api/graphql/project/project_members_spec.rb
new file mode 100644
index 00000000000..cb937432ef7
--- /dev/null
+++ b/spec/requests/api/graphql/project/project_members_spec.rb
@@ -0,0 +1,121 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'getting project members information' do
+ include GraphqlHelpers
+
+ let_it_be(:parent_group) { create(:group, :public) }
+ let_it_be(:parent_project) { create(:project, :public, group: parent_group) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:user_1) { create(:user, username: 'user') }
+ let_it_be(:user_2) { create(:user, username: 'test') }
+
+ let(:member_data) { graphql_data['project']['projectMembers']['edges'] }
+
+ before_all do
+ [user_1, user_2].each { |user| parent_group.add_guest(user) }
+ end
+
+ context 'when the request is correct' do
+ it_behaves_like 'a working graphql query' do
+ before_all do
+ fetch_members(project: parent_project)
+ end
+ end
+
+ it 'returns project members successfully' do
+ fetch_members(project: parent_project)
+
+ expect(graphql_errors).to be_nil
+ expect_array_response(user_1, user_2)
+ end
+
+ it 'returns members that match the search query' do
+ fetch_members(project: parent_project, args: { search: 'test' })
+
+ expect(graphql_errors).to be_nil
+ expect_array_response(user_2)
+ end
+ end
+
+ context 'member relations' do
+ let_it_be(:child_group) { create(:group, :public, parent: parent_group) }
+ let_it_be(:child_project) { create(:project, :public, group: child_group) }
+ let_it_be(:invited_group) { create(:group, :public) }
+ let_it_be(:child_user) { create(:user) }
+ let_it_be(:invited_user) { create(:user) }
+ let_it_be(:group_link) { create(:project_group_link, project: child_project, group: invited_group) }
+
+ before_all do
+ child_project.add_guest(child_user)
+ invited_group.add_guest(invited_user)
+ end
+
+ it 'returns direct members' do
+ fetch_members(project: child_project, args: { relations: [:DIRECT] })
+
+ expect(graphql_errors).to be_nil
+ expect_array_response(child_user)
+ end
+
+ it 'returns invited members plus inherited members' do
+ fetch_members(project: child_project, args: { relations: [:INVITED_GROUPS] })
+
+ expect(graphql_errors).to be_nil
+ expect_array_response(invited_user, user_1, user_2)
+ end
+
+ it 'returns direct, inherited, descendant, and invited members' do
+ fetch_members(project: child_project, args: { relations: [:DIRECT, :INHERITED, :DESCENDANTS, :INVITED_GROUPS] })
+
+ expect(graphql_errors).to be_nil
+ expect_array_response(child_user, user_1, user_2, invited_user)
+ end
+
+ it 'returns an error for an invalid member relation' do
+ fetch_members(project: child_project, args: { relations: [:OBLIQUE] })
+
+ expect(graphql_errors.first)
+ .to include('path' => %w[query project projectMembers relations],
+ 'message' => a_string_including('invalid value ([OBLIQUE])'))
+ end
+ end
+
+ context 'when unauthenticated' do
+ it 'returns members' do
+ fetch_members(current_user: nil, project: parent_project)
+
+ expect(graphql_errors).to be_nil
+ expect_array_response(user_1, user_2)
+ end
+ end
+
+ def fetch_members(project:, current_user: user, args: {})
+ post_graphql(members_query(project.full_path, args), current_user: current_user)
+ end
+
+ def members_query(group_path, args = {})
+ members_node = <<~NODE
+ edges {
+ node {
+ user {
+ id
+ }
+ }
+ }
+ NODE
+
+ graphql_query_for('project',
+ { full_path: group_path },
+ [query_graphql_field('projectMembers', args, members_node)]
+ )
+ end
+
+ def expect_array_response(*items)
+ expect(response).to have_gitlab_http_status(:success)
+ expect(member_data).to be_an Array
+ expect(member_data.map { |node| node['node']['user']['id'] })
+ .to match_array(items.map { |u| global_id_of(u) })
+ end
+end
diff --git a/spec/requests/api/graphql/project/project_pipeline_statistics_spec.rb b/spec/requests/api/graphql/project/project_pipeline_statistics_spec.rb
new file mode 100644
index 00000000000..0f495f3e671
--- /dev/null
+++ b/spec/requests/api/graphql/project/project_pipeline_statistics_spec.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'rendering project pipeline statistics' do
+ include GraphqlHelpers
+
+ let_it_be(:project) { create(:project) }
+ let(:user) { create(:user) }
+
+ let(:fields) do
+ <<~QUERY
+ weekPipelinesTotals
+ weekPipelinesLabels
+ monthPipelinesLabels
+ monthPipelinesTotals
+ yearPipelinesLabels
+ yearPipelinesTotals
+ QUERY
+ end
+
+ let(:query) do
+ graphql_query_for('project',
+ { 'fullPath' => project.full_path },
+ query_graphql_field('pipelineAnalytics', {}, fields))
+ end
+
+ before do
+ project.add_maintainer(user)
+ end
+
+ it_behaves_like 'a working graphql query' do
+ before do
+ post_graphql(query, current_user: user)
+ end
+ end
+
+ it "contains two arrays of 8 elements each for the week pipelines" do
+ post_graphql(query, current_user: user)
+
+ expect(graphql_data_at(:project, :pipelineAnalytics, :weekPipelinesTotals).length).to eq(8)
+ expect(graphql_data_at(:project, :pipelineAnalytics, :weekPipelinesLabels).length).to eq(8)
+ end
+
+ it "contains two arrays of 31 elements each for the month pipelines" do
+ post_graphql(query, current_user: user)
+
+ expect(graphql_data_at(:project, :pipelineAnalytics, :monthPipelinesTotals).length).to eq(31)
+ expect(graphql_data_at(:project, :pipelineAnalytics, :monthPipelinesLabels).length).to eq(31)
+ end
+
+ it "contains two arrays of 13 elements each for the year pipelines" do
+ post_graphql(query, current_user: user)
+
+ expect(graphql_data_at(:project, :pipelineAnalytics, :yearPipelinesTotals).length).to eq(13)
+ expect(graphql_data_at(:project, :pipelineAnalytics, :yearPipelinesLabels).length).to eq(13)
+ end
+end
diff --git a/spec/requests/api/graphql/project/release_spec.rb b/spec/requests/api/graphql/project/release_spec.rb
index 57dbe258ce4..99b15ff00b1 100644
--- a/spec/requests/api/graphql/project/release_spec.rb
+++ b/spec/requests/api/graphql/project/release_spec.rb
@@ -36,7 +36,7 @@ RSpec.describe 'Query.project(fullPath).release(tagName)' do
let(:path) { path_prefix }
let(:release_fields) do
- query_graphql_field(%{
+ %{
tagName
tagPath
description
@@ -45,7 +45,7 @@ RSpec.describe 'Query.project(fullPath).release(tagName)' do
createdAt
releasedAt
upcomingRelease
- })
+ }
end
before do
@@ -233,7 +233,7 @@ RSpec.describe 'Query.project(fullPath).release(tagName)' do
let(:path) { path_prefix }
let(:release_fields) do
- query_graphql_field('description')
+ 'description'
end
before do
@@ -394,10 +394,10 @@ RSpec.describe 'Query.project(fullPath).release(tagName)' do
let(:current_user) { developer }
let(:release_fields) do
- query_graphql_field(%{
+ %{
releasedAt
upcomingRelease
- })
+ }
end
before do
diff --git a/spec/requests/api/graphql/project/terraform/states_spec.rb b/spec/requests/api/graphql/project/terraform/states_spec.rb
index 8b67b549efa..2879530acc5 100644
--- a/spec/requests/api/graphql/project/terraform/states_spec.rb
+++ b/spec/requests/api/graphql/project/terraform/states_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe 'query terraform states' do
include GraphqlHelpers
+ include ::API::Helpers::RelatedResourcesHelpers
let_it_be(:project) { create(:project) }
let_it_be(:terraform_state) { create(:terraform_state, :with_version, :locked, project: project) }
@@ -23,6 +24,8 @@ RSpec.describe 'query terraform states' do
latestVersion {
id
+ downloadPath
+ serial
createdAt
updatedAt
@@ -50,22 +53,32 @@ RSpec.describe 'query terraform states' do
post_graphql(query, current_user: current_user)
end
- it 'returns terraform state data', :aggregate_failures do
- state = data.dig('nodes', 0)
- version = state['latestVersion']
-
- expect(state['id']).to eq(terraform_state.to_global_id.to_s)
- expect(state['name']).to eq(terraform_state.name)
- expect(state['lockedAt']).to eq(terraform_state.locked_at.iso8601)
- expect(state['createdAt']).to eq(terraform_state.created_at.iso8601)
- expect(state['updatedAt']).to eq(terraform_state.updated_at.iso8601)
- expect(state.dig('lockedByUser', 'id')).to eq(terraform_state.locked_by_user.to_global_id.to_s)
-
- expect(version['id']).to eq(latest_version.to_global_id.to_s)
- expect(version['createdAt']).to eq(latest_version.created_at.iso8601)
- expect(version['updatedAt']).to eq(latest_version.updated_at.iso8601)
- expect(version.dig('createdByUser', 'id')).to eq(latest_version.created_by_user.to_global_id.to_s)
- expect(version.dig('job', 'name')).to eq(latest_version.build.name)
+ it 'returns terraform state data' do
+ download_path = expose_path(
+ api_v4_projects_terraform_state_versions_path(
+ id: project.id,
+ name: terraform_state.name,
+ serial: latest_version.version
+ )
+ )
+
+ expect(data['nodes']).to contain_exactly({
+ 'id' => global_id_of(terraform_state),
+ 'name' => terraform_state.name,
+ 'lockedAt' => terraform_state.locked_at.iso8601,
+ 'createdAt' => terraform_state.created_at.iso8601,
+ 'updatedAt' => terraform_state.updated_at.iso8601,
+ 'lockedByUser' => { 'id' => global_id_of(terraform_state.locked_by_user) },
+ 'latestVersion' => {
+ 'id' => eq(global_id_of(latest_version)),
+ 'serial' => eq(latest_version.version),
+ 'downloadPath' => eq(download_path),
+ 'createdAt' => eq(latest_version.created_at.iso8601),
+ 'updatedAt' => eq(latest_version.updated_at.iso8601),
+ 'createdByUser' => { 'id' => eq(global_id_of(latest_version.created_by_user)) },
+ 'job' => { 'name' => eq(latest_version.build.name) }
+ }
+ })
end
it 'returns count of terraform states' do
diff --git a/spec/requests/api/graphql/project_query_spec.rb b/spec/requests/api/graphql/project_query_spec.rb
index 4b8ffb0675c..b29f9ae913f 100644
--- a/spec/requests/api/graphql/project_query_spec.rb
+++ b/spec/requests/api/graphql/project_query_spec.rb
@@ -5,36 +5,34 @@ require 'spec_helper'
RSpec.describe 'getting project information' do
include GraphqlHelpers
- let(:group) { create(:group) }
- let(:project) { create(:project, :repository, group: group) }
- let(:current_user) { create(:user) }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, :repository, group: group) }
+ let_it_be(:current_user) { create(:user) }
+ let(:fields) { all_graphql_fields_for(Project, max_depth: 2, excluded: %w(jiraImports services)) }
let(:query) do
- graphql_query_for(
- 'project',
- { 'fullPath' => project.full_path },
- all_graphql_fields_for('project'.to_s.classify, excluded: %w(jiraImports services))
- )
+ graphql_query_for(:project, { full_path: project.full_path }, fields)
end
context 'when the user has full access to the project' do
let(:full_access_query) do
- graphql_query_for('project', 'fullPath' => project.full_path)
+ graphql_query_for(:project, { full_path: project.full_path },
+ all_graphql_fields_for('Project', max_depth: 2))
end
before do
project.add_maintainer(current_user)
end
- it 'includes the project' do
- post_graphql(query, current_user: current_user)
+ it 'includes the project', :use_clean_rails_memory_store_caching, :request_store do
+ post_graphql(full_access_query, current_user: current_user)
expect(graphql_data['project']).not_to be_nil
end
end
- context 'when the user has access to the project' do
- before do
+ context 'when the user has access to the project', :use_clean_rails_memory_store_caching, :request_store do
+ before_all do
project.add_developer(current_user)
end
@@ -55,10 +53,12 @@ RSpec.describe 'getting project information' do
create(:ci_pipeline, project: project)
end
+ let(:fields) { query_nodes(:pipelines) }
+
it 'is included in the pipelines connection' do
post_graphql(query, current_user: current_user)
- expect(graphql_data['project']['pipelines']['edges'].size).to eq(1)
+ expect(graphql_data_at(:project, :pipelines, :nodes)).to contain_exactly(a_kind_of(Hash))
end
end
@@ -109,7 +109,7 @@ RSpec.describe 'getting project information' do
end
describe 'performance' do
- before do
+ before_all do
project.add_developer(current_user)
mrs = create_list(:merge_request, 10, :closed, :with_head_pipeline,
source_project: project,
@@ -151,8 +151,9 @@ RSpec.describe 'getting project information' do
)))
end
- it 'can lookahead to eliminate N+1 queries', :use_clean_rails_memory_store_caching, :request_store do
- expect { run_query(10) }.to issue_same_number_of_queries_as { run_query(1) }.or_fewer.ignoring_cached_queries
+ it 'can lookahead to eliminate N+1 queries' do
+ baseline = ActiveRecord::QueryRecorder.new { run_query(1) }
+ expect { run_query(10) }.not_to exceed_query_limit(baseline)
end
end
diff --git a/spec/requests/api/graphql/user_query_spec.rb b/spec/requests/api/graphql/user_query_spec.rb
index 8c45a67cb0f..60520906e87 100644
--- a/spec/requests/api/graphql/user_query_spec.rb
+++ b/spec/requests/api/graphql/user_query_spec.rb
@@ -58,9 +58,25 @@ RSpec.describe 'getting user information' do
source_project: project_b, author: user)
end
+ let_it_be(:reviewed_mr) do
+ create(:merge_request, :unique_branches, :unique_author,
+ source_project: project_a, reviewers: [user])
+ end
+
+ let_it_be(:reviewed_mr_b) do
+ create(:merge_request, :unique_branches, :unique_author,
+ source_project: project_b, reviewers: [user])
+ end
+
+ let_it_be(:reviewed_mr_c) do
+ create(:merge_request, :unique_branches, :unique_author,
+ source_project: project_b, reviewers: [user])
+ end
+
let(:current_user) { authorised_user }
let(:authored_mrs) { graphql_data_at(:user, :authored_merge_requests, :nodes) }
let(:assigned_mrs) { graphql_data_at(:user, :assigned_merge_requests, :nodes) }
+ let(:reviewed_mrs) { graphql_data_at(:user, :review_requested_merge_requests, :nodes) }
let(:user_params) { { username: user.username } }
before do
@@ -82,7 +98,8 @@ RSpec.describe 'getting user information' do
'username' => presenter.username,
'webUrl' => presenter.web_url,
'avatarUrl' => presenter.avatar_url,
- 'email' => presenter.public_email
+ 'email' => presenter.public_email,
+ 'publicEmail' => presenter.public_email
))
expect(graphql_data['user']['status']).to match(
@@ -156,6 +173,23 @@ RSpec.describe 'getting user information' do
)
end
end
+
+ context 'filtering by reviewer' do
+ let(:reviewer) { create(:user) }
+ let(:mr_args) { { reviewer_username: reviewer.username } }
+
+ it 'finds the assigned mrs' do
+ assigned_mr_b.reviewers << reviewer
+ assigned_mr_c.reviewers << reviewer
+
+ post_graphql(query, current_user: current_user)
+
+ expect(assigned_mrs).to contain_exactly(
+ a_hash_including('id' => global_id_of(assigned_mr_b)),
+ a_hash_including('id' => global_id_of(assigned_mr_c))
+ )
+ end
+ end
end
context 'the current user does not have access' do
@@ -167,6 +201,95 @@ RSpec.describe 'getting user information' do
end
end
+ describe 'reviewRequestedMergeRequests' do
+ let(:user_fields) do
+ query_graphql_field(:review_requested_merge_requests, mr_args, 'nodes { id }')
+ end
+
+ let(:mr_args) { nil }
+
+ it_behaves_like 'a working graphql query'
+
+ it 'can be found' do
+ expect(reviewed_mrs).to contain_exactly(
+ a_hash_including('id' => global_id_of(reviewed_mr)),
+ a_hash_including('id' => global_id_of(reviewed_mr_b)),
+ a_hash_including('id' => global_id_of(reviewed_mr_c))
+ )
+ end
+
+ context 'applying filters' do
+ context 'filtering by IID without specifying a project' do
+ let(:mr_args) do
+ { iids: [reviewed_mr_b.iid.to_s] }
+ end
+
+ it 'return an argument error that mentions the missing fields' do
+ expect_graphql_errors_to_include(/projectPath/)
+ end
+ end
+
+ context 'filtering by project path and IID' do
+ let(:mr_args) do
+ { project_path: project_b.full_path, iids: [reviewed_mr_b.iid.to_s] }
+ end
+
+ it 'selects the correct MRs' do
+ expect(reviewed_mrs).to contain_exactly(
+ a_hash_including('id' => global_id_of(reviewed_mr_b))
+ )
+ end
+ end
+
+ context 'filtering by project path' do
+ let(:mr_args) do
+ { project_path: project_b.full_path }
+ end
+
+ it 'selects the correct MRs' do
+ expect(reviewed_mrs).to contain_exactly(
+ a_hash_including('id' => global_id_of(reviewed_mr_b)),
+ a_hash_including('id' => global_id_of(reviewed_mr_c))
+ )
+ end
+ end
+
+ context 'filtering by author' do
+ let(:author) { reviewed_mr_b.author }
+ let(:mr_args) { { author_username: author.username } }
+
+ it 'finds the authored mrs' do
+ expect(reviewed_mrs).to contain_exactly(
+ a_hash_including('id' => global_id_of(reviewed_mr_b))
+ )
+ end
+ end
+
+ context 'filtering by assignee' do
+ let(:assignee) { create(:user) }
+ let(:mr_args) { { assignee_username: assignee.username } }
+
+ it 'finds the authored mrs' do
+ reviewed_mr_c.assignees << assignee
+
+ post_graphql(query, current_user: current_user)
+
+ expect(reviewed_mrs).to contain_exactly(
+ a_hash_including('id' => global_id_of(reviewed_mr_c))
+ )
+ end
+ end
+ end
+
+ context 'the current user does not have access' do
+ let(:current_user) { unauthorized_user }
+
+ it 'cannot be found' do
+ expect(reviewed_mrs).to be_empty
+ end
+ end
+ end
+
describe 'authoredMergeRequests' do
let(:user_fields) do
query_graphql_field(:authored_merge_requests, mr_args, 'nodes { id }')
@@ -212,6 +335,23 @@ RSpec.describe 'getting user information' do
end
end
+ context 'filtering by reviewer' do
+ let(:reviewer) { create(:user) }
+ let(:mr_args) { { reviewer_username: reviewer.username } }
+
+ it 'finds the assigned mrs' do
+ authored_mr_b.reviewers << reviewer
+ authored_mr_c.reviewers << reviewer
+
+ post_graphql(query, current_user: current_user)
+
+ expect(authored_mrs).to contain_exactly(
+ a_hash_including('id' => global_id_of(authored_mr_b)),
+ a_hash_including('id' => global_id_of(authored_mr_c))
+ )
+ end
+ end
+
context 'filtering by project path and IID' do
let(:mr_args) do
{ project_path: project_b.full_path, iids: [authored_mr_b.iid.to_s] }
diff --git a/spec/requests/api/graphql/users_spec.rb b/spec/requests/api/graphql/users_spec.rb
index 91ac206676b..72d86c10df1 100644
--- a/spec/requests/api/graphql/users_spec.rb
+++ b/spec/requests/api/graphql/users_spec.rb
@@ -59,20 +59,16 @@ RSpec.describe 'Users' do
describe 'sorting and pagination' do
let_it_be(:data_path) { [:users] }
- def pagination_query(params, page_info)
- graphql_query_for("users", params, "#{page_info} edges { node { id } }")
- end
-
- def pagination_results_data(data)
- data.map { |user| user.dig('node', 'id') }
+ def pagination_query(params)
+ graphql_query_for(:users, params, "#{page_info} nodes { id }")
end
context 'when sorting by created_at' do
- let_it_be(:ascending_users) { [user3, user2, user1, current_user].map(&:to_global_id).map(&:to_s) }
+ let_it_be(:ascending_users) { [user3, user2, user1, current_user].map { |u| global_id_of(u) } }
context 'when ascending' do
it_behaves_like 'sorted paginated query' do
- let(:sort_param) { 'created_asc' }
+ let(:sort_param) { :CREATED_ASC }
let(:first_param) { 1 }
let(:expected_results) { ascending_users }
end
@@ -80,7 +76,7 @@ RSpec.describe 'Users' do
context 'when descending' do
it_behaves_like 'sorted paginated query' do
- let(:sort_param) { 'created_desc' }
+ let(:sort_param) { :CREATED_DESC }
let(:first_param) { 1 }
let(:expected_results) { ascending_users.reverse }
end